mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-05 14:24:38 +00:00
geode was runnable only via ./gradlew :geode:run and was absent from CI. Give it the same release process as the amy CLI (it's the same kind of application-plugin JVM module), plus the pieces a long-running server daemon needs that a one-shot CLI does not. - Main.kt: add terminal --version/-V and --help/-h flags so a packaged binary has a fast, exit-0 command (Homebrew test block, package smoke checks, Docker healthcheck). - build.gradle.kts: jlinkRuntime + geodeImage (portable flat app-image with a bundled JRE, plus config.example.toml + geode.service under share/) + jpackageDeb/jpackageRpm, mirroring cli/. No Compose to exclude — geode depends only on :quartz. - Dockerfile + .dockerignore: multi-stage image (gradle installDist -> temurin JRE), the primary channel for relay operators. - packaging/: systemd unit, macOS hardened-runtime entitlements, and a reference Homebrew formula. - scripts/asset-name.sh: geode_asset_name/collect_geode_assets under the canonical geode-<version>-<family>-<arch>.<ext> scheme. - create-release.yml: build-geode matrix (tarball + deb/rpm + no-JRE jvm bundle, with a serve+NIP-11 smoke test of the jlink image) and a docker-geode job pushing ghcr.io/<owner>/geode:<version> (+ :latest). - bump-homebrew-geode-formula.yml: auto-sync the reference formula on stable releases. - build.yml: run :geode:test in CI (it ran in no workflow before). - README.md + plans/2026-07-24-geode-release.md: operator docs + design. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KCdJwdhGtmLZ12ViS56S3k
993 lines
45 KiB
YAML
993 lines
45 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: 60 # linux-portable leg also downloads the freedesktop runtime + builds the Flatpak bundle
|
|
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
|
|
|
|
# Flatpak tooling + the freedesktop runtime/sdk the manifest pins
|
|
# (runtime-version is greped from the manifest so this never drifts).
|
|
# Retried: the runtime download from Flathub is ~1 GB and flatpak
|
|
# install resumes cleanly on re-run.
|
|
- name: Install Flatpak tooling + runtimes (linux-portable only)
|
|
if: matrix.family == 'linux-portable'
|
|
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
|
|
with:
|
|
max_attempts: 3
|
|
timeout_minutes: 15
|
|
command: |
|
|
set -euo pipefail
|
|
sudo apt-get update && sudo apt-get install -y flatpak flatpak-builder
|
|
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
|
|
FDO_VER=$(grep -E "^runtime-version:" desktopApp/packaging/flatpak/com.vitorpamplona.amethyst.Desktop.yml | cut -d"'" -f2)
|
|
flatpak install --user --noninteractive flathub \
|
|
"org.freedesktop.Platform//${FDO_VER}" \
|
|
"org.freedesktop.Sdk//${FDO_VER}"
|
|
|
|
# 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
|
|
|
|
# Flatpak bundle: wraps the same createReleaseDistributable tree the
|
|
# AppImage uses. The manifest (desktopApp/packaging/flatpak/) copies the
|
|
# prebuilt jpackage tree into /app — no Gradle runs inside the sandbox.
|
|
# build-bundle emits a single-file .flatpak whose baked-in runtime-repo
|
|
# lets the user's flatpak fetch the freedesktop runtime from Flathub on
|
|
# install. --disable-rofiles-fuse: GH runners lack a usable rofiles-fuse.
|
|
- name: Build Flatpak bundle (linux-portable only)
|
|
if: matrix.family == 'linux-portable'
|
|
run: |
|
|
set -euo pipefail
|
|
VER="${{ steps.ver.outputs.version }}"
|
|
PKG="desktopApp/packaging/flatpak"
|
|
APP_ID="com.vitorpamplona.amethyst.Desktop"
|
|
OUT="desktopApp/build/flatpak"
|
|
# Inject the AppStream <release> entry for this build (the checked-in
|
|
# metainfo deliberately carries none — CI is the source of truth).
|
|
sed -i "s|<releases>|<releases>\n <release version=\"${VER}\" date=\"$(date -u +%F)\" />|" \
|
|
"${PKG}/${APP_ID}.metainfo.xml"
|
|
mkdir -p "$OUT"
|
|
flatpak-builder --user --force-clean --disable-rofiles-fuse \
|
|
--state-dir="${OUT}/.flatpak-builder" \
|
|
--repo="${OUT}/repo" \
|
|
"${OUT}/build-dir" \
|
|
"${PKG}/${APP_ID}.yml"
|
|
flatpak build-bundle "${OUT}/repo" \
|
|
"${OUT}/Amethyst-${VER}-x86_64.flatpak" \
|
|
"$APP_ID" \
|
|
--runtime-repo=https://dl.flathub.org/repo/flathub.flatpakrepo
|
|
ls -la "$OUT"
|
|
|
|
- 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"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# geode relay build matrix. Same shape as build-cli — geode is the same kind
|
|
# of `application`-plugin JVM module — producing a self-contained bundle with a
|
|
# minimal jlink'd JRE (no system Java required) plus native Linux packages.
|
|
#
|
|
# geodeImage task (all legs): geode/build/geode-image/geode/ → geode-*.tar.gz
|
|
# jpackageDeb / jpackageRpm: geode/build/jpackage/geode_*.deb + geode-*.rpm
|
|
# no-JRE jvm bundle (linux): geode-<ver>-jvm.tar.gz for the Homebrew formula
|
|
#
|
|
# Unlike build-cli there is no "no Compose UI" assertion — geode depends only on
|
|
# :quartz and never pulls the Compose render stack. The GHCR Docker image is a
|
|
# separate job (docker-geode) below.
|
|
#
|
|
# Asset naming: geode-<version>-<family>-<arch>.<ext>. See scripts/asset-name.sh.
|
|
# ---------------------------------------------------------------------------
|
|
build-geode:
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- { os: macos-14, arch: arm64, family: macos, tasks: "geodeImage" }
|
|
- { os: ubuntu-latest, arch: x64, family: linux, tasks: "geodeImage 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)
|
|
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 geode artifacts
|
|
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
|
|
with:
|
|
max_attempts: 2
|
|
timeout_minutes: 15
|
|
command: ./gradlew --no-daemon :geode:${{ matrix.tasks }}
|
|
|
|
# Boot the bundled jlink image before shipping it. `--version` proves the
|
|
# JVM starts and the main class loads; the serve leg proves the jlink
|
|
# module list is complete for the real relay path (Ktor CIO + SQLite +
|
|
# NIP-11 serialization) — a too-tight module list links fine but fails
|
|
# here with NoClassDefFound instead of on an operator's machine.
|
|
- name: Smoke-test the geode image
|
|
run: |
|
|
set -euo pipefail
|
|
IMG="geode/build/geode-image/geode"
|
|
"$IMG/bin/geode" --version
|
|
"$IMG/bin/geode" --port 17447 &
|
|
PID=$!
|
|
ok=0
|
|
for i in $(seq 1 20); do
|
|
if curl -fsS -H 'Accept: application/nostr+json' http://127.0.0.1:17447/ -o /tmp/nip11.json; then ok=1; break; fi
|
|
sleep 1
|
|
done
|
|
kill "$PID" 2>/dev/null || true
|
|
wait "$PID" 2>/dev/null || true
|
|
[[ "$ok" == 1 ]] || { echo "::error::geode image did not serve NIP-11 within 20s"; exit 1; }
|
|
echo "NIP-11 doc:"; head -c 400 /tmp/nip11.json; echo
|
|
grep -q '"supported_nips"' /tmp/nip11.json || { echo "::error::NIP-11 doc missing supported_nips"; exit 1; }
|
|
|
|
# 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 (geode-<ver>-macos-arm64.tar.gz)
|
|
# for operators 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 geode 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="geode/build/geode-image/geode"
|
|
ENTITLEMENTS="geode/packaging/macos/geode.entitlements"
|
|
# Sign the macOS Mach-O natives buried INSIDE the bundled jars
|
|
# (secp256k1/sqlite) first — 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. 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.
|
|
ZIP="$RUNNER_TEMP/geode-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 geode/build/jpackage/*.deb
|
|
|
|
- name: Collect + rename assets
|
|
run: |
|
|
set -euo pipefail
|
|
# shellcheck source=scripts/asset-name.sh
|
|
source scripts/asset-name.sh
|
|
collect_geode_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/geode + 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. geodeImage depends on installDist, so the
|
|
# geode/build/install/geode 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="geode/build/install/geode"
|
|
test -x "$SRC/bin/geode"
|
|
( cd "$SRC" && tar czf "$OLDPWD/dist/geode-${VER}-jvm.tar.gz" bin lib )
|
|
echo "Collected: dist/geode-${VER}-jvm.tar.gz"
|
|
|
|
- name: Enforce geode size budget (200 MB 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 > 209715200 )); then
|
|
echo "::error file=$f::asset is ${mb} MB — exceeds 200 MB geode 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: geode ${{ 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"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# geode Docker image → GitHub Container Registry (GHCR). A relay is most often
|
|
# deployed as a container, so this is geode's primary distribution channel.
|
|
# Builds geode/Dockerfile (multi-stage: gradle installDist → temurin JRE) and
|
|
# pushes ghcr.io/<owner>/geode:<version> (+ :latest on a stable release).
|
|
# Tag-push only — skipped on the workflow_dispatch dry-run.
|
|
# ---------------------------------------------------------------------------
|
|
docker-geode:
|
|
if: github.event_name != 'workflow_dispatch'
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v7
|
|
|
|
- name: Resolve version + tags
|
|
id: meta
|
|
run: |
|
|
set -euo pipefail
|
|
TAG="${GITHUB_REF_NAME}"
|
|
VER="${TAG#v}"
|
|
# Lowercase owner — GHCR repository paths must be lowercase.
|
|
OWNER="$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')"
|
|
IMAGE="ghcr.io/${OWNER}/geode"
|
|
TAGS="${IMAGE}:${VER}"
|
|
# Only move :latest for a stable vX.Y.Z tag, never a prerelease.
|
|
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
TAGS="${TAGS},${IMAGE}:latest"
|
|
fi
|
|
echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
|
|
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
|
|
- name: Log in to GHCR
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ghcr.io
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Build and push image
|
|
uses: docker/build-push-action@v6
|
|
with:
|
|
context: .
|
|
file: geode/Dockerfile
|
|
push: true
|
|
tags: ${{ steps.meta.outputs.tags }}
|
|
labels: |
|
|
org.opencontainers.image.revision=${{ github.sha }}
|
|
org.opencontainers.image.version=${{ steps.meta.outputs.tags }}
|
|
cache-from: type=gha
|
|
cache-to: type=gha,mode=max
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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 }}
|