c428661601
Phases 3, 4, 5, 6 of the multi-platform distribution plan. ## create-release.yml rewrite - Replace deprecated actions/create-release@v1 + upload-release-asset@v1 with softprops/action-gh-release@v2 (SHA-pinned) - Expand build-desktop matrix: macos-13 (Intel), macos-14 (ARM), windows-latest, ubuntu-latest × 2 legs (deb+rpm, AppImage+tar.gz) - Each matrix job uploads directly to release (no artifact round-trip — saves ~10 min + 1.5GB transfer per run) - Inline portable archives: Windows .zip via 7z, Linux .tar.gz via tar - linuxdeploy SHA-verified fetch for AppImage builds (not `continuous` tag) - Per-asset size budget: hard fail at 1 GB per asset - prerelease inferred from tag regex (-rc|-beta|-alpha|-dev|-snapshot) - workflow_dispatch dry_run input: builds all assets without publishing, skips Android + bump workflows - Tag-vs-libs.versions.toml assertion as first step in each matrix job - Android + Quartz jobs preserved; migrated to softprops/action-gh-release@v2 ## Package manager bump workflows (Homebrew + Winget) - .github/workflows/bump-homebrew.yml — macauley/action-homebrew-bump-cask on ubuntu-latest (saves macOS runner quota). Cask name: `amethyst-nostr`. - .github/workflows/bump-winget.yml — vedantmgoyal9/winget-releaser on windows-latest. PackageIdentifier: `VitorPamplona.Amethyst`. - Shared composite action .github/actions/assert-stable-release rejects draft/prerelease/malformed-tag releases at action boundary (defense in depth vs workflow-level `if:` alone). - Both workflows auto-open `release-ops`-labeled GH Issues on failure. - Concurrency groups per tag prevent re-fire races. - AUR + Scoop deferred to follow-up PR (unresolved ownership questions). ## Documentation - BUILDING.md: per-platform build commands, asset naming contract, release runbook, bootstrap runbook, troubleshooting, uninstall paths, incident response, fallback plans (macos-13 retirement, Homebrew Sept 2026 deadline) - README: expanded Download section with per-OS install matrix for 7 formats + 2 package managers. Deploying section points at BUILDING.md. ## Supply chain - .github/dependabot.yml: monthly bumps for github-actions ecosystem - All new third-party actions SHA-pinned: - softprops/action-gh-release v2.6.2 - macauley/action-homebrew-bump-cask v4.0.0 - vedantmgoyal9/winget-releaser v2 - nick-fields/retry v3.0.2 - linuxdeploy binary SHA256-verified against pinned release tag
314 lines
13 KiB
YAML
314 lines
13 KiB
YAML
name: Create Release Assets
|
|
|
|
on:
|
|
push:
|
|
tags:
|
|
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
|
workflow_dispatch:
|
|
inputs:
|
|
dry_run:
|
|
description: 'Dry run: build assets but do not publish to GH Release or trigger bump workflows'
|
|
type: boolean
|
|
default: false
|
|
test_tag:
|
|
description: 'Synthetic tag name for dry-run (e.g. v1.08.0-dryrun); ignored on tag push'
|
|
type: string
|
|
default: 'v0.0.0-dryrun'
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
env:
|
|
# Asset naming contract: amethyst-desktop-<version>-<family>-<arch>.<ext>
|
|
# Single source of truth in scripts/asset-name.sh.
|
|
# linuxdeploy pinned release — bump via Dependabot, verify SHA256 via env var below.
|
|
LINUXDEPLOY_URL: https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20240109-1/linuxdeploy-x86_64.AppImage
|
|
LINUXDEPLOY_SHA256: c86d6540f1df31061f02f539a2d3445f8d7f85cc3994eee1e74cd1ac97b76df0
|
|
|
|
jobs:
|
|
# ---------------------------------------------------------------------------
|
|
# Desktop build matrix. Each leg uploads directly to the GH Release via
|
|
# softprops/action-gh-release@v2 (upsert by tag_name). No artifact round-trip.
|
|
# ---------------------------------------------------------------------------
|
|
build-desktop:
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- { os: macos-13, arch: x64, family: macos, tasks: "packageReleaseDmg" }
|
|
- { os: macos-14, arch: arm64, family: macos, tasks: "packageReleaseDmg" }
|
|
- { os: windows-latest, arch: x64, family: windows, tasks: "packageReleaseMsi createReleaseDistributable" }
|
|
- { os: ubuntu-latest, arch: x64, family: linux, tasks: "packageReleaseDeb packageReleaseRpm" }
|
|
- { os: ubuntu-latest, arch: x64, family: linux-portable, tasks: "createReleaseAppImage createReleaseDistributable" }
|
|
runs-on: ${{ matrix.os }}
|
|
timeout-minutes: 45
|
|
defaults:
|
|
run:
|
|
shell: bash
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v6
|
|
|
|
- name: Set up JDK 21
|
|
uses: actions/setup-java@v5
|
|
with:
|
|
distribution: 'zulu'
|
|
java-version: 21
|
|
|
|
- name: Resolve tag + version
|
|
id: ver
|
|
env:
|
|
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
|
|
TEST_TAG: ${{ github.event.inputs.test_tag || '' }}
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
|
|
TAG="${TEST_TAG:-v0.0.0-dryrun}"
|
|
else
|
|
TAG="${GITHUB_REF_NAME}"
|
|
fi
|
|
VER="${TAG#v}"
|
|
TOML_VER=$(grep -E '^app\s*=' gradle/libs.versions.toml | head -1 | cut -d'"' -f2)
|
|
# On dry-run we only require that TOML has a version; on real tag push we require exact match.
|
|
if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then
|
|
if [[ "$TOML_VER" != "$VER" ]]; then
|
|
echo "::error::gradle/libs.versions.toml app=$TOML_VER but tag is $TAG"
|
|
exit 1
|
|
fi
|
|
fi
|
|
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
|
echo "version=$VER" >> "$GITHUB_OUTPUT"
|
|
echo "toml=$TOML_VER" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Install RPM tooling (linux families)
|
|
if: startsWith(matrix.family, 'linux')
|
|
run: sudo apt-get update && sudo apt-get install -y rpm fakeroot
|
|
|
|
- name: Fetch linuxdeploy (linux-portable only, SHA-verified)
|
|
if: matrix.family == 'linux-portable'
|
|
run: |
|
|
set -euo pipefail
|
|
curl -fsSL --retry 3 "$LINUXDEPLOY_URL" -o packaging/appimage/linuxdeploy-x86_64.AppImage
|
|
actual=$(sha256sum packaging/appimage/linuxdeploy-x86_64.AppImage | awk '{print $1}')
|
|
if [[ "$actual" != "$LINUXDEPLOY_SHA256" ]]; then
|
|
echo "::error::linuxdeploy SHA256 mismatch. Expected $LINUXDEPLOY_SHA256, got $actual"
|
|
exit 1
|
|
fi
|
|
chmod +x packaging/appimage/linuxdeploy-x86_64.AppImage
|
|
# linuxdeploy needs FUSE; on newer runners, --appimage-extract-and-run is required.
|
|
# Bypass FUSE requirement by pre-extracting the AppImage.
|
|
(cd packaging/appimage && ./linuxdeploy-x86_64.AppImage --appimage-extract >/dev/null && \
|
|
mv squashfs-root linuxdeploy-extracted && \
|
|
ln -sf linuxdeploy-extracted/AppRun linuxdeploy-x86_64.bin && \
|
|
chmod +x linuxdeploy-x86_64.bin)
|
|
|
|
- name: Build desktop artifacts
|
|
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
|
|
with:
|
|
max_attempts: 2
|
|
timeout_minutes: 40
|
|
command: ./gradlew --no-daemon :desktopApp:${{ matrix.tasks }}
|
|
|
|
- name: Build portable archives (windows + linux-portable)
|
|
if: matrix.family == 'windows' || matrix.family == 'linux-portable'
|
|
run: |
|
|
set -euo pipefail
|
|
VER="${{ steps.ver.outputs.version }}"
|
|
APP="desktopApp/build/compose/binaries/main-release/app"
|
|
mkdir -p desktopApp/build/portable
|
|
if [[ "${{ matrix.family }}" == "windows" ]]; then
|
|
( cd "$APP" && 7z a -tzip "../../../../portable/amethyst-desktop-${VER}-windows-x64.zip" Amethyst/ )
|
|
else
|
|
( cd "$APP" && tar czf "../../../../portable/amethyst-desktop-${VER}-linux-x64.tar.gz" Amethyst/ )
|
|
fi
|
|
|
|
- name: Collect + rename assets
|
|
run: |
|
|
set -euo pipefail
|
|
# shellcheck source=scripts/asset-name.sh
|
|
source scripts/asset-name.sh
|
|
# linux-portable family holds AppImage + tar.gz, but we re-tag with plain `linux` for the asset name.
|
|
FAMILY="${{ matrix.family }}"
|
|
[[ "$FAMILY" == "linux-portable" ]] && FAMILY="linux"
|
|
mkdir -p dist
|
|
collect_assets "$FAMILY" "${{ matrix.arch }}" "${{ steps.ver.outputs.version }}" dist
|
|
|
|
- name: Enforce asset size budget (1 GB per asset)
|
|
run: |
|
|
set -euo pipefail
|
|
fail=0
|
|
for f in dist/*; do
|
|
if [[ -f "$f" ]]; then
|
|
size=$(wc -c < "$f")
|
|
mb=$(( size / 1048576 ))
|
|
if (( size > 1073741824 )); then
|
|
echo "::error file=$f::asset is ${mb} MB — exceeds 1 GB budget"
|
|
fail=1
|
|
else
|
|
echo "OK: $f — ${mb} MB"
|
|
fi
|
|
fi
|
|
done
|
|
[[ "$fail" == 0 ]]
|
|
|
|
- name: Classify release
|
|
id: classify
|
|
run: |
|
|
TAG="${{ steps.ver.outputs.tag }}"
|
|
if [[ "$TAG" =~ -(rc|beta|alpha|dev|dryrun|snapshot) ]]; then
|
|
echo "prerelease=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "prerelease=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Upload to GH Release (skip on dry-run)
|
|
if: github.event_name != 'workflow_dispatch' || github.event.inputs.dry_run != 'true'
|
|
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
|
|
with:
|
|
files: dist/*
|
|
tag_name: ${{ steps.ver.outputs.tag }}
|
|
prerelease: ${{ steps.classify.outputs.prerelease }}
|
|
draft: false
|
|
fail_on_unmatched_files: true
|
|
generate_release_notes: true
|
|
|
|
- name: Dry-run summary
|
|
if: github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'true'
|
|
run: |
|
|
echo "### Dry-run: ${{ matrix.family }}/${{ matrix.arch }}" >> "$GITHUB_STEP_SUMMARY"
|
|
echo "" >> "$GITHUB_STEP_SUMMARY"
|
|
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
|
ls -la dist >> "$GITHUB_STEP_SUMMARY"
|
|
echo '```' >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Android build + sign + direct-upload. Logic preserved from previous workflow;
|
|
# uses softprops/action-gh-release@v2 instead of deprecated upload-release-asset.
|
|
# ---------------------------------------------------------------------------
|
|
deploy-android:
|
|
if: github.event_name != 'workflow_dispatch' # dry-run skips Android (tag-push only)
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 60
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v6
|
|
|
|
- name: Set up JDK 21
|
|
uses: actions/setup-java@v5
|
|
with:
|
|
distribution: 'zulu'
|
|
java-version: 21
|
|
|
|
- name: Cache gradle
|
|
uses: actions/cache@v5
|
|
with:
|
|
path: |
|
|
~/.gradle/caches
|
|
~/.gradle/wrapper
|
|
key: ${{ runner.os }}-android-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
restore-keys: |
|
|
${{ runner.os }}-android-gradle-
|
|
|
|
- name: Build AAB
|
|
run: ./gradlew clean bundleRelease --stacktrace
|
|
|
|
- name: Sign AAB (Google Play)
|
|
uses: r0adkll/sign-android-release@v1
|
|
with:
|
|
releaseDirectory: amethyst/build/outputs/bundle/playRelease
|
|
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
|
alias: ${{ secrets.KEY_ALIAS }}
|
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
|
env:
|
|
BUILD_TOOLS_VERSION: "36.0.0"
|
|
|
|
- name: Sign AAB (F-Droid)
|
|
uses: r0adkll/sign-android-release@v1
|
|
with:
|
|
releaseDirectory: amethyst/build/outputs/bundle/fdroidRelease
|
|
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
|
alias: ${{ secrets.KEY_ALIAS }}
|
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
|
env:
|
|
BUILD_TOOLS_VERSION: "36.0.0"
|
|
|
|
- name: Build APK
|
|
run: ./gradlew assembleRelease --stacktrace
|
|
|
|
- name: Sign APK (Google Play)
|
|
uses: r0adkll/sign-android-release@v1
|
|
with:
|
|
releaseDirectory: amethyst/build/outputs/apk/play/release
|
|
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
|
alias: ${{ secrets.KEY_ALIAS }}
|
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
|
env:
|
|
BUILD_TOOLS_VERSION: "36.0.0"
|
|
|
|
- name: Sign APK (F-Droid)
|
|
uses: r0adkll/sign-android-release@v1
|
|
with:
|
|
releaseDirectory: amethyst/build/outputs/apk/fdroid/release
|
|
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
|
alias: ${{ secrets.KEY_ALIAS }}
|
|
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
|
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
|
env:
|
|
BUILD_TOOLS_VERSION: "36.0.0"
|
|
|
|
- name: Collect Android assets (rename to canonical scheme)
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p dist
|
|
TAG="${GITHUB_REF_NAME}"
|
|
|
|
# Play APKs (5 variants)
|
|
for variant in universal x86 x86_64 arm64-v8a armeabi-v7a; do
|
|
cp "amethyst/build/outputs/apk/play/release/amethyst-play-${variant}-release-unsigned-signed.apk" \
|
|
"dist/amethyst-googleplay-${variant}-${TAG}.apk"
|
|
done
|
|
|
|
# F-Droid APKs (5 variants)
|
|
for variant in universal x86 x86_64 arm64-v8a armeabi-v7a; do
|
|
cp "amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-${variant}-release-unsigned-signed.apk" \
|
|
"dist/amethyst-fdroid-${variant}-${TAG}.apk"
|
|
done
|
|
|
|
# AABs
|
|
cp "amethyst/build/outputs/bundle/playRelease/amethyst-play-release.aab" \
|
|
"dist/amethyst-googleplay-${TAG}.aab"
|
|
cp "amethyst/build/outputs/bundle/fdroidRelease/amethyst-fdroid-release.aab" \
|
|
"dist/amethyst-fdroid-${TAG}.aab"
|
|
ls -la dist
|
|
|
|
- name: Classify release
|
|
id: classify
|
|
run: |
|
|
TAG="${GITHUB_REF_NAME}"
|
|
if [[ "$TAG" =~ -(rc|beta|alpha|dev|snapshot) ]]; then
|
|
echo "prerelease=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "prerelease=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Upload Android assets to GH Release
|
|
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
|
|
with:
|
|
files: dist/*
|
|
tag_name: ${{ github.ref_name }}
|
|
prerelease: ${{ steps.classify.outputs.prerelease }}
|
|
draft: false
|
|
fail_on_unmatched_files: true
|
|
generate_release_notes: true
|
|
|
|
- name: Publish Quartz Lib
|
|
run: ./gradlew publishAllPublicationsToMavenCentral --no-configuration-cache
|
|
env:
|
|
ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }}
|
|
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }}
|
|
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }}
|
|
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}
|