mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-08 23:54:39 +00:00
v1.12.4 got the keychain right ("...in keychain [/Users/.../amethyst-signing
.keychain-db]") but createReleaseDistributable still failed with "Could not
find certificate". Different layer of the same problem:
Compose's MacSigner maps its identity to a cert by running `security
find-certificate -c <identity>`, prepending "Developer ID Application: " when
the identity doesn't already start with it. `codesign --sign` (used by the
signMacJarNatives task, which succeeds in the same job) instead matches a
SHA-1 hash OR any common-name substring. So a MAC_SIGN_IDENTITY secret that is
a fingerprint or a team-ID/partial name signs fine with codesign but, once
prefixed by Compose, is not a substring of the cert's common name -> zero
matches -> failure.
Reproduced locally against the real Developer ID cert:
find-certificate -c "Developer ID Application: <TEAMID>" -> 0 matches
find-certificate -c "Developer ID Application: <full CN>" -> 1 match
Fix: import-macos-cert now resolves the certificate's full "Developer ID
Application: NAME (TEAMID)" common name from the keychain (via find-identity)
and exposes it as an `identity` output. The desktop build feeds that to
Compose's signing.identity, falling back to the raw secret if resolution
fails. Independent of whatever form the secret takes. The amy CLI leg keeps
using bare codesign with the secret directly and is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
603 lines
27 KiB
YAML
603 lines
27 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@v6
|
|
|
|
- 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@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
|
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@v6
|
|
|
|
- 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@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
|
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@v6
|
|
|
|
- name: Set up JDK 21
|
|
uses: actions/setup-java@v5
|
|
with:
|
|
distribution: 'temurin'
|
|
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', '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
|
|
|
|
- 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@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
|
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 }}
|