Files
amethyst/.github/workflows/create-release.yml
T
dependabot[bot]andGitHub ebc4bee4d9 chore(actions): bump the actions group with 4 updates
Bumps the actions group with 4 updates: [actions/checkout](https://github.com/actions/checkout), [softprops/action-gh-release](https://github.com/softprops/action-gh-release), [actions/cache](https://github.com/actions/cache) and [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request).


Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

Updates `softprops/action-gh-release` from 3.0.0 to 3.0.1
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](https://github.com/softprops/action-gh-release/compare/b4309332981a82ec1c5618f44dd2e27cc8bfbfda...718ea10b132b3b2eba29c1007bb80653f286566b)

Updates `actions/cache` from 5 to 6
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v5...v6)

Updates `peter-evans/create-pull-request` from 7 to 8
- [Release notes](https://github.com/peter-evans/create-pull-request/releases)
- [Commits](https://github.com/peter-evans/create-pull-request/compare/v7...v8)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: softprops/action-gh-release
  dependency-version: 3.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: actions
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: peter-evans/create-pull-request
  dependency-version: '8'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-01 00:45:34 +00:00

645 lines
29 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. vX.YY.Z-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.
# appimagetool pinned release — bump via Dependabot, verify SHA256 via env var below.
# We used to use linuxdeploy here, but it auto-walks the AppDir with ldd to
# bundle deps — that fights jpackage's self-contained JRE (libjvm.so has
# $ORIGIN RPATH so ldd can't resolve it standalone). appimagetool only
# embeds the AppDir as-is, which is what we actually want.
APPIMAGETOOL_URL: https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage
APPIMAGETOOL_SHA256: 46fdd785094c7f6e545b61afcfb0f3d98d8eab243f644b4b17698c01d06083d1
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-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@v7
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
distribution: 'temurin'
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 (deb+rpm leg only)
if: matrix.family == 'linux'
run: sudo apt-get update && sudo apt-get install -y rpm fakeroot
- name: Fetch appimagetool (linux-portable only, SHA-verified)
if: matrix.family == 'linux-portable'
run: |
set -euo pipefail
# appimagetool 1.9.0 validates the .desktop file via desktop-file-validate.
sudo apt-get update && sudo apt-get install -y desktop-file-utils
curl -fsSL --retry 3 "$APPIMAGETOOL_URL" -o desktopApp/packaging/appimage/appimagetool-x86_64.AppImage
actual=$(sha256sum desktopApp/packaging/appimage/appimagetool-x86_64.AppImage | awk '{print $1}')
if [[ "$actual" != "$APPIMAGETOOL_SHA256" ]]; then
echo "::error::appimagetool SHA256 mismatch. Expected $APPIMAGETOOL_SHA256, got $actual"
exit 1
fi
chmod +x desktopApp/packaging/appimage/appimagetool-x86_64.AppImage
# macOS only: import the Developer ID Application cert into a throwaway
# keychain so jpackage's codesign pass can find it. Soft — if the
# MAC_CERTIFICATE_P12 secret isn't set (forks, or before Apple creds are
# provisioned) the DMG is built UNSIGNED, exactly as before. notarytool
# runs as part of the gradle task when the identity env is exported below.
- name: Import Apple Developer ID certificate (macOS leg, if configured)
if: matrix.family == 'macos'
id: mac_keychain
uses: ./.github/actions/import-macos-cert
with:
certificate-p12-base64: ${{ secrets.MAC_CERTIFICATE_P12 }}
certificate-password: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
- name: Build desktop artifacts
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
env:
# Empty on non-macOS legs and on the macOS leg when no cert is
# configured — the gradle macOS{} block skips signing when the
# identity is blank. Prefer the full common name resolved from the
# keychain over the raw secret: Compose's signer maps the identity via
# `security find-certificate` and only matches the exact "Developer ID
# Application: …" CN, whereas the secret may be a hash or partial name
# (which bare codesign accepts but Compose does not). Fall back to the
# secret if resolution failed.
AMETHYST_MAC_SIGN_IDENTITY: ${{ steps.mac_keychain.outputs.signing == 'true' && (steps.mac_keychain.outputs.identity || secrets.MAC_SIGN_IDENTITY) || '' }}
# Explicit keychain for Compose's MacSigner. Its `security
# find-certificate` lookup doesn't resolve the imported cert via the
# search list on these runners ("Could not find certificate ... in
# keychain []"), so point it at the throwaway keychain directly.
AMETHYST_MAC_SIGN_KEYCHAIN: ${{ steps.mac_keychain.outputs.signing == 'true' && steps.mac_keychain.outputs.keychain || '' }}
AMETHYST_NOTARY_APPLE_ID: ${{ secrets.MAC_NOTARY_APPLE_ID }}
AMETHYST_NOTARY_PASSWORD: ${{ secrets.MAC_NOTARY_PASSWORD }}
AMETHYST_NOTARY_TEAM_ID: ${{ secrets.MAC_NOTARY_TEAM_ID }}
with:
max_attempts: 2
timeout_minutes: 15
command: ./gradlew --no-daemon :desktopApp:${{ matrix.tasks }}
# jpackage pins libicu to the build host's version (libicu74 on
# ubuntu-24.04). Rewrite the .deb so it installs across Debian/Ubuntu.
- name: Relax libicu dependency in .deb
if: matrix.family == 'linux'
run: |
set -euo pipefail
chmod +x scripts/relax-deb-libicu.sh
scripts/relax-deb-libicu.sh desktopApp/build/compose/binaries/main-release/deb/*.deb
- 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
# collect_assets normalizes linux-portable → linux internally.
collect_assets "${{ matrix.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 }}"
# Stable = exactly vMAJOR.MINOR.PATCH; everything else is prerelease.
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "prerelease=false" >> "$GITHUB_OUTPUT"
else
echo "prerelease=true" >> "$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@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
with:
files: dist/*
tag_name: ${{ steps.ver.outputs.tag }}
prerelease: ${{ steps.classify.outputs.prerelease }}
draft: false
fail_on_unmatched_files: true
generate_release_notes: false # Android job writes release notes (last-writer-wins race)
- 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"
# ---------------------------------------------------------------------------
# Amy CLI build matrix. Each leg produces a self-contained amy bundle with a
# minimal jlink'd JRE — no system Java required on the user's machine.
#
# amyImage task (all legs): cli/build/amy-image/amy/ → amy-*.tar.gz
# jpackageDeb / jpackageRpm: cli/build/jpackage/amy_*.deb + amy-*.rpm
#
# macOS legs ship only the tarball. We deliberately avoid jpackage --type
# app-image on macOS because it produces an .app bundle (burying the binary
# at Contents/MacOS/amy) — wrong UX for a CLI.
#
# Windows is intentionally deferred — cli/ has not been validated on Windows
# yet (data-dir path handling, file locking on groups/<gid>.mls, line endings
# in identity.json).
#
# Asset naming: amy-<version>-<family>-<arch>.<ext>. See scripts/asset-name.sh.
# ---------------------------------------------------------------------------
build-cli:
strategy:
fail-fast: false
matrix:
include:
- { os: macos-14, arch: arm64, family: macos, tasks: "amyImage" }
- { os: ubuntu-latest, arch: x64, family: linux, tasks: "amyImage jpackageDeb jpackageRpm" }
runs-on: ${{ matrix.os }}
timeout-minutes: 45 # macOS leg also codesigns + notarizes the jlink image
defaults:
run:
shell: bash
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
distribution: 'temurin'
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"
- name: Install RPM tooling (linux only)
if: matrix.family == 'linux'
run: sudo apt-get update && sudo apt-get install -y rpm fakeroot
- name: Build amy artifacts
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
with:
max_attempts: 2
timeout_minutes: 15
command: ./gradlew --no-daemon :cli:${{ matrix.tasks }}
# amy is headless: the Compose UI render stack (skiko + its native dylibs,
# foundation/material/material3/ui/animation) must never reach the CLI
# image. cli/build.gradle.kts excludes it from runtimeClasspath; this
# guards against a transitive dep silently dragging it back (size + macOS
# notarization-surface regression). compose.runtime is CLI-safe and stays.
- name: Assert no Compose UI in the amy image
run: |
set -euo pipefail
LIB="cli/build/install/amy/lib"
leak="$(ls "$LIB" | grep -iE 'skiko|foundation(-layout)?-desktop|material3?-desktop|material-ripple|ui-desktop|animation(-core)?-desktop' || true)"
if [ -n "$leak" ]; then
echo "::error::Compose UI render stack leaked into the amy CLI image:"
echo "$leak" | sed 's/^/ /'
echo "Exclude it in cli/build.gradle.kts (configurations.runtimeClasspath)."
exit 1
fi
echo "OK: no skiko / Compose UI render jars in the amy image ($(du -sh "$LIB" | cut -f1))."
# macOS only: import the Developer ID cert (no-op without the secret) so
# the next step can codesign the jlink image. The jvm bundle for
# Homebrew-core is NOT signed here — Homebrew strips quarantine itself.
- name: Import Apple Developer ID certificate (macOS leg, if configured)
if: matrix.family == 'macos'
id: mac_keychain
uses: ./.github/actions/import-macos-cert
with:
certificate-p12-base64: ${{ secrets.MAC_CERTIFICATE_P12 }}
certificate-password: ${{ secrets.MAC_CERTIFICATE_PASSWORD }}
# Codesign + notarize the macOS jlink image (amy-<ver>-macos-arm64.tar.gz)
# for users who download it directly. A loose tarball can't be stapled
# (stapler only does .app/.dmg/.pkg), so Gatekeeper verifies notarization
# online on first run. Runs before "Collect" so the tarred image is signed.
- name: Sign + notarize amy image (macOS leg, if configured)
if: matrix.family == 'macos' && steps.mac_keychain.outputs.signing == 'true'
env:
SIGN_IDENTITY: ${{ secrets.MAC_SIGN_IDENTITY }}
NOTARY_APPLE_ID: ${{ secrets.MAC_NOTARY_APPLE_ID }}
NOTARY_PASSWORD: ${{ secrets.MAC_NOTARY_PASSWORD }}
NOTARY_TEAM_ID: ${{ secrets.MAC_NOTARY_TEAM_ID }}
run: |
set -euo pipefail
IMG="cli/build/amy-image/amy"
ENTITLEMENTS="cli/packaging/macos/amy.entitlements"
# First sign the macOS Mach-O natives buried INSIDE the bundled jars
# (secp256k1/jna/sqlite/skiko/jkeychain/mediaplayer). The loose-file
# loop below can't see them, but Apple's notary recurses into jars and
# rejects any unsigned Mach-O — so this must run before notarize.
SIGN_IDENTITY="$SIGN_IDENTITY" scripts/sign-macos-jar-natives.sh "$IMG"
# Sign every loose Mach-O binary in the bundled JRE. Each is signed
# independently (no enclosing .app seals them), so order is irrelevant.
# Executables get the hardened-runtime entitlements; dylibs don't.
while IFS= read -r f; do
case "$(file -b "$f")" in
*Mach-O*executable*)
codesign --force --options runtime --timestamp \
--entitlements "$ENTITLEMENTS" --sign "$SIGN_IDENTITY" "$f" ;;
*Mach-O*)
codesign --force --options runtime --timestamp \
--sign "$SIGN_IDENTITY" "$f" ;;
esac
done < <(find "$IMG" -type f)
codesign --verify --strict --verbose=2 "$IMG/runtime/bin/java"
# Notarize: zip the signed image, submit, wait for Apple's verdict.
# The notary service recursively inspects the lib/*.jar files; their
# embedded Mach-O natives are signed by sign-macos-jar-natives.sh
# above. Surface the per-file log on any non-Accepted verdict so a
# regression is diagnostic rather than a bare failure.
ZIP="$RUNNER_TEMP/amy-notarize.zip"
OUT="$RUNNER_TEMP/notary-submit.json"
ditto -c -k --keepParent "$IMG" "$ZIP"
if ! xcrun notarytool submit "$ZIP" \
--apple-id "$NOTARY_APPLE_ID" --password "$NOTARY_PASSWORD" \
--team-id "$NOTARY_TEAM_ID" --wait --output-format json > "$OUT"; then
echo "::warning::notarytool submit exited non-zero"
fi
cat "$OUT"
STATUS="$(jq -r '.status // "Unknown"' "$OUT" 2>/dev/null || echo Unknown)"
SUBMISSION_ID="$(jq -r '.id // empty' "$OUT" 2>/dev/null || true)"
if [ "$STATUS" != "Accepted" ]; then
echo "::error::Notarization status: $STATUS"
if [ -n "$SUBMISSION_ID" ]; then
echo "----- notary log -----"
xcrun notarytool log "$SUBMISSION_ID" \
--apple-id "$NOTARY_APPLE_ID" --password "$NOTARY_PASSWORD" \
--team-id "$NOTARY_TEAM_ID" || true
fi
exit 1
fi
# jpackage pins libicu to the build host's version (libicu74 on
# ubuntu-24.04). Rewrite the .deb so it installs across Debian/Ubuntu.
- name: Relax libicu dependency in .deb
if: matrix.family == 'linux'
run: |
set -euo pipefail
chmod +x scripts/relax-deb-libicu.sh
scripts/relax-deb-libicu.sh cli/build/jpackage/*.deb
- name: Collect + rename assets
run: |
set -euo pipefail
# shellcheck source=scripts/asset-name.sh
source scripts/asset-name.sh
collect_cli_assets "${{ matrix.family }}" "${{ matrix.arch }}" "${{ steps.ver.outputs.version }}" dist
# Homebrew-core ships a no-JRE jar bundle and depends_on "openjdk" — it
# cannot use the jlink tarball above (bundled runtime) nor build from
# source (its sandbox blocks Gradle's Maven downloads). installDist
# (bin/amy + lib/*.jar, no runtime/) is exactly that bundle. It is pure
# JVM bytecode, so one platform-independent asset serves every OS; we cut
# it on the linux leg only. amyImage depends on installDist, so the
# cli/build/install/amy tree already exists here.
- name: Package no-JRE jvm bundle for Homebrew (linux leg only)
if: matrix.family == 'linux'
run: |
set -euo pipefail
VER="${{ steps.ver.outputs.version }}"
SRC="cli/build/install/amy"
test -x "$SRC/bin/amy"
( cd "$SRC" && tar czf "$OLDPWD/dist/amy-${VER}-jvm.tar.gz" bin lib )
echo "Collected: dist/amy-${VER}-jvm.tar.gz"
- name: Enforce CLI size budget (200 MB per asset)
run: |
set -euo pipefail
# The plan at cli/plans/2026-04-21-cli-distribution.md §size-budget
# targets < 80 MB, but :commons currently leaks Compose + Skiko as
# transitive deps (~40 MB of unused UI jars). Budget is set to
# 200 MB until commons is split into core + ui modules — track that
# as a follow-up. Until then, this gate just catches pathological
# regressions (e.g. accidental :amethyst dep pulling Android libs).
fail=0
for f in dist/*; do
if [[ -f "$f" ]]; then
size=$(wc -c < "$f")
mb=$(( size / 1048576 ))
if (( size > 209715200 )); then
echo "::error file=$f::asset is ${mb} MB — exceeds 200 MB amy 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" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "prerelease=false" >> "$GITHUB_OUTPUT"
else
echo "prerelease=true" >> "$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@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
with:
files: dist/*
tag_name: ${{ steps.ver.outputs.tag }}
prerelease: ${{ steps.classify.outputs.prerelease }}
draft: false
fail_on_unmatched_files: true
generate_release_notes: false # Android job writes release notes (last-writer-wins race)
- name: Dry-run summary
if: github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'true'
run: |
echo "### Dry-run: amy ${{ 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@v7
- name: Set up JDK 21
uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: 21
- name: Cache gradle
uses: actions/cache@v6
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-android-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'gradle/libs.versions.toml') }}
restore-keys: |
${{ runner.os }}-android-gradle-
- name: Build AAB
run: ./gradlew clean bundleRelease --stacktrace
- name: Sign AAB (Google Play)
uses: r0adkll/sign-android-release@349ebdef58775b1e0d8099458af0816dc79b6407 # 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@349ebdef58775b1e0d8099458af0816dc79b6407 # 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@349ebdef58775b1e0d8099458af0816dc79b6407 # 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@349ebdef58775b1e0d8099458af0816dc79b6407 # 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
# Accrescent does not accept AABs or monolithic APKs — it requires a signed
# APK set (.apks) of split APKs generated by bundletool from the AAB. We build
# it from the F-Droid flavor (no proprietary Google deps) and sign the splits
# with the same release keystore used above. Upload is still manual: drop this
# .apks into https://console.accrescent.app (no publish API/CLI exists yet).
- name: Build Accrescent APK set (F-Droid)
env:
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_STORE_PASSWORD: ${{ secrets.KEY_STORE_PASSWORD }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
run: |
set -euo pipefail
TAG="${GITHUB_REF_NAME}"
BUNDLETOOL_VERSION="1.18.3" # must be >= 1.11.4 per Accrescent requirements
curl -fsSL -o bundletool.jar \
"https://github.com/google/bundletool/releases/download/${BUNDLETOOL_VERSION}/bundletool-all-${BUNDLETOOL_VERSION}.jar"
# Same base64 keystore secret consumed by the r0adkll signing steps above.
echo "$SIGNING_KEY" | base64 -d > release.keystore
# --mode=default emits the split-APK set Accrescent wants (NOT --mode=universal,
# which produces a monolithic APK that Accrescent rejects).
java -jar bundletool.jar build-apks \
--bundle="dist/amethyst-fdroid-${TAG}.aab" \
--output="dist/amethyst-fdroid-${TAG}.apks" \
--ks=release.keystore \
--ks-key-alias="$KEY_ALIAS" \
--ks-pass="pass:$KEY_STORE_PASSWORD" \
--key-pass="pass:$KEY_PASSWORD" \
--mode=default
rm -f release.keystore bundletool.jar
# Accrescent's automated check rejects an APK set larger than 128 MiB.
SIZE_BYTES=$(stat -c%s "dist/amethyst-fdroid-${TAG}.apks")
echo "Accrescent APK set size: $((SIZE_BYTES / 1024 / 1024)) MiB"
if [ "$SIZE_BYTES" -gt $((128 * 1024 * 1024)) ]; then
echo "::warning::amethyst-fdroid-${TAG}.apks exceeds Accrescent's 128 MiB limit; the console will reject this upload."
fi
ls -la dist
- name: Classify release
id: classify
run: |
TAG="${GITHUB_REF_NAME}"
# Stable = exactly vMAJOR.MINOR.PATCH; everything else is prerelease.
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "prerelease=false" >> "$GITHUB_OUTPUT"
else
echo "prerelease=true" >> "$GITHUB_OUTPUT"
fi
- name: Upload Android assets to GH Release
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
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 }}