feat(release): expand desktop distribution to 8 assets + 2 package managers
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
This commit is contained in:
@@ -0,0 +1,44 @@
|
|||||||
|
name: Assert Stable Release
|
||||||
|
description: >-
|
||||||
|
Defense-in-depth guard for package-manager bump workflows. Re-validates
|
||||||
|
tag format, prerelease flag, and draft status before invoking third-party
|
||||||
|
actions that hold write credentials to external package manager repos
|
||||||
|
(Homebrew, Winget, AUR, Scoop). Prevents RC builds from reaching stable
|
||||||
|
channels even if the `release.released` event gating is bypassed.
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Assert release is stable
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
TAG: ${{ github.event.release.tag_name }}
|
||||||
|
IS_PRERELEASE: ${{ github.event.release.prerelease }}
|
||||||
|
IS_DRAFT: ${{ github.event.release.draft }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "tag=$TAG prerelease=$IS_PRERELEASE draft=$IS_DRAFT"
|
||||||
|
|
||||||
|
# Reject prerelease suffix even if GitHub's flag says false.
|
||||||
|
if [[ "$TAG" =~ -(rc|beta|alpha|dev|snapshot) ]]; then
|
||||||
|
echo "::error::Tag $TAG contains prerelease suffix; refusing bump"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Enforce strict vMAJOR.MINOR.PATCH format.
|
||||||
|
if ! [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||||
|
echo "::error::Tag $TAG does not match vMAJOR.MINOR.PATCH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Draft releases must never trigger bumps.
|
||||||
|
if [[ "$IS_DRAFT" == "true" ]]; then
|
||||||
|
echo "::error::Release is draft; refusing bump"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Prerelease flag cross-check (belt-and-suspenders with workflow-level `if:`).
|
||||||
|
if [[ "$IS_PRERELEASE" == "true" ]]; then
|
||||||
|
echo "::error::Release is prerelease; refusing bump"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
# Auto-update SHA-pinned GitHub Actions across all workflows.
|
||||||
|
# Required for supply-chain safety — SHA pins only age well with active bumps.
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: monthly
|
||||||
|
labels:
|
||||||
|
- release-ops
|
||||||
|
- dependencies
|
||||||
|
commit-message:
|
||||||
|
prefix: chore(actions)
|
||||||
|
groups:
|
||||||
|
actions:
|
||||||
|
patterns:
|
||||||
|
- "*"
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
name: Bump Homebrew Cask
|
||||||
|
|
||||||
|
# Fires when a GH Release is published (not draft, not prerelease).
|
||||||
|
# `release.types: [released]` event fires only for stable releases — still
|
||||||
|
# double-checked by .github/actions/assert-stable-release for defense-in-depth.
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [released]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Release tag to bump (for manual recovery)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
# Serialize bumps per tag; do not cancel in-progress bumps.
|
||||||
|
group: bump-homebrew-${{ github.event.release.tag_name || inputs.tag }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bump:
|
||||||
|
if: github.event_name == 'workflow_dispatch' || github.event.release.prerelease == false
|
||||||
|
runs-on: ubuntu-latest # brew runs on Linux — saves macOS runner quota
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Re-assert stable release
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
uses: ./.github/actions/assert-stable-release
|
||||||
|
|
||||||
|
- name: Bump cask (push-or-update PR)
|
||||||
|
uses: macauley/action-homebrew-bump-cask@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.HOMEBREW_TOKEN }}
|
||||||
|
tap: homebrew/cask
|
||||||
|
cask: amethyst-nostr
|
||||||
|
tag: ${{ github.event.release.tag_name || inputs.tag }}
|
||||||
|
|
||||||
|
- name: Report failure
|
||||||
|
if: failure()
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const tag = context.payload.release?.tag_name || context.payload.inputs?.tag || 'unknown';
|
||||||
|
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||||
|
await github.rest.issues.create({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
title: `[release-ops] bump-homebrew failed for ${tag}`,
|
||||||
|
body: [
|
||||||
|
`Homebrew cask bump failed for release \`${tag}\`.`,
|
||||||
|
``,
|
||||||
|
`- Run: ${runUrl}`,
|
||||||
|
`- Channel: Homebrew Cask (\`amethyst-nostr\`)`,
|
||||||
|
``,
|
||||||
|
`Recovery options:`,
|
||||||
|
`1. Re-run the workflow once underlying issue is fixed`,
|
||||||
|
`2. Manually run \`brew bump-cask-pr amethyst-nostr --version ${tag.replace(/^v/, '')}\``,
|
||||||
|
`3. File PR directly against Homebrew/homebrew-cask`
|
||||||
|
].join('\n'),
|
||||||
|
labels: ['release-ops', 'bug']
|
||||||
|
});
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
name: Bump Winget Manifest
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [released]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Release tag to submit (for manual recovery)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: bump-winget-${{ github.event.release.tag_name || inputs.tag }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bump:
|
||||||
|
if: github.event_name == 'workflow_dispatch' || github.event.release.prerelease == false
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Re-assert stable release
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
uses: ./.github/actions/assert-stable-release
|
||||||
|
|
||||||
|
- name: Submit manifest to winget-pkgs
|
||||||
|
uses: vedantmgoyal9/winget-releaser@4ffc7888bffd451b357355dc214d43bb9f23917e # v2
|
||||||
|
with:
|
||||||
|
identifier: VitorPamplona.Amethyst
|
||||||
|
version: ${{ github.event.release.tag_name || inputs.tag }}
|
||||||
|
# Asset naming contract: scripts/asset-name.sh
|
||||||
|
installers-regex: '^amethyst-desktop-.*-windows-x64\.msi$'
|
||||||
|
token: ${{ secrets.WINGET_TOKEN }}
|
||||||
|
|
||||||
|
- name: Report failure
|
||||||
|
if: failure()
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const tag = context.payload.release?.tag_name || context.payload.inputs?.tag || 'unknown';
|
||||||
|
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
|
||||||
|
await github.rest.issues.create({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
title: `[release-ops] bump-winget failed for ${tag}`,
|
||||||
|
body: [
|
||||||
|
`Winget manifest submission failed for release \`${tag}\`.`,
|
||||||
|
``,
|
||||||
|
`- Run: ${runUrl}`,
|
||||||
|
`- Channel: Winget (\`VitorPamplona.Amethyst\`)`,
|
||||||
|
``,
|
||||||
|
`Recovery options:`,
|
||||||
|
`1. Re-run the workflow once underlying issue is fixed`,
|
||||||
|
`2. Manually submit via \`wingetcreate update VitorPamplona.Amethyst -v ${tag.replace(/^v/, '')}\``,
|
||||||
|
`3. File PR directly against microsoft/winget-pkgs`
|
||||||
|
].join('\n'),
|
||||||
|
labels: ['release-ops', 'bug']
|
||||||
|
});
|
||||||
@@ -3,31 +3,192 @@ name: Create Release Assets
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
- '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:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
jobs:
|
env:
|
||||||
create-release:
|
# Asset naming contract: amethyst-desktop-<version>-<family>-<arch>.<ext>
|
||||||
runs-on: ubuntu-latest
|
# Single source of truth in scripts/asset-name.sh.
|
||||||
outputs:
|
# linuxdeploy pinned release — bump via Dependabot, verify SHA256 via env var below.
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
LINUXDEPLOY_URL: https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20240109-1/linuxdeploy-x86_64.AppImage
|
||||||
steps:
|
LINUXDEPLOY_SHA256: c86d6540f1df31061f02f539a2d3445f8d7f85cc3994eee1e74cd1ac97b76df0
|
||||||
- name: Create Release
|
|
||||||
id: create_release
|
|
||||||
uses: actions/create-release@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
tag_name: ${{ github.ref }}
|
|
||||||
release_name: Release ${{ github.ref }}
|
|
||||||
draft: false
|
|
||||||
prerelease: true
|
|
||||||
|
|
||||||
|
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:
|
deploy-android:
|
||||||
needs: create-release
|
if: github.event_name != 'workflow_dispatch' # dry-run skips Android (tag-push only)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
@@ -44,9 +205,9 @@ jobs:
|
|||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
~/.gradle/wrapper
|
~/.gradle/wrapper
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
key: ${{ runner.os }}-android-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-gradle-
|
${{ runner.os }}-android-gradle-
|
||||||
|
|
||||||
- name: Build AAB
|
- name: Build AAB
|
||||||
run: ./gradlew clean bundleRelease --stacktrace
|
run: ./gradlew clean bundleRelease --stacktrace
|
||||||
@@ -98,141 +259,50 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
BUILD_TOOLS_VERSION: "36.0.0"
|
BUILD_TOOLS_VERSION: "36.0.0"
|
||||||
|
|
||||||
# Google Play APK
|
- name: Collect Android assets (rename to canonical scheme)
|
||||||
- name: Upload Play APK Universal Asset
|
run: |
|
||||||
id: upload-release-asset-play-universal-apk
|
set -euo pipefail
|
||||||
uses: actions/upload-release-asset@v1
|
mkdir -p dist
|
||||||
env:
|
TAG="${GITHUB_REF_NAME}"
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-universal-release-unsigned-signed.apk
|
|
||||||
asset_name: amethyst-googleplay-universal-${{ github.ref_name }}.apk
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Upload Play APK x86 Asset
|
# Play APKs (5 variants)
|
||||||
id: upload-release-asset-play-x86-apk
|
for variant in universal x86 x86_64 arm64-v8a armeabi-v7a; do
|
||||||
uses: actions/upload-release-asset@v1
|
cp "amethyst/build/outputs/apk/play/release/amethyst-play-${variant}-release-unsigned-signed.apk" \
|
||||||
env:
|
"dist/amethyst-googleplay-${variant}-${TAG}.apk"
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
done
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86-release-unsigned-signed.apk
|
|
||||||
asset_name: amethyst-googleplay-x86-${{ github.ref_name }}.apk
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Upload Play APK x86_64 Asset
|
# F-Droid APKs (5 variants)
|
||||||
id: upload-release-asset-play-x86-64-apk
|
for variant in universal x86 x86_64 arm64-v8a armeabi-v7a; do
|
||||||
uses: actions/upload-release-asset@v1
|
cp "amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-${variant}-release-unsigned-signed.apk" \
|
||||||
env:
|
"dist/amethyst-fdroid-${variant}-${TAG}.apk"
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
done
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-x86_64-release-unsigned-signed.apk
|
|
||||||
asset_name: amethyst-googleplay-x86_64-${{ github.ref_name }}.apk
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Upload Play APK arm64-v8a Asset
|
# AABs
|
||||||
id: upload-release-asset-play-arm64-v8a-apk
|
cp "amethyst/build/outputs/bundle/playRelease/amethyst-play-release.aab" \
|
||||||
uses: actions/upload-release-asset@v1
|
"dist/amethyst-googleplay-${TAG}.aab"
|
||||||
env:
|
cp "amethyst/build/outputs/bundle/fdroidRelease/amethyst-fdroid-release.aab" \
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
"dist/amethyst-fdroid-${TAG}.aab"
|
||||||
with:
|
ls -la dist
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-arm64-v8a-release-unsigned-signed.apk
|
|
||||||
asset_name: amethyst-googleplay-arm64-v8a-${{ github.ref_name }}.apk
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Upload Play APK armeabi-v7a Asset
|
- name: Classify release
|
||||||
id: upload-release-asset-play-armeabi-v7a-apk
|
id: classify
|
||||||
uses: actions/upload-release-asset@v1
|
run: |
|
||||||
env:
|
TAG="${GITHUB_REF_NAME}"
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
if [[ "$TAG" =~ -(rc|beta|alpha|dev|snapshot) ]]; then
|
||||||
with:
|
echo "prerelease=true" >> "$GITHUB_OUTPUT"
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
else
|
||||||
asset_path: amethyst/build/outputs/apk/play/release/amethyst-play-armeabi-v7a-release-unsigned-signed.apk
|
echo "prerelease=false" >> "$GITHUB_OUTPUT"
|
||||||
asset_name: amethyst-googleplay-armeabi-v7a-${{ github.ref_name }}.apk
|
fi
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
# F-Droid APK
|
- name: Upload Android assets to GH Release
|
||||||
- name: Upload F-Droid APK Universal Asset
|
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
|
||||||
id: upload-release-asset-fdroid-universal-apk
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
files: dist/*
|
||||||
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-universal-release-unsigned-signed.apk
|
tag_name: ${{ github.ref_name }}
|
||||||
asset_name: amethyst-fdroid-universal-${{ github.ref_name }}.apk
|
prerelease: ${{ steps.classify.outputs.prerelease }}
|
||||||
asset_content_type: application/zip
|
draft: false
|
||||||
|
fail_on_unmatched_files: true
|
||||||
- name: Upload F-Droid APK x86 Asset
|
generate_release_notes: true
|
||||||
id: upload-release-asset-fdroid-x86-apk
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86-release-unsigned-signed.apk
|
|
||||||
asset_name: amethyst-fdroid-x86-${{ github.ref_name }}.apk
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Upload F-Droid APK x86_64 Asset
|
|
||||||
id: upload-release-asset-fdroid-x86-64-apk
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-x86_64-release-unsigned-signed.apk
|
|
||||||
asset_name: amethyst-fdroid-x86_64-${{ github.ref_name }}.apk
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Upload F-Droid APK arm64-v8a Asset
|
|
||||||
id: upload-release-asset-fdroid-arm64-v8a-apk
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-arm64-v8a-release-unsigned-signed.apk
|
|
||||||
asset_name: amethyst-fdroid-arm64-v8a-${{ github.ref_name }}.apk
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Upload F-Droid APK armeabi-v7a Asset
|
|
||||||
id: upload-release-asset-fdroid-armeabi-v7a-apk
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/apk/fdroid/release/amethyst-fdroid-armeabi-v7a-release-unsigned-signed.apk
|
|
||||||
asset_name: amethyst-fdroid-armeabi-v7a-${{ github.ref_name }}.apk
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
# Google Play AAB
|
|
||||||
- name: Upload Google Play AAB Asset
|
|
||||||
id: upload-release-asset-play-aab
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/bundle/playRelease/amethyst-play-release.aab
|
|
||||||
asset_name: amethyst-googleplay-${{ github.ref_name }}.aab
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
# FDroid AAB
|
|
||||||
- name: Upload F-Droid AAB Asset
|
|
||||||
id: upload-release-asset-fdroid-aab
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: amethyst/build/outputs/bundle/fdroidRelease/amethyst-fdroid-release.aab
|
|
||||||
asset_name: amethyst-fdroid-${{ github.ref_name }}.aab
|
|
||||||
asset_content_type: application/zip
|
|
||||||
|
|
||||||
- name: Publish Quartz Lib
|
- name: Publish Quartz Lib
|
||||||
run: ./gradlew publishAllPublicationsToMavenCentral --no-configuration-cache
|
run: ./gradlew publishAllPublicationsToMavenCentral --no-configuration-cache
|
||||||
@@ -241,65 +311,3 @@ jobs:
|
|||||||
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }}
|
ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }}
|
||||||
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }}
|
ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_PRIVATE_KEY }}
|
||||||
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}
|
ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }}
|
||||||
|
|
||||||
deploy-desktop:
|
|
||||||
needs: create-release
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: ubuntu-latest
|
|
||||||
task: packageDeb
|
|
||||||
format: deb
|
|
||||||
platform: linux
|
|
||||||
- os: macos-latest
|
|
||||||
task: packageDmg
|
|
||||||
format: dmg
|
|
||||||
platform: macos
|
|
||||||
- os: windows-latest
|
|
||||||
task: packageMsi
|
|
||||||
format: msi
|
|
||||||
platform: windows
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
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: Cache gradle
|
|
||||||
uses: actions/cache@v5
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.gradle/caches
|
|
||||||
~/.gradle/wrapper
|
|
||||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-gradle-
|
|
||||||
|
|
||||||
- name: Build Desktop Distribution
|
|
||||||
run: ./gradlew :desktopApp:${{ matrix.task }}
|
|
||||||
|
|
||||||
- name: Find distribution file
|
|
||||||
id: find-dist
|
|
||||||
run: |
|
|
||||||
DIST_FILE=$(find desktopApp/build/compose/binaries/main/${{ matrix.format }} -type f \( -name "*.deb" -o -name "*.dmg" -o -name "*.msi" \) | head -1)
|
|
||||||
echo "path=$DIST_FILE" >> $GITHUB_OUTPUT
|
|
||||||
echo "name=$(basename $DIST_FILE)" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Upload Desktop Distribution to Release
|
|
||||||
uses: actions/upload-release-asset@v1
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
upload_url: ${{ needs.create-release.outputs.upload_url }}
|
|
||||||
asset_path: ${{ steps.find-dist.outputs.path }}
|
|
||||||
asset_name: amethyst-desktop-${{ matrix.platform }}-${{ github.ref_name }}.${{ matrix.format }}
|
|
||||||
asset_content_type: application/octet-stream
|
|
||||||
|
|||||||
+395
@@ -0,0 +1,395 @@
|
|||||||
|
# Building Amethyst Desktop
|
||||||
|
|
||||||
|
This guide covers building Amethyst Desktop from source, the release pipeline,
|
||||||
|
and one-time bootstrap steps for distribution channels.
|
||||||
|
|
||||||
|
- [Prerequisites](#prerequisites)
|
||||||
|
- [Clone + first build](#clone--first-build)
|
||||||
|
- [Per-format build commands](#per-format-build-commands)
|
||||||
|
- [Asset naming contract](#asset-naming-contract)
|
||||||
|
- [Release runbook](#release-runbook)
|
||||||
|
- [Bootstrap runbook (one-time)](#bootstrap-runbook-one-time)
|
||||||
|
- [Troubleshooting installs](#troubleshooting-installs)
|
||||||
|
- [Uninstall + state paths](#uninstall--state-paths)
|
||||||
|
- [Incident response](#incident-response)
|
||||||
|
- [Fallback plans](#fallback-plans)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
All platforms:
|
||||||
|
|
||||||
|
- **JDK 21** (Zulu or Temurin recommended)
|
||||||
|
- **Git**
|
||||||
|
|
||||||
|
Platform-specific:
|
||||||
|
|
||||||
|
- **macOS**: Xcode Command Line Tools (`xcode-select --install`)
|
||||||
|
- **Windows**: WiX Toolset 3.x on PATH (for MSI). `winget install WiXToolset.WiXToolset`
|
||||||
|
- **Linux (all)**: nothing extra for `.deb`; `rpm` + `fakeroot` for `.rpm`; `linuxdeploy` for AppImage
|
||||||
|
|
||||||
|
Install Linux RPM tooling:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Debian/Ubuntu
|
||||||
|
sudo apt-get install -y rpm fakeroot
|
||||||
|
|
||||||
|
# Fedora
|
||||||
|
sudo dnf install -y rpm-build
|
||||||
|
```
|
||||||
|
|
||||||
|
Install linuxdeploy locally (CI fetches its own — SHA-verified):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL -o packaging/appimage/linuxdeploy-x86_64.AppImage \
|
||||||
|
https://github.com/linuxdeploy/linuxdeploy/releases/download/1-alpha-20240109-1/linuxdeploy-x86_64.AppImage
|
||||||
|
chmod +x packaging/appimage/linuxdeploy-x86_64.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Clone + first build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/vitorpamplona/amethyst.git
|
||||||
|
cd amethyst
|
||||||
|
|
||||||
|
# Dev loop (launches Amethyst Desktop)
|
||||||
|
./gradlew :desktopApp:run
|
||||||
|
|
||||||
|
# Package for current OS
|
||||||
|
./gradlew :desktopApp:packageDistributionForCurrentOS
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Per-format build commands
|
||||||
|
|
||||||
|
| Artifact | Command | Output |
|
||||||
|
|---|---|---|
|
||||||
|
| macOS DMG (host arch) | `./gradlew :desktopApp:packageReleaseDmg` | `desktopApp/build/compose/binaries/main-release/dmg/Amethyst-*.dmg` |
|
||||||
|
| Windows MSI | `./gradlew :desktopApp:packageReleaseMsi` | `desktopApp/build/compose/binaries/main-release/msi/Amethyst-*.msi` |
|
||||||
|
| Linux `.deb` | `./gradlew :desktopApp:packageReleaseDeb` | `desktopApp/build/compose/binaries/main-release/deb/amethyst_*.deb` |
|
||||||
|
| Linux `.rpm` | `./gradlew :desktopApp:packageReleaseRpm` | `desktopApp/build/compose/binaries/main-release/rpm/amethyst-*.rpm` |
|
||||||
|
| Linux AppImage | `./gradlew :desktopApp:createReleaseAppImage` | `desktopApp/build/appimage/Amethyst-*-x86_64.AppImage` |
|
||||||
|
| Windows `.zip` portable | See below (inline `7z`) | — |
|
||||||
|
| Linux `.tar.gz` portable | See below (inline `tar`) | — |
|
||||||
|
|
||||||
|
**Inline portable archives** (run after `createReleaseDistributable`):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew :desktopApp:createReleaseDistributable
|
||||||
|
|
||||||
|
# Linux tar.gz
|
||||||
|
VER=$(grep -E '^app\s*=' gradle/libs.versions.toml | head -1 | cut -d'"' -f2)
|
||||||
|
( cd desktopApp/build/compose/binaries/main-release/app \
|
||||||
|
&& tar czf "../../../../portable/amethyst-desktop-${VER}-linux-x64.tar.gz" Amethyst/ )
|
||||||
|
|
||||||
|
# Windows .zip (PowerShell)
|
||||||
|
Compress-Archive -Path desktopApp\build\compose\binaries\main-release\app\Amethyst `
|
||||||
|
-DestinationPath "desktopApp\build\portable\amethyst-desktop-$env:VER-windows-x64.zip"
|
||||||
|
```
|
||||||
|
|
||||||
|
Cross-platform architecture note: **`jpackage` cannot cross-compile**. An Intel
|
||||||
|
DMG must be built on `macos-13` (x64); an ARM DMG must be built on `macos-14`
|
||||||
|
or later. CI runs both.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Asset naming contract
|
||||||
|
|
||||||
|
All GH Release assets follow:
|
||||||
|
|
||||||
|
```
|
||||||
|
amethyst-desktop-<version>-<family>-<arch>.<ext>
|
||||||
|
```
|
||||||
|
|
||||||
|
Where:
|
||||||
|
|
||||||
|
| Field | Values |
|
||||||
|
|---|---|
|
||||||
|
| `<version>` | Tag stripped of leading `v` (e.g. `1.08.0`) |
|
||||||
|
| `<family>` | `macos`, `windows`, `linux` |
|
||||||
|
| `<arch>` | `x64`, `arm64` |
|
||||||
|
| `<ext>` | `dmg`, `msi`, `zip`, `deb`, `rpm`, `AppImage`, `tar.gz` |
|
||||||
|
|
||||||
|
Single source of truth: [`scripts/asset-name.sh`](scripts/asset-name.sh).
|
||||||
|
Package manager manifests (Homebrew cask, Winget) depend on this exact scheme —
|
||||||
|
any change is a breaking contract.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- `amethyst-desktop-1.08.0-macos-x64.dmg`
|
||||||
|
- `amethyst-desktop-1.08.0-macos-arm64.dmg`
|
||||||
|
- `amethyst-desktop-1.08.0-windows-x64.msi`
|
||||||
|
- `amethyst-desktop-1.08.0-linux-x64.AppImage`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Release runbook
|
||||||
|
|
||||||
|
The release flow is driven by a tag push. Every cut ships Android + Desktop +
|
||||||
|
Quartz library in one pipeline.
|
||||||
|
|
||||||
|
1. **Bump the app version** in `gradle/libs.versions.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[versions]
|
||||||
|
app = "1.08.1" # new semver
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Bump Android `versionCode`** in `amethyst/build.gradle` (monotonic integer,
|
||||||
|
must increment even for same `versionName`):
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
versionCode = 443
|
||||||
|
versionName = generateVersionName(libs.versions.app.get())
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Commit + tag + push**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit -am "chore(release): 1.08.1"
|
||||||
|
git tag -s v1.08.1 -m "Release 1.08.1"
|
||||||
|
git push && git push --tags
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Wait** for the `Create Release Assets` workflow to finish (~25–30 min).
|
||||||
|
|
||||||
|
5. **Verify**:
|
||||||
|
- GH Release contains 8 desktop assets + 12 Android assets
|
||||||
|
- Asset sizes look sane (see §Enforce asset size budget — CI auto-fails at 1 GB/asset)
|
||||||
|
- Intel + ARM DMGs both present
|
||||||
|
- Android flow unchanged
|
||||||
|
|
||||||
|
6. **Stable vs prerelease** — a tag containing `-rc`, `-beta`, `-alpha`, `-dev`,
|
||||||
|
or `-snapshot` is auto-classified as prerelease. Stable tags trigger the
|
||||||
|
Homebrew + Winget bump workflows.
|
||||||
|
|
||||||
|
### Dry-run (no tag push)
|
||||||
|
|
||||||
|
Use `workflow_dispatch` to exercise the full matrix without publishing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gh workflow run create-release.yml \
|
||||||
|
-f dry_run=true \
|
||||||
|
-f test_tag=v0.0.0-dryrun \
|
||||||
|
--ref feat/my-branch
|
||||||
|
```
|
||||||
|
|
||||||
|
Assets are built and size-checked, but not uploaded; bump workflows do not
|
||||||
|
fire. Use for pre-merge validation of workflow changes.
|
||||||
|
|
||||||
|
### Version constraint: tag must match `libs.versions.toml`
|
||||||
|
|
||||||
|
The first step in each build-desktop matrix job asserts:
|
||||||
|
|
||||||
|
```
|
||||||
|
tag (stripped of 'v') == gradle/libs.versions.toml [versions] app
|
||||||
|
```
|
||||||
|
|
||||||
|
If they drift, the workflow fails fast. Always bump the TOML first, then tag.
|
||||||
|
|
||||||
|
### NEVER change Windows `upgradeUuid`
|
||||||
|
|
||||||
|
`desktopApp/build.gradle.kts:upgradeUuid` is the MSI product family GUID.
|
||||||
|
Changing it breaks in-place upgrades for existing Windows users — they must
|
||||||
|
uninstall before a new release. Leave it alone forever.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bootstrap runbook (one-time)
|
||||||
|
|
||||||
|
### Secrets to provision in GitHub repo settings
|
||||||
|
|
||||||
|
| Secret | Purpose | Scope |
|
||||||
|
|---|---|---|
|
||||||
|
| `HOMEBREW_TOKEN` | Bump Homebrew cask | Fine-grained PAT — `Homebrew/homebrew-cask` only — `Contents: write` + `Pull requests: write` — 90d expiry |
|
||||||
|
| `WINGET_TOKEN` | Submit Winget manifests | Classic PAT — `public_repo` — 90d expiry (dedicated bot account preferred; `vedantmgoyal9/winget-releaser` does not support fine-grained) |
|
||||||
|
|
||||||
|
All existing secrets (`SIGNING_KEY`, `SONATYPE_USERNAME`, etc.) remain
|
||||||
|
unchanged.
|
||||||
|
|
||||||
|
Rotate both on a 90-day cadence. Owner: assigned via `docs/RELEASE_OPS.md`
|
||||||
|
or equivalent issue tracker. On rotation, paste new token and run
|
||||||
|
`gh workflow run bump-homebrew.yml` on the most recent stable tag to verify.
|
||||||
|
|
||||||
|
### Homebrew cask (one-time initial PR)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew bump-cask-pr amethyst-nostr \
|
||||||
|
--version 1.08.0 \
|
||||||
|
--url "https://github.com/vitorpamplona/amethyst/releases/download/v1.08.0/amethyst-desktop-1.08.0-macos-arm64.dmg"
|
||||||
|
```
|
||||||
|
|
||||||
|
The cask filename is `amethyst-nostr` (not `amethyst` — that's taken by a
|
||||||
|
tiling window manager). After the first PR is merged, `bump-homebrew.yml`
|
||||||
|
auto-submits new version bumps on each stable release.
|
||||||
|
|
||||||
|
### Winget (one-time initial submission)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wingetcreate new \
|
||||||
|
https://github.com/vitorpamplona/amethyst/releases/download/v1.08.0/amethyst-desktop-1.08.0-windows-x64.msi
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `PackageIdentifier = VitorPamplona.Amethyst`. After the first manifest is
|
||||||
|
merged into `microsoft/winget-pkgs`, `bump-winget.yml` auto-submits new
|
||||||
|
version manifests.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting installs
|
||||||
|
|
||||||
|
### macOS — Gatekeeper "damaged and can't be opened"
|
||||||
|
|
||||||
|
Amethyst Desktop is currently unsigned. First-time launch requires:
|
||||||
|
|
||||||
|
1. **Right-click → Open** on the app (don't double-click) — then click **Open** on the Gatekeeper dialog
|
||||||
|
2. Or: `xattr -cr /Applications/Amethyst.app` to strip quarantine
|
||||||
|
3. Or: System Settings → Privacy & Security → "Open Anyway" after a blocked launch
|
||||||
|
|
||||||
|
Recommended path: install via Homebrew (`brew install --cask amethyst-nostr`)
|
||||||
|
— cask flow handles this seamlessly.
|
||||||
|
|
||||||
|
### Windows — SmartScreen "Windows protected your PC"
|
||||||
|
|
||||||
|
Amethyst Desktop is currently unsigned (no Authenticode). First-time launch:
|
||||||
|
|
||||||
|
1. Click **More info** on the SmartScreen dialog
|
||||||
|
2. Click **Run anyway**
|
||||||
|
|
||||||
|
Alternatively use `winget install VitorPamplona.Amethyst` — winget install
|
||||||
|
bypasses the UI dialog after accepting the installer's inherent trust.
|
||||||
|
|
||||||
|
### Linux AppImage won't execute
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x Amethyst-*.AppImage
|
||||||
|
./Amethyst-*.AppImage
|
||||||
|
```
|
||||||
|
|
||||||
|
On Fedora Silverblue / very minimal distros, FUSE might be missing. Use
|
||||||
|
`--appimage-extract-and-run`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./Amethyst-*.AppImage --appimage-extract-and-run
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Uninstall + state paths
|
||||||
|
|
||||||
|
State is shared across install channels (DMG, Homebrew, MSI, Winget, .deb,
|
||||||
|
.rpm, AppImage, tar.gz). Switching channels does not duplicate data but may
|
||||||
|
expose downgrade migration risks — **prefer a single install channel per
|
||||||
|
machine**.
|
||||||
|
|
||||||
|
| OS | App location | State directories |
|
||||||
|
|---|---|---|
|
||||||
|
| macOS | `/Applications/Amethyst.app` | `~/Library/Application Support/Amethyst`<br>`~/Library/Preferences/com.vitorpamplona.amethyst.desktop.plist`<br>`~/Library/Caches/Amethyst` |
|
||||||
|
| Windows | `%LOCALAPPDATA%\Amethyst` or `C:\Program Files\Amethyst` | `%APPDATA%\Amethyst`<br>`%LOCALAPPDATA%\Amethyst` |
|
||||||
|
| Linux (deb/rpm) | `/opt/amethyst` | `~/.config/amethyst`<br>`~/.local/share/amethyst`<br>`~/.cache/amethyst` |
|
||||||
|
| Linux (AppImage/tar.gz) | user-chosen | Same as above |
|
||||||
|
|
||||||
|
Uninstall:
|
||||||
|
|
||||||
|
- Homebrew: `brew uninstall --cask amethyst-nostr && brew zap amethyst-nostr`
|
||||||
|
- Winget: `winget uninstall VitorPamplona.Amethyst`
|
||||||
|
- .deb: `sudo apt remove amethyst`
|
||||||
|
- .rpm: `sudo dnf remove amethyst`
|
||||||
|
- AppImage / tar.gz: delete the file / extracted directory
|
||||||
|
- macOS `.dmg`: drag from `/Applications` to Trash, then delete state dirs manually
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Incident response
|
||||||
|
|
||||||
|
### Bad GH Release asset
|
||||||
|
|
||||||
|
1. Immediately mark release as prerelease (pauses bump workflows):
|
||||||
|
```bash
|
||||||
|
gh release edit v1.08.1 --prerelease
|
||||||
|
```
|
||||||
|
2. Delete the bad asset:
|
||||||
|
```bash
|
||||||
|
gh release delete-asset v1.08.1 amethyst-desktop-1.08.1-macos-arm64.dmg --yes
|
||||||
|
```
|
||||||
|
3. Rebuild locally or rerun the failing matrix job:
|
||||||
|
```bash
|
||||||
|
gh run rerun <run-id> --failed
|
||||||
|
```
|
||||||
|
4. Flip back to stable once verified (re-fires bump workflows — confirm fix first):
|
||||||
|
```bash
|
||||||
|
gh release edit v1.08.1 --prerelease=false
|
||||||
|
```
|
||||||
|
|
||||||
|
### Bad build reached Homebrew
|
||||||
|
|
||||||
|
**Preferred**: ship a point release (e.g. v1.08.2) — users on v1.08.1 get the
|
||||||
|
fix via `brew upgrade`.
|
||||||
|
|
||||||
|
**Alternative**: close the open PR in `Homebrew/homebrew-cask` before merge,
|
||||||
|
or file a revert PR if already merged. Typical Homebrew turn-around: 1–2 days.
|
||||||
|
|
||||||
|
### Bad build reached Winget
|
||||||
|
|
||||||
|
Winget manifests are append-only — no hard unpublish. Options:
|
||||||
|
|
||||||
|
1. Ship a point release (preferred — users upgrade via `winget upgrade`)
|
||||||
|
2. File a manifest-removal PR against `microsoft/winget-pkgs`. Moderator
|
||||||
|
review: 24–72h.
|
||||||
|
|
||||||
|
### User-facing communication
|
||||||
|
|
||||||
|
On any incident:
|
||||||
|
|
||||||
|
1. Edit the release body on GitHub with a warning banner + workaround
|
||||||
|
2. Pin a GH Issue with downgrade instructions per channel
|
||||||
|
3. Announce via Nostr relay + project social channels
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fallback plans
|
||||||
|
|
||||||
|
### macOS Intel runner retirement
|
||||||
|
|
||||||
|
GitHub's `macos-13` runner will eventually be deprecated. Monitor
|
||||||
|
<https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners>
|
||||||
|
for the deprecation date. When it hits:
|
||||||
|
|
||||||
|
1. Drop the `macos-13` matrix entry from `.github/workflows/create-release.yml`
|
||||||
|
2. Add a cross-arch build step on `macos-14` using a bundled x64 JDK + `jpackage --mac-signing-prefix` shenanigans, OR accept that only Apple Silicon DMGs ship and direct Intel users to `winget` on a Parallels VM or to rebuild from source.
|
||||||
|
3. Update README install matrix to reflect the change.
|
||||||
|
|
||||||
|
### Homebrew main-cask rejects unsigned app (post-Sept 1 2026)
|
||||||
|
|
||||||
|
Homebrew has committed to disabling unsigned casks in `Homebrew/homebrew-cask`
|
||||||
|
on 2026-09-01. Before that date:
|
||||||
|
|
||||||
|
**Option A**: Commit budget to Apple Developer Program ($99/yr), add
|
||||||
|
`signing { sign.set(true) }` + `notarization {}` blocks to
|
||||||
|
`desktopApp/build.gradle.kts`, wire Developer ID + notary creds into CI.
|
||||||
|
|
||||||
|
**Option B**: Pivot to a private Homebrew tap:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create repo: vitorpamplona/homebrew-amethyst
|
||||||
|
# Update bump-homebrew.yml:
|
||||||
|
# tap: vitorpamplona/amethyst
|
||||||
|
# cask: amethyst-nostr
|
||||||
|
# Users install: brew tap vitorpamplona/amethyst && brew install --cask amethyst-nostr
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: a private tap does NOT bypass Gatekeeper itself (macOS OS-level) — users
|
||||||
|
still see the "unsigned developer" dialog. Tap only sidesteps Homebrew's
|
||||||
|
internal policy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Follow-up channels (separate PRs)
|
||||||
|
|
||||||
|
- **AUR** (`amethyst-desktop-bin`) — blocked on AUR account ownership decision
|
||||||
|
- **Scoop** (Windows) — blocked on bucket strategy (own vs Extras)
|
||||||
|
- **Flathub** — deferred (moderate ongoing maintenance)
|
||||||
@@ -21,6 +21,8 @@ Join the social network you control.
|
|||||||
|
|
||||||
## Download and Install
|
## Download and Install
|
||||||
|
|
||||||
|
### Android
|
||||||
|
|
||||||
[<img src="./docs/design/zapstore.svg"
|
[<img src="./docs/design/zapstore.svg"
|
||||||
alt="Get it on Zap Store"
|
alt="Get it on Zap Store"
|
||||||
height="70">](https://github.com/zapstore/zapstore/releases)
|
height="70">](https://github.com/zapstore/zapstore/releases)
|
||||||
@@ -33,6 +35,24 @@ height="70">](https://github.com/vitorpamplona/amethyst/releases)
|
|||||||
alt="Get it on Google Play"
|
alt="Get it on Google Play"
|
||||||
height="70">](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst)
|
height="70">](https://play.google.com/store/apps/details?id=com.vitorpamplona.amethyst)
|
||||||
|
|
||||||
|
### Desktop
|
||||||
|
|
||||||
|
| OS | CLI install | Direct download |
|
||||||
|
|---|---|---|
|
||||||
|
| macOS (Apple Silicon) | `brew install --cask amethyst-nostr` | [.dmg arm64](https://github.com/vitorpamplona/amethyst/releases/latest) |
|
||||||
|
| macOS (Intel) | `brew install --cask amethyst-nostr` | [.dmg x64](https://github.com/vitorpamplona/amethyst/releases/latest) |
|
||||||
|
| Windows 10/11 | `winget install VitorPamplona.Amethyst` | [.msi](https://github.com/vitorpamplona/amethyst/releases/latest) · [.zip portable](https://github.com/vitorpamplona/amethyst/releases/latest) |
|
||||||
|
| Debian/Ubuntu | — | [.deb](https://github.com/vitorpamplona/amethyst/releases/latest) |
|
||||||
|
| Fedora/RHEL/openSUSE | — | [.rpm](https://github.com/vitorpamplona/amethyst/releases/latest) |
|
||||||
|
| Any Linux | — | [AppImage](https://github.com/vitorpamplona/amethyst/releases/latest) · [.tar.gz](https://github.com/vitorpamplona/amethyst/releases/latest) |
|
||||||
|
|
||||||
|
_Coming soon (separate PR): Scoop (Windows), AUR (Arch Linux)._
|
||||||
|
|
||||||
|
**Build from source:** see [BUILDING.md](BUILDING.md).
|
||||||
|
|
||||||
|
**Install troubleshooting** (Gatekeeper / SmartScreen / AppImage): see
|
||||||
|
[BUILDING.md § Troubleshooting installs](BUILDING.md#troubleshooting-installs).
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Supported Features
|
## Supported Features
|
||||||
@@ -249,22 +269,18 @@ For the Play build:
|
|||||||
|
|
||||||
## Deploying
|
## Deploying
|
||||||
|
|
||||||
1. Generate a new signing key
|
Full release + bootstrap runbooks (Android AAB upload, desktop packaging,
|
||||||
```
|
Homebrew cask, Winget manifest, Apple Developer signing budget time-box) live
|
||||||
keytool -genkey -v -keystore <my-release-key.keystore> -alias <alias_name> -keyalg RSA -keysize 2048 -validity 10000
|
in [BUILDING.md § Release runbook](BUILDING.md#release-runbook) and
|
||||||
openssl base64 < <my-release-key.keystore> | tr -d '\n' | tee some_signing_key.jks.base64.txt
|
[BUILDING.md § Bootstrap runbook (one-time)](BUILDING.md#bootstrap-runbook-one-time).
|
||||||
```
|
|
||||||
2. Create four Secret Key variables on your GitHub repository and fill in the signing key information
|
TL;DR for cutting a release:
|
||||||
- `KEY_ALIAS` <- `<alias_name>`
|
|
||||||
- `KEY_PASSWORD` <- `<your password>`
|
1. Bump `app` in `gradle/libs.versions.toml` (e.g. `"1.08.1"`)
|
||||||
- `KEY_STORE_PASSWORD` <- `<your key store password>`
|
2. Bump `versionCode` in `amethyst/build.gradle`
|
||||||
- `SIGNING_KEY` <- the data from `<my-release-key.keystore>`
|
3. `git commit -am "chore(release): 1.08.1" && git tag -s v1.08.1 && git push --tags`
|
||||||
3. Change the `versionCode` and `versionName` on `amethyst/build.gradle`
|
4. Wait for `Create Release Assets` workflow — 20 Android assets + 8 desktop assets go live on GH Release; Homebrew + Winget auto-bump on stable tags
|
||||||
4. Commit and push.
|
5. Upload AAB to Play Store manually (existing step)
|
||||||
5. Tag the commit with `v{x.x.x}`
|
|
||||||
6. Let the [Create Release GitHub Action](https://github.com/vitorpamplona/amethyst/actions/workflows/create-release.yml) build a new `aab` file.
|
|
||||||
7. Add your CHANGE LOG to the description of the new release
|
|
||||||
8. Download the `aab` file and upload it to the PlayStore.
|
|
||||||
|
|
||||||
## Using the Quartz library
|
## Using the Quartz library
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user