Amethyst v1.13.1 (and every prior release) shipped only linux-x64 desktop
binaries — .deb, .rpm, .AppImage, .flatpak, .tar.gz. Same for the amy
CLI and geode relay. Users on aarch64 hardware (Pinebook, Ampere
Altra, Raspberry Pi 4/5, AWS Graviton, arm64 servers, arm64 Chromebooks
running crostini, etc.) can't install any of them.
This teaches the release matrix about arm64:
- Add `ubuntu-24.04-arm` legs to build-desktop, build-cli, and
build-geode. This is a standard, free public-repo GitHub-hosted
runner (4 CPU / 16 GB / 14 GB SSD / arm64) since early 2025. No
cross-compilation: jpackage / jlink / Compose Multiplatform 1.11
all produce host-native artifacts.
- Fetch the matching `appimagetool-<arch>.AppImage` from the same
1.9.0 release with an arch-specific SHA256 pin. `APPIMAGETOOL_URL`
becomes `APPIMAGETOOL_VERSION` + per-arch SHA256 env vars.
- Parametrize the portable tarball/zip filename by `matrix.arch`
(`amethyst-desktop-<ver>-linux-arm64.tar.gz` is now produced).
- Parametrize the Flatpak bundle filename and rewrite the manifest's
`GST_PLUGIN_SYSTEM_PATH` from `x86_64-linux-gnu` to
`aarch64-linux-gnu` on the arm64 leg. The Flathub-submission manifest
(`desktopApp/packaging/flatpak/flathub/`) still gates on
`only-arches: x86_64` — flipping that to include aarch64 is a
follow-up once a Flathub aarch64 build has been validated end-to-end.
- Make the `createReleaseAppImage` gradle task pick its host arch from
`System.getProperty("os.arch")` (amd64/x86_64 → `x86_64`, aarch64/
arm64 → `aarch64`). Same task, same command, drives both legs.
- Fix `desktopApp/packaging/appimage/AppRun` to compute the multiarch
library path from `uname -m` at launch time instead of hard-coding
`x86_64-linux-gnu`. One script works in both AppImages on the target
machine.
- Extend the desktop smoke test to run the release .deb build + launch
probe on `ubuntu-24.04-arm` too, so arch-specific ProGuard/jlink
breakage (missing native lib, arch-specific reflection root) is
caught at PR time.
- Update BUILDING.md and scripts/asset-name.sh docs with the new
arm64 asset names.
Follow-up assets published for the next tag push (v1.13.2+):
- amethyst-desktop-<ver>-linux-arm64.{deb,rpm,AppImage,flatpak,tar.gz}
- amy-<ver>-linux-arm64.{deb,rpm,tar.gz}
- geode-<ver>-linux-arm64.{deb,rpm,tar.gz}
Verification (local, before submitting):
- `python3 -c 'import yaml; yaml.safe_load(open(".github/workflows/create-release.yml"))'` — parses clean
- `bash -n scripts/asset-name.sh desktopApp/packaging/appimage/AppRun` — parses clean
- `actionlint` — reports only pre-existing shellcheck style hints; no new errors
- Confirmed `linuxdeploy-aarch64.AppImage` and
`appimagetool-aarch64.AppImage` exist under the same pinned release
tags used for x86_64; SHA256 recorded from a fresh download.
Not addressed (out of scope for this PR):
- Homebrew / winget bump workflows (`bump-homebrew*.yml`,
`bump-winget.yml`) — those consume the assets by name; the new arm64
filenames don't change any x86_64 name they already reference.
- Android arm64 continues to ship as before (already had it).
Mirrors the Homebrew cask change. The old design stored a classic `public_repo`
PAT as WINGET_TOKEN and handed it to the third-party
vedantmgoyal9/winget-releaser action: a token with write access to every public
repo the owning account can reach, given to code we do not control, in a place
any push-access collaborator could read it from (a pushed branch containing a
workflow runs with repo secrets).
- CI (bump-winget.yml, now "Sync Winget Manifest Reference") uses GITHUB_TOKEN
only: downloads the MSI, computes the sha256, reads the ProductCode from the
MSI Property table via msitools, and opens an in-repo PR syncing
desktopApp/packaging/winget/. Runs on ubuntu (1x billing) rather than Windows
since msitools reads the Property table fine.
- scripts/bump-winget.sh does the upstream PR. It needs NO new token: it drives
`gh`, which a maintainer already has authenticated, and it does not need
wingetcreate (Windows-only) because the manifests are plain YAML — so it runs
from macOS or Linux.
Add the three reference manifests under desktopApp/packaging/winget/, matching
the schema 1.12.0 shape used upstream. All three validate against Microsoft's
published JSON schemas.
Validation caught one real bug worth noting: an all-digit 64-char InstallerSha256
parses as a YAML *integer* and fails the schema's `string` type, so it is written
quoted. ProductCode is re-read every release because jpackage regenerates it per
build; it is the ARP key `winget upgrade` matches on.
Drops the last package-manager PAT from the secret inventory.
`brew bump-cask-pr` forks homebrew-cask into the token owner's account, which
requires a CLASSIC PAT with the `repo` scope. That scope cannot be narrowed: it
grants write to every repository the owning account can reach, including this
one. As an Actions secret it would be usable by anyone with push access here,
because a pushed branch containing a workflow runs with repo secrets — a strict
escalation for a channel that ships one DMG a month.
Split the work so the token never becomes a CI secret:
- CI (bump-homebrew.yml, now "Sync Homebrew Cask Reference") does the
error-prone bookkeeping with GITHUB_TOKEN only: downloads the DMG, asserts it
is notarized + stapled, computes the sha256, and opens an in-repo PR syncing
desktopApp/packaging/homebrew/amethyst-nostr.rb. This makes it a third sibling
of the amy/geode formula-sync workflows rather than a special case.
- scripts/bump-homebrew-cask.sh does the upstream PR from a maintainer's shell,
re-verifying sha256 and the notarization ticket against the live asset first,
and refusing to submit if either fails. Verified against v1.13.1: the sha256
matches and it correctly refuses on the missing notarization ticket.
Drop HOMEBREW_TOKEN from the secret inventory, and rewrite the two formula
TODO(bootstrap) notes that proposed reintroducing it so a future homebrew-core
submission follows the same local pattern.
BUILDING.md called for a fine-grained PAT scoped to Homebrew/homebrew-cask.
That token cannot exist: a fine-grained PAT's repository selector only lists
repos owned by the resource owner, so a repo you do not own can never be
selected, and Homebrew's API layer authorises against classic OAuth scopes
(x-oauth-scopes) which fine-grained tokens do not emit.
brew bump-cask-pr forks homebrew-cask into the TOKEN OWNER's account, pushes a
branch there, and opens the PR upstream. Homebrew declares the requirement as
CREATE_ISSUE_FORK_OR_PR_SCOPES = ["repo"] in utils/github.rb, so it is a classic
PAT with the `repo` scope.
Add the create-token link and call out that `repo` grants write to every repo
the account can reach -- including this one -- so the CI secret should belong to
a dedicated bot account whose only asset is the fork.
The macOS leg ran only `packageReleaseDmg`, which SIGNS the DMG but does not
notarize it — notarization is a separate Compose task. The `notarization {}`
block in desktopApp/build.gradle.kts only supplies credentials; nothing invoked
it. So every release up to v1.13.1 shipped a signed but unnotarized DMG:
`xcrun stapler validate` reports no ticket on either the .app or the .dmg, and
Gatekeeper blocks it on first launch.
Append `:desktopApp:notarizeReleaseDmg` on the macOS leg. Compose 1.11.1's
AbstractNotarizationTask runs `notarytool submit --wait` and then `stapler
staple` in place, so the asset-collection step still finds the same file. Raise
that leg's inner timeout to 45m since the submit blocks on Apple.
Gate it on the cert AND all three notary secrets, so forks and credential-less
runs keep producing a plain unsigned DMG instead of failing.
Add a verify step that asserts the stapled ticket. The bug survived many
releases precisely because nothing ever checked the outcome.
Also update the reference cask to 1.13.1 and make it pass `brew audit --new
--cask` + `brew style` cleanly: add the missing conflicts_with (the tiling
window manager's cask installs the same Amethyst.app), add depends_on :macos,
fix stanza order, drop the unnecessary `verified:` (url and homepage share a
domain), and widen the zap to the state dirs the source actually uses.
Correct BUILDING.md's macOS state table, which listed a
com.vitorpamplona.amethyst.desktop.plist that does not exist and omitted
~/.amethyst. Note that Java's Preferences API writes to a SHARED
com.apple.java.util.prefs.plist, which is why the cask must not zap it.
All four bump workflows triggered on `release: types: [released]`, which never
fires here: create-release.yml publishes the release with GITHUB_TOKEN, and
GitHub suppresses workflow-triggering events for GITHUB_TOKEN actions. They had
zero runs across every release up to v1.13.1.
Switch them to `workflow_run` on "Create Release Assets" completion, filtered to
a successful tag push. That also removes a latent race: `released` fired while
the matrix legs were still uploading assets, whereas workflow_run fires after
all of them finish.
The workflow_run payload carries no draft/prerelease flags, so add a
resolve-release composite action that reads them back from the API and feeds
assert-stable-release, keeping the defense-in-depth guard intact instead of
inferring stability from the tag string alone.
Also gate the cask/winget bumps on the package existing upstream. Neither
`amethyst-nostr` nor `VitorPamplona.Amethyst` has been bootstrapped, and
bump-cask-pr/winget-releaser can only update an existing package — without the
gate, fixing the trigger would file a spurious [release-ops] issue on every
release.
Docs: correct the claims this uncovered — Homebrew/Winget are not shipping,
macOS is arm64-only (no Intel DMG), the release carries 31 assets (13 Android,
not 12), Maven Central publishes from a step inside deploy-android and lags
repo1 by tens of minutes, RELEASE_NOTES_ID is minor-releases-only, and note the
git-credential-manager hang that blocks the release push.
The linux-portable release leg now wraps the createReleaseDistributable
tree it already builds into a single-file Flatpak bundle and attaches it
to the GH Release as amethyst-desktop-<version>-linux-x64.flatpak.
Packaging fixes to the existing (previously unwired) Flathub manifest:
- add the missing 512x512 icon (copy of desktopApp icon.png) and install
it under hicolor/512x512 instead of the never-present 256px path
- drop the openjdk module + sdk-extension: the jpackage tree bundles its
own trimmed JRE, so /app/jre was pure bloat
- grant --socket=x11 instead of wayland/fallback-x11: Compose Desktop
renders via AWT/skiko (X11-only on Linux, XWayland on Wayland), so
fallback-x11 left the app socketless on Wayland sessions
CI wiring:
- install flatpak tooling + the freedesktop runtime/sdk (version greped
from the manifest so the pin can't drift), retried like other fetches
- inject the AppStream <release> entry for the tagged version at build
time (the checked-in metainfo deliberately carries none)
- flatpak-builder with --disable-rofiles-fuse (GH runners) and a
--state-dir under desktopApp/build so the repo tree stays clean
- collect via the existing asset-name.sh contract (new flatpak ext)
Verified end-to-end locally: built the bundle from the manifest with a
stubbed jpackage tree, installed it, and ran the exported command inside
the sandbox (freedesktop 24.08, args forwarded through the wrapper).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYfNxhPZC3WRrn82Dm2Cb4
Reverts the Kover coverage aggregation (introduced in dca345212b) from every
module, the root aggregation block, the Sonar coverage import, the version
catalog, and BUILDING.md. With the plugin present the 16 GB CI runner OOM-killed
during the Android test+build job even with Kover opt-in disabled; removing it
entirely clears the OOM. No other CI changes — the diff versus main is exactly
the inverse of dca345212b.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Apply Kover (Apache-2.0, build-time only) to the KMP/JVM modules and aggregate at the root
- include amethyst playDebug in the aggregated Kover coverage
Adds a `sonar` Gradle target that activates only when `sonar.host.url`
is present in local.properties (gitignored). Developers who don't opt
in are unaffected: the scanner plugin is neither resolved nor applied,
so no dependency downloads, no extra tasks, no config-time cost
Bugs / inconsistencies found while reviewing the branch for merge:
- dependenciesInfo comment falsely claimed Play "still derives this data
server-side, nothing is lost." Not true: includeInBundle=false means the
.aab carries no dependency metadata, so Play Console's dependency-insights /
SDK-vulnerability alerts go unpopulated (uploads still succeed). Corrected
the comment and the BUILDING.md framing (it called the blob "the one
remaining blocker" when the Arti .so was the bigger one).
- Version-bump workflow was broken: the README told you to run
`build-arti.sh --clean` to refresh Cargo.lock, but the build is now --locked
(fails on a stale lock) and the clone moved to the canonical /tmp path. Added
a dedicated `--regen-lock` mode (clone + cargo generate-lockfile, no NDK
needed) and pointed the docs at it. Verified it reproduces the committed lock
byte-for-byte.
- verify-reproducible.sh: new helper that builds twice and diffs to prove
byte-for-byte reproducibility; uses portable sha256 (sha256sum/shasum) and
plain `sort` so it runs on macOS too.
- README verify recipe referenced paths that only resolved from the repo root
while telling you to cd into tools/arti-build — replaced with the helper.
- rust-toolchain.toml listed four Android targets but only two ABIs ship a
.so; trimmed to match (check_prerequisites adds any other on the fly).
- BUILDING.md: documented that the bundled Arti .so is reproducible-from-source
and that secp256k1/webrtc are version-pinned Maven prebuilts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JtjUcSjjpu4auFndw1QKeU
AGP embeds a dependency-metadata blob (the resolved dependency tree, a
protobuf encrypted with a Google public key) into the APK/AAB signing
block by default. That ciphertext is non-deterministic, so it was the one
remaining thing preventing our release artifacts from being rebuilt
bit-for-bit by a third party.
Disable it via dependenciesInfo { includeInApk = false; includeInBundle =
false }. Combined with the already-pinned toolchain (AGP/Kotlin/R8/JDK 21),
the absence of any build-time clock in BuildConfig, and a deterministic
version name, release APKs now reproduce exactly — letting F-Droid /
Zapstore independently verify our developer-signed builds. Play derives
this dependency data server-side, so nothing is lost there.
Document the guarantee and a diffoscope-based verification recipe in
BUILDING.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JtjUcSjjpu4auFndw1QKeU
The Homebrew formula is the cli module's product (amy), and the CLI already
owns its packaging artifacts under cli/packaging/ (cli/packaging/macos/
amy.entitlements). The root packaging/ dir was new in this branch and held
nothing else, so co-locate the formula with the module that owns it and drop
the stray root dir. Updates the two BUILDING.md references.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sso31DfSF9B6EFCVkEqWD
macOS validation (Developer ID D77MCV9NZ7) confirmed the hardened-runtime
entitlements are correct and load-bearing: amy init derives a secp256k1 key
cleanly, and dropping disable-library-validation reproduces the runtime
dlopen Team-ID failure. The one unverified gap is whether Apple's notary
service accepts the unsigned Mach-O dylibs embedded inside lib/*.jar
(secp256k1/jna/sqlite/skiko), which it inspects recursively.
- create-release.yml: the notarize step now submits with --output-format json,
and on any non-Accepted status dumps `notarytool log` (per-file issues) and
fails — so the first real run names the offending files instead of failing
opaquely. No speculative in-jar signing yet; gather the log first.
- BUILDING.md: record the validation result, the embedded-jar-native risk, the
one-run way to decide it (workflow_dispatch dry_run with MAC_* secrets), and
the staged fixes (sign-in-jar and/or strip the skiko/Compose leak). Note the
desktop app shares the same jars and needs its own dry-run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sso31DfSF9B6EFCVkEqWD
Sign the macOS jlink image (amy-<version>-macos-arm64.tar.gz) so users who
download it directly clear Gatekeeper. Reuses the same Developer ID cert and
the six MAC_* secrets as the desktop DMG; no-op when they're absent.
- .github/actions/import-macos-cert: factor the throwaway-keychain cert import
into a composite action; the desktop leg now uses it too (was inline).
- create-release.yml (build-cli macOS leg): import the cert, then codesign
every Mach-O binary in the bundled JRE (executables get hardened-runtime
entitlements, dylibs don't) and notarize via notarytool --wait. Runs before
the collect step so the tarred image is signed. Job timeout 30->45 min for
notarization headroom.
- cli/packaging/macos/amy.entitlements: hardened-runtime entitlements; the
disable-library-validation key lets the JVM load the secp256k1 native dylib
it extracts from a jar at runtime (would otherwise crash under notarization).
- BUILDING.md: document the tarball signing, the no-stapling/online-check
caveat, and that the Homebrew-core jvm bundle is intentionally left unsigned.
Untested end-to-end (no macOS runner / Apple creds here) — validate with a
workflow_dispatch dry-run once the secrets are provisioned.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sso31DfSF9B6EFCVkEqWD
Enable distributing the `amy` CLI via Homebrew-core (mainline formulae).
Homebrew-core builds in a network-sandboxed env, so a from-source Gradle
build can't resolve Maven deps there; the accepted pattern for JVM tools is a
pre-built no-JRE jar bundle + `depends_on "openjdk"`. installDist already
produces exactly that (bin/amy + lib/*.jar, no bundled runtime).
- create-release.yml: publish `amy-<version>-jvm.tar.gz` (the installDist tree)
as a release asset on the linux leg. Pure JVM bytecode, so one
platform-independent artifact serves every OS.
- packaging/homebrew/amy.rb: reference formula (depends_on openjdk, livecheck
for BrewTestBot auto-bumps, `amy --help` smoke test). Not consumed by any
build here — it's the artifact to submit to Homebrew/homebrew-core.
- BUILDING.md: homebrew-core submission runbook; note that the desktop app is
already on mainline Homebrew (homebrew/cask); document name-collision and
pre-built-jar review caveats.
- asset-name.sh: document the jvm bundle naming exception.
Verified locally: :cli:installDist builds with only a JDK (no Android SDK),
and the extracted bundle runs via JAVA_HOME (`amy --help` exits 0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sso31DfSF9B6EFCVkEqWD
Add gated code-signing + notarization for the macOS desktop DMG so it can
clear Gatekeeper and stay in Homebrew's main cask (unsigned casks are
rejected after 2026-09-01).
- desktopApp/build.gradle.kts: macOS signing{}/notarization{} blocks, gated
on the AMETHYST_MAC_SIGN_IDENTITY env var. Absent => unsigned DMG, exactly
as before, so local dev and PR CI are unaffected.
- create-release.yml: import a Developer ID cert into a throwaway keychain on
the macOS leg and export the signing/notary env. Soft-gated on the
MAC_CERTIFICATE_P12 secret — no secret => unsigned build.
- BUILDING.md: document the six MAC_* secrets, how to generate them, and flip
the unsigned-cask fallback note to reflect the wiring is now in place
(pending Apple credentials).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015sso31DfSF9B6EFCVkEqWD
Make BUILDING.md everything a fork needs to build and release, and add a
maintainer-facing ship checklist.
- BUILDING.md: full CI secrets inventory (Android keystore vs GPG/Maven, with
generation commands), a distribution-channels table (GitHub, Maven, Play,
F-Droid pull, Zapstore, Homebrew/Winget), and a "generated & vendored
artifacts" section (Material Symbols subset, Arti native libs).
- RELEASE_OPS.md (new, repo root): Amethyst-specific shipping — pre-tag
checklist, per-channel rollout (manual Play AAB, Zapstore `zsp publish` with
our nsec, F-Droid build-from-source), the push-notification server
(amethyst-push-notif-server), secret ownership, and verification.
- README.md: slim the Deploying section to point at both docs, fix the stale
quartz dependency version, and add a Maven Central badge plus JitPack
snapshot docs.
- android-expert skill: drop the duplicated build-config block, delegate to
gradle-expert, keep only the Android-specific flavor note.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
linuxdeploy auto-walks every binary in the staged AppDir with ldd to
bundle their shared-library deps. That fights jpackage's self-contained
output on two axes and aborts the createReleaseAppImage task before any
AppImage is produced:
- The bundled JRE puts libjvm.so at usr/lib/runtime/lib/server/, while
its sibling libs (libmanagement.so, libawt_xawt.so, libfontmanager.so
and others in usr/lib/runtime/lib/) have RPATH=$ORIGIN. ldd cannot
resolve libjvm.so from those libs without help, so linuxdeploy errors:
ERROR: Could not find dependency: libjvm.so
ERROR: Failed to deploy dependencies for existing files
- The bundled libvlc plugins under usr/lib/app/resources/vlc/ are
UPX-compressed by the vlc-setup plugin. patchelf cannot read their
section headers, so linuxdeploy aborts:
ERROR: Call to patchelf failed:
patchelf: no section headers. The input file is probably a
statically linked, self-decompressing binary
- Even after working around libjvm.so via LD_LIBRARY_PATH, several VLC
libs have RUNPATH pointing at the wrong directory, so ldd fails on
libva.so.2 / libvlccore.so.9 next.
This is why v1.09.2 shipped .deb + .rpm from the linux leg but no
.AppImage and no linux .tar.gz from the linux-portable leg — the gradle
step failed on linuxdeploy, so the subsequent "Build portable archives"
step never ran.
Swap to appimagetool, which only embeds the AppDir as-is into a
SquashFS-backed, runtime-prepended AppImage and never touches the
contents. jpackage already bundles a complete, self-contained app —
we don't need linuxdeploy's dep-discovery. AppRun continues to handle
LD_LIBRARY_PATH at launch.
Verified locally on ubuntu-24.04: a fresh
:desktopApp:createReleaseAppImage now produces a valid 254 MB
Amethyst-1.09.2-x86_64.AppImage in ~30 s on top of an up-to-date
createReleaseDistributable.
Side changes:
- Workflow installs desktop-file-utils (appimagetool 1.9.0 calls
desktop-file-validate on the .desktop entry).
- BUILDING.md updated with the new local-dev fetch command.
- .gitignore replaces the linuxdeploy paths with appimagetool's.
The AppImage build inputs (AppRun, .desktop entry, icon, and the
CI-fetched linuxdeploy binary) are consumed only by desktopApp's
createReleaseAppImage task. Co-locating them under
desktopApp/packaging/appimage/ removes the `../` path escape from the
build script and keeps all desktop packaging assets inside the module.
https://claude.ai/code/session_0137ULcfJkASmfmffFBdW8ac