Files
amethyst/BUILDING.md
T
Claude 87c16afe1d refactor: move amy.rb into cli/packaging/homebrew (was root packaging/)
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
2026-06-18 13:13:43 +00:00

614 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Building Amethyst Desktop
This guide has everything **any fork** needs to build Amethyst from source and
cut its own release: prerequisites, build commands, the CI release pipeline, the
secrets it needs, the distribution channels, and one-time bootstrap steps.
> **Amethyst maintainers:** the account-specific checklist for shipping the
> official build (Play Console upload, Zapstore `zsp publish` with our nsec,
> secret ownership) lives in [`RELEASE_OPS.md`](RELEASE_OPS.md). This
> file stays fork-generic.
- [Prerequisites](#prerequisites)
- [Clone + first build](#clone--first-build)
- [Generated & vendored artifacts](#generated--vendored-artifacts)
- [Per-format build commands](#per-format-build-commands)
- [Asset naming contract](#asset-naming-contract)
- [Release runbook](#release-runbook)
- [Secrets the CI needs](#secrets-the-ci-needs)
- [Distribution channels](#distribution-channels)
- [Bootstrap runbook (one-time)](#bootstrap-runbook-one-time)
- [Troubleshooting installs](#troubleshooting-installs)
- [Uninstall + state paths](#uninstall--state-paths)
- [Incident response](#incident-response)
- [Fallback plans](#fallback-plans)
---
## Prerequisites
All platforms:
- **JDK 21** (Zulu or Temurin recommended)
- **Git**
Platform-specific:
- **macOS**: Xcode Command Line Tools (`xcode-select --install`)
- **Windows**: WiX Toolset 3.x on PATH (for MSI). `winget install WiXToolset.WiXToolset`
- **Linux (all)**: nothing extra for `.deb`; `rpm` + `fakeroot` for `.rpm`;
`appimagetool` + `desktop-file-utils` for AppImage
Install Linux RPM tooling:
```bash
# Debian/Ubuntu
sudo apt-get install -y rpm fakeroot
# Fedora
sudo dnf install -y rpm-build
```
Install appimagetool locally (CI fetches its own — SHA-verified):
```bash
# Debian/Ubuntu — appimagetool calls desktop-file-validate on the .desktop entry
sudo apt-get install -y desktop-file-utils
curl -fsSL -o desktopApp/packaging/appimage/appimagetool-x86_64.AppImage \
https://github.com/AppImage/appimagetool/releases/download/1.9.0/appimagetool-x86_64.AppImage
chmod +x desktopApp/packaging/appimage/appimagetool-x86_64.AppImage
```
---
## Clone + first build
```bash
git clone https://github.com/vitorpamplona/amethyst.git
cd amethyst
# Dev loop (launches Amethyst Desktop)
./gradlew :desktopApp:run
# Package for current OS
./gradlew :desktopApp:packageDistributionForCurrentOS
```
---
## Generated & vendored artifacts
Two build inputs are **generated by tools but committed to the repo**, so a
normal build or release does **not** run either — Gradle just consumes the
checked-in output. You only regenerate them under the specific conditions below,
and each has its own guide:
| Artifact | Committed at | Regenerate when | Guide |
|---|---|---|---|
| **Material Symbols subset font** | `commons/src/commonMain/composeResources/font/material_symbols_outlined.ttf` | You add/remove a `MaterialSymbol("\uXXXX")` codepoint in `MaterialSymbols.kt`, or bump the upstream font | [`tools/material-symbols-subset/README.md`](tools/material-symbols-subset/README.md) — run `./tools/material-symbols-subset/subset.sh` |
| **Arti (Tor) native libs** | `amethyst/src/main/jniLibs/*.so` | You update the pinned Arti version, change the JNI wrapper, or want to reproduce the binaries | [`tools/arti-build/README.md`](tools/arti-build/README.md) |
> **Material Symbols is mandatory after icon changes.** The bundled font is a
> ~210-glyph subset; a new codepoint that isn't in it renders as tofu (□) at
> runtime. Regenerate and commit the `.ttf` alongside the `MaterialSymbols.kt`
> change. Reusing an existing codepoint needs no regeneration.
Both tools have their own prerequisites (`fonttools`/`brotli` for the font; a
Rust toolchain + Android NDK 25+ for Arti) documented in their READMEs — they
are **not** required to build Amethyst from the committed sources.
---
## Per-format build commands
| Artifact | Command | Output |
|---|---|---|
| macOS DMG (host arch) | `./gradlew :desktopApp:packageReleaseDmg` | `desktopApp/build/compose/binaries/main-release/dmg/Amethyst-*.dmg` |
| Windows MSI | `./gradlew :desktopApp:packageReleaseMsi` | `desktopApp/build/compose/binaries/main-release/msi/Amethyst-*.msi` |
| Linux `.deb` | `./gradlew :desktopApp:packageReleaseDeb` | `desktopApp/build/compose/binaries/main-release/deb/amethyst_*.deb` |
| Linux `.rpm` | `./gradlew :desktopApp:packageReleaseRpm` | `desktopApp/build/compose/binaries/main-release/rpm/amethyst-*.rpm` |
| Linux AppImage | `./gradlew :desktopApp:createReleaseAppImage` | `desktopApp/build/appimage/Amethyst-*-x86_64.AppImage` |
| Windows `.zip` portable | See below (inline `7z`) | — |
| Linux `.tar.gz` portable | See below (inline `tar`) | — |
**Inline portable archives** (run after `createReleaseDistributable`):
```bash
./gradlew :desktopApp:createReleaseDistributable
# Linux tar.gz
VER=$(grep -E '^app\s*=' gradle/libs.versions.toml | head -1 | cut -d'"' -f2)
( cd desktopApp/build/compose/binaries/main-release/app \
&& tar czf "../../../../portable/amethyst-desktop-${VER}-linux-x64.tar.gz" Amethyst/ )
# Windows .zip (PowerShell)
Compress-Archive -Path desktopApp\build\compose\binaries\main-release\app\Amethyst `
-DestinationPath "desktopApp\build\portable\amethyst-desktop-$env:VER-windows-x64.zip"
```
Cross-platform architecture note: **`jpackage` cannot cross-compile**. An Intel
DMG must be built on `macos-13` (x64); an ARM DMG must be built on `macos-14`
or later. CI runs both.
---
## Asset naming contract
All GH Release assets follow:
```
amethyst-desktop-<version>-<family>-<arch>.<ext>
```
Where:
| Field | Values |
|---|-------------------------------------------------------|
| `<version>` | Tag stripped of leading `vX.YY.ZZ` |
| `<family>` | `macos`, `windows`, `linux` |
| `<arch>` | `x64`, `arm64` |
| `<ext>` | `dmg`, `msi`, `zip`, `deb`, `rpm`, `AppImage`, `tar.gz` |
Single source of truth: [`scripts/asset-name.sh`](scripts/asset-name.sh).
Package manager manifests (Homebrew cask, Winget) depend on this exact scheme —
any change is a breaking contract.
Examples:
- `amethyst-desktop-1.12.1-macos-x64.dmg`
- `amethyst-desktop-1.12.1-macos-arm64.dmg`
- `amethyst-desktop-1.12.1-windows-x64.msi`
- `amethyst-desktop-1.12.1-linux-x64.AppImage`
---
## Release runbook
The release flow is driven by a tag push. Every cut ships Android + Desktop +
Quartz library in one pipeline.
1. **Bump the app version and Android `versionCode`** in
`gradle/libs.versions.toml` (`appCode` is a monotonic integer — it must
increment even when `app` is unchanged):
```toml
[versions]
app = "1.08.1" # new semver
appCode = "449" # Android versionCode
```
`amethyst/build.gradle.kts` reads both from the catalog
(`versionCode = libs.versions.appCode.get().toInt()`), so there is nothing
else to edit.
2. **Commit + tag + push**:
```bash
git commit -am "chore(release): 1.12.1"
git tag -s v1.12.1 -m "Release 1.12.1"
git push && git push --tags
```
3. **Wait** for the `Create Release Assets` workflow to finish (~2530 min).
4. **Verify**:
- GH Release contains 8 desktop assets + 12 Android assets
- Asset sizes look sane (see §Enforce asset size budget — CI auto-fails at 1 GB/asset)
- Intel + ARM DMGs both present
- Android flow unchanged
5. **Stable vs prerelease** — a tag containing `-rc`, `-beta`, `-alpha`, `-dev`,
or `-snapshot` is auto-classified as prerelease. Stable tags trigger the
Homebrew + Winget bump workflows.
### Dry-run (no tag push)
Use `workflow_dispatch` to exercise the full matrix without publishing:
```bash
gh workflow run create-release.yml \
-f dry_run=true \
-f test_tag=v0.0.0-dryrun \
--ref feat/my-branch
```
Assets are built and size-checked, but not uploaded; bump workflows do not
fire. Use for pre-merge validation of workflow changes.
### Version constraint: tag must match `libs.versions.toml`
The first step in each build-desktop matrix job asserts:
```
tag (stripped of 'v') == gradle/libs.versions.toml [versions] app
```
If they drift, the workflow fails fast. Always bump the TOML first, then tag.
### NEVER change Windows `upgradeUuid`
`desktopApp/build.gradle.kts:upgradeUuid` is the MSI product family GUID.
Changing it breaks in-place upgrades for existing Windows users — they must
uninstall before a new release. Leave it alone forever.
---
## Secrets the CI needs
The `Create Release Assets` workflow reads these from GitHub repo secrets. A
fork must provide its **own** values — none are inherited. (`GITHUB_TOKEN` is
provided automatically; everything else you set yourself.)
| Secret | What it is | Used for |
|---|---|---|
| `SIGNING_KEY` | Base64 of your **Android keystore** (`.jks`/`.keystore`) | Signs the Play + F-Droid **AAB and APK** |
| `KEY_ALIAS` | Keystore key alias | Same Android signing step |
| `KEY_STORE_PASSWORD` | Keystore password | Same |
| `KEY_PASSWORD` | Key password | Same |
| `SONATYPE_USERNAME` | Maven Central (Sonatype) user token name | Publishing the `quartz` library |
| `SONATYPE_PASSWORD` | Maven Central user token password | Same |
| `SIGNING_PRIVATE_KEY` | **GPG/PGP** private key, ASCII-armored | Signs the Maven artifacts (Central requires it) |
| `SIGNING_PASSWORD` | Passphrase for that GPG key | Same |
| `MAC_CERTIFICATE_P12` | Base64 of your **Apple Developer ID Application** cert (`.p12`, includes the private key) | Signs the macOS desktop **DMG** and the macOS **amy** jlink tarball |
| `MAC_CERTIFICATE_PASSWORD` | Password set when exporting the `.p12` | Imports the cert into the CI keychain |
| `MAC_SIGN_IDENTITY` | Full identity string, e.g. `Developer ID Application: Your Name (TEAMID)` | The `codesign` identity to sign with |
| `MAC_NOTARY_APPLE_ID` | Apple ID email of the notarization account | Apple notarization (`notarytool`) |
| `MAC_NOTARY_PASSWORD` | **App-specific** password for that Apple ID (not the login password) | Same |
| `MAC_NOTARY_TEAM_ID` | 10-char Apple Developer **Team ID** | Same |
| `HOMEBREW_TOKEN` | PAT for `Homebrew/homebrew-cask` | Desktop cask bump (stable tags) |
| `WINGET_TOKEN` | PAT for `microsoft/winget-pkgs` | Desktop winget bump (stable tags) |
| `CROWDIN_PERSONAL_TOKEN`, `CROWDIN_PROJECT_ID` | Crowdin API creds | Translation sync (separate workflow, not the release) |
Note the **three distinct signing identities** people often conflate:
`SIGNING_KEY` + `KEY_*` is the **Android keystore**; `SIGNING_PRIVATE_KEY` +
`SIGNING_PASSWORD` is the **GPG key** for Maven Central; `MAC_CERTIFICATE_*` +
`MAC_SIGN_IDENTITY` + `MAC_NOTARY_*` is the **Apple Developer ID** for the macOS
desktop DMG. They are unrelated — each comes from a different authority.
The macOS signing secrets are **optional**: if `MAC_CERTIFICATE_P12` is unset
the release workflow still builds the DMG **and** the macOS `amy` tarball, just
**unsigned** (the previous behavior). Provision all six to switch signing +
notarization on for both. Obtaining them requires Apple Developer Program
membership ($99/yr). The same one certificate signs both artifacts.
The macOS `amy` tarball is the jlink image (bundled JRE), so signing it means
codesigning every Mach-O binary in that runtime with hardened-runtime
entitlements (`cli/packaging/macos/amy.entitlements` — needed so the JVM can
load the secp256k1 native library it extracts at runtime). A loose `.tar.gz`
cannot be **stapled** (Apple's `stapler` only handles `.app`/`.dmg`/`.pkg`), so
Gatekeeper verifies notarization **online** on first run — fine for a CLI.
Note the Homebrew-core jvm bundle (`amy-<version>-jvm.tar.gz`) is **not** signed:
Homebrew removes the quarantine attribute on its own downloads.
> **Validated (Developer ID `D77MCV9NZ7`):** signing every Mach-O in the bundled
> JRE with hardened runtime + `amy.entitlements` lets `amy init` derive a key via
> secp256k1 with no library-validation crash. Dropping `disable-library-validation`
> reproduces `UnsatisfiedLinkError: … different Team IDs` on the runtime-extracted
> `libsecp256k1-jni.dylib` — so that entitlement is load-bearing, not decorative.
>
> **Open risk — embedded jar natives.** The notary service unpacks `lib/*.jar`
> recursively and checks every Mach-O for a signature + hardened runtime. Our
> sign loop only touches loose files, so 9 unsigned natives ride along inside
> jars on a macOS build: `secp256k1` (1, required at runtime), `jna` (2),
> `sqlite` (2), and `skiko` (4, dead weight — Compose UI the CLI never renders).
> Whether `notarytool` returns `Accepted` or `Invalid` on these is **unverified**
> (the local validation had no notary creds). **Decide it with one run:** set the
> six `MAC_*` secrets and trigger `create-release.yml` via `workflow_dispatch`
> with `dry_run=true` — the sign+notarize step runs regardless of `dry_run` and
> now prints the per-file notary log on a non-`Accepted` verdict. If it comes
> back `Invalid`, the fix is to codesign the dylibs *inside* those jars before
> zipping (and/or strip the unused `skiko`/Compose jars from the CLI image — the
> `:commons` core/ui split the size budget already flags). The **desktop** app
> bundles the same jars through Compose/jpackage notarization, so run a desktop
> dry-run too; its in-jar handling differs and is likewise unverified.
Generating the values:
```bash
# Android keystore → base64 for SIGNING_KEY (one line, no wrapping)
keytool -genkey -v -keystore upload.jks -keyalg RSA -keysize 2048 \
-validity 10000 -alias upload # creates the keystore (once)
base64 -i upload.jks | tr -d '\n' # paste output into SIGNING_KEY
# GPG key → armored private key for SIGNING_PRIVATE_KEY
gpg --full-generate-key # create the key (once)
gpg --armor --export-secret-keys <KEY_ID> # paste output into SIGNING_PRIVATE_KEY
# Apple Developer ID Application cert → base64 for MAC_CERTIFICATE_P12.
# In Keychain Access, export the "Developer ID Application: ..." cert (with its
# private key) as a .p12, setting an export password (-> MAC_CERTIFICATE_PASSWORD).
base64 -i developer_id.p12 | tr -d '\n' # paste output into MAC_CERTIFICATE_P12
security find-identity -v -p codesigning # shows the exact MAC_SIGN_IDENTITY string
# MAC_NOTARY_PASSWORD is an app-specific password from https://appleid.apple.com
# (Sign-In and Security -> App-Specific Passwords), NOT your Apple ID login.
```
`SONATYPE_USERNAME`/`SONATYPE_PASSWORD` are a **user token** from
<https://central.sonatype.com> (Account → Generate User Token), not your login.
A fork that doesn't publish a library can drop the `Publish Quartz Lib` step and
the four Sonatype/GPG secrets.
---
## Distribution channels
One `v*` tag fans out to several channels. Which apply depends on where a fork
distributes; the official Amethyst rollout for each is in
[`RELEASE_OPS.md`](RELEASE_OPS.md).
| Channel | How it ships | Push or pull |
|---|---|---|
| **GitHub Releases** | The release workflow builds + signs all assets and attaches them to the tag's Release | Automatic (CI) |
| **Maven Central** | Same workflow runs `publishAllPublicationsToMavenCentral` for `quartz` | Automatic (CI) |
| **Google Play** | Download the signed `amethyst-googleplay-<version>.aab` from the GH Release and upload it in Play Console | **Manual push** |
| **F-Droid** | F-Droid's build server detects the new tag and **builds the `fdroid` flavor from source** per its recipe in the external [`fdroiddata`](https://gitlab.com/fdroid/fdroiddata) repo, then signs + publishes itself | **Pull (build-from-source)** |
| **Zapstore** | The [`zsp`](https://zapstore.dev/) CLI reads [`zapstore.yaml`](zapstore.yaml) and publishes a Nostr software-release event signed with the app's nsec | **Manual push (Nostr)** |
| **Homebrew + Winget** | `bump-homebrew.yml` / `bump-winget.yml` open version-bump PRs on stable tags | Automatic (CI) |
Two channels need the build to stay split into product flavors (see
`amethyst/build.gradle.kts` → `productFlavors`):
- **`play`** carries Firebase / Google Play Services (push notifications, ML
Kit, etc.) → the Google Play AAB.
- **`fdroid`** swaps those for UnifiedPush and no-op/open-source
implementations (`amethyst/src/fdroid/…`) so the build is free of proprietary
dependencies → what F-Droid builds and what Zapstore distributes.
**F-Droid is pull, not push.** We never upload to F-Droid; its server builds our
tagged source. Keeping the `fdroid` flavor proprietary-free and the
`fastlane/metadata/android/` descriptions current is all that's required. F-Droid
reads an optional per-release changelog from
`fastlane/metadata/android/en-US/changelogs/<versionCode>.txt`.
---
## Bootstrap runbook (one-time)
### Secrets to provision in GitHub repo settings
The full secret inventory is in [§ Secrets the CI needs](#secrets-the-ci-needs).
The two that need the most setup care are the package-manager PATs, because of
their token type and scope:
| Secret | Purpose | Scope |
|---|---|---|
| `HOMEBREW_TOKEN` | Bump Homebrew cask | Fine-grained PAT — `Homebrew/homebrew-cask` only — `Contents: write` + `Pull requests: write` — 90d expiry |
| `WINGET_TOKEN` | Submit Winget manifests | Classic PAT — `public_repo` — 90d expiry (dedicated bot account preferred; `vedantmgoyal9/winget-releaser` does not support fine-grained) |
Rotate both on a 90-day cadence. Owner: assigned via `RELEASE_OPS.md`
or equivalent issue tracker. On rotation, paste new token and run
`gh workflow run bump-homebrew.yml` on the most recent stable tag to verify.
### Homebrew cask (one-time initial PR)
```bash
brew bump-cask-pr amethyst-nostr \
--version 1.12.1 \
--url "https://github.com/vitorpamplona/amethyst/releases/download/v1.12.1/amethyst-desktop-1.12.1-macos-arm64.dmg"
```
The cask filename is `amethyst-nostr` (not `amethyst` — that's taken by a
tiling window manager). After the first PR is merged, `bump-homebrew.yml`
auto-submits new version bumps on each stable release.
> **The desktop app is already on mainline Homebrew.** `homebrew/cask` *is* the
> mainline cask repo — GUI apps live in homebrew-**cask**, CLIs in
> homebrew-**core**; both are "mainline." A private tap is only the *fallback*
> if Homebrew ever rejects the (now signed + notarized) cask.
### Homebrew-core formula for the `amy` CLI (one-time initial PR)
The CLI goes to **homebrew-core** (mainline formulae), not homebrew-cask —
casks are for GUI apps. homebrew-core builds in a **network-sandboxed**
environment, so a from-source Gradle build can't resolve its Maven
dependencies there. Instead the formula downloads the pre-built **no-JRE jar
bundle** `amy-<version>-jvm.tar.gz` (published by `create-release.yml`) and
`depends_on "openjdk"`. The reference formula lives at
[`cli/packaging/homebrew/amy.rb`](cli/packaging/homebrew/amy.rb).
To submit:
```bash
# 1. Grab the published asset's sha256
curl -fsSL -o amy-jvm.tar.gz \
https://github.com/vitorpamplona/amethyst/releases/download/v1.12.1/amy-1.12.1-jvm.tar.gz
shasum -a 256 amy-jvm.tar.gz
# 2. Fill the url + sha256 into cli/packaging/homebrew/amy.rb, then open the PR
brew create --set-name amy --tap homebrew/core \
https://github.com/vitorpamplona/amethyst/releases/download/v1.12.1/amy-1.12.1-jvm.tar.gz
# (paste the reference formula body, run `brew audit --new amy`,
# `brew install --build-from-source amy`, `brew test amy`, then PR it.)
```
Caveats that the maintainer must weigh before submitting:
- **Name collision.** `amy` may already exist in homebrew-core — check with
`brew search amy` first. If taken, fall back to `amethyst-cli`.
- **Pre-built-jar scrutiny.** homebrew-core prefers source builds; downloading
a jar bundle is an accepted-but-reviewed pattern for JVM tools. Be ready to
justify it (sandboxed Gradle can't fetch Maven deps).
- **Bundle size.** The bundle is ~70 MB today because `:commons` leaks
Compose/Skiko jars onto the CLI classpath. Trimming that (a `:commons`
core/ui split) would shrink it and smooth review — tracked as a follow-up.
After the formula merges, the `livecheck` block lets homebrew-core's BrewTestBot
auto-open version-bump PRs on each stable release — no token or workflow on our
side (unlike the cask/winget bumps).
### Winget (one-time initial submission)
```bash
wingetcreate new \
https://github.com/vitorpamplona/amethyst/releases/download/v1.12.1/amethyst-desktop-1.12.1-windows-x64.msi
```
Set `PackageIdentifier = VitorPamplona.Amethyst`. After the first manifest is
merged into `microsoft/winget-pkgs`, `bump-winget.yml` auto-submits new
version manifests.
---
## Troubleshooting installs
### macOS — Gatekeeper "damaged and can't be opened"
Amethyst Desktop is currently unsigned. First-time launch requires:
1. **Right-click → Open** on the app (don't double-click) — then click **Open** on the Gatekeeper dialog
2. Or: `xattr -cr /Applications/Amethyst.app` to strip quarantine
3. Or: System Settings → Privacy & Security → "Open Anyway" after a blocked launch
Recommended path: install via Homebrew (`brew install --cask amethyst-nostr`)
— cask flow handles this seamlessly.
### Windows — SmartScreen "Windows protected your PC"
Amethyst Desktop is currently unsigned (no Authenticode). First-time launch:
1. Click **More info** on the SmartScreen dialog
2. Click **Run anyway**
Alternatively use `winget install VitorPamplona.Amethyst` — winget install
bypasses the UI dialog after accepting the installer's inherent trust.
### Linux AppImage won't execute
```bash
chmod +x Amethyst-*.AppImage
./Amethyst-*.AppImage
```
On Fedora Silverblue / very minimal distros, FUSE might be missing. Use
`--appimage-extract-and-run`:
```bash
./Amethyst-*.AppImage --appimage-extract-and-run
```
---
## Uninstall + state paths
State is shared across install channels (DMG, Homebrew, MSI, Winget, .deb,
.rpm, AppImage, tar.gz). Switching channels does not duplicate data but may
expose downgrade migration risks — **prefer a single install channel per
machine**.
| OS | App location | State directories |
|---|---|---|
| macOS | `/Applications/Amethyst.app` | `~/Library/Application Support/Amethyst`<br>`~/Library/Preferences/com.vitorpamplona.amethyst.desktop.plist`<br>`~/Library/Caches/Amethyst` |
| Windows | `%LOCALAPPDATA%\Amethyst` or `C:\Program Files\Amethyst` | `%APPDATA%\Amethyst`<br>`%LOCALAPPDATA%\Amethyst` |
| Linux (deb/rpm) | `/opt/amethyst` | `~/.config/amethyst`<br>`~/.local/share/amethyst`<br>`~/.cache/amethyst` |
| Linux (AppImage/tar.gz) | user-chosen | Same as above |
Uninstall:
- Homebrew: `brew uninstall --cask amethyst-nostr && brew zap amethyst-nostr`
- Winget: `winget uninstall VitorPamplona.Amethyst`
- .deb: `sudo apt remove amethyst`
- .rpm: `sudo dnf remove amethyst`
- AppImage / tar.gz: delete the file / extracted directory
- macOS `.dmg`: drag from `/Applications` to Trash, then delete state dirs manually
---
## Incident response
### Bad GH Release asset
1. Immediately mark release as prerelease (pauses bump workflows):
```bash
gh release edit v1.08.1 --prerelease
```
2. Delete the bad asset:
```bash
gh release delete-asset v1.08.1 amethyst-desktop-1.08.1-macos-arm64.dmg --yes
```
3. Rebuild locally or rerun the failing matrix job:
```bash
gh run rerun <run-id> --failed
```
4. Flip back to stable once verified (re-fires bump workflows — confirm fix first):
```bash
gh release edit v1.08.1 --prerelease=false
```
### Bad build reached Homebrew
**Preferred**: ship a point release (e.g. v1.08.2) — users on v1.08.1 get the
fix via `brew upgrade`.
**Alternative**: close the open PR in `Homebrew/homebrew-cask` before merge,
or file a revert PR if already merged. Typical Homebrew turn-around: 12 days.
### Bad build reached Winget
Winget manifests are append-only — no hard unpublish. Options:
1. Ship a point release (preferred — users upgrade via `winget upgrade`)
2. File a manifest-removal PR against `microsoft/winget-pkgs`. Moderator
review: 2472h.
### User-facing communication
On any incident:
1. Edit the release body on GitHub with a warning banner + workaround
2. Pin a GH Issue with downgrade instructions per channel
3. Announce via Nostr relay + project social channels
---
## Fallback plans
### macOS Intel runner retirement
GitHub's `macos-13` runner will eventually be deprecated. Monitor
<https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners>
for the deprecation date. When it hits:
1. Drop the `macos-13` matrix entry from `.github/workflows/create-release.yml`
2. Add a cross-arch build step on `macos-14` using a bundled x64 JDK + `jpackage --mac-signing-prefix` shenanigans, OR accept that only Apple Silicon DMGs ship and direct Intel users to `winget` on a Parallels VM or to rebuild from source.
3. Update README install matrix to reflect the change.
### Homebrew main-cask rejects unsigned app (post-Sept 1 2026)
Homebrew has committed to disabling unsigned casks in `Homebrew/homebrew-cask`
on 2026-09-01. Before that date:
**Option A (wiring done — needs Apple creds)**: The `signing { sign.set(true) }`
+ `notarization {}` blocks are already in `desktopApp/build.gradle.kts` (gated on
the `AMETHYST_MAC_SIGN_IDENTITY` env var), and the macOS leg of
`create-release.yml` imports a Developer ID cert into a throwaway keychain and
exports the signing/notary env. It all stays a **no-op until the six
`MAC_*`/notary secrets are provisioned** (see [§ Secrets the CI
needs](#secrets-the-ci-needs)) — until then the DMG builds unsigned. To turn it
on: join the Apple Developer Program ($99/yr), create a *Developer ID
Application* certificate, generate an app-specific password, and set the six
secrets. The first signed+notarized DMG is best validated with a
`workflow_dispatch` dry-run before a real tag.
**Option B**: Pivot to a private Homebrew tap:
```bash
# Create repo: vitorpamplona/homebrew-amethyst
# Update bump-homebrew.yml:
# tap: vitorpamplona/amethyst
# cask: amethyst-nostr
# Users install: brew tap vitorpamplona/amethyst && brew install --cask amethyst-nostr
```
Note: a private tap does NOT bypass Gatekeeper itself (macOS OS-level) — users
still see the "unsigned developer" dialog. Tap only sidesteps Homebrew's
internal policy.
---
## Follow-up channels (separate PRs)
- **AUR** (`amethyst-desktop-bin`) — blocked on AUR account ownership decision
- **Scoop** (Windows) — blocked on bucket strategy (own vs Extras)
- **Flathub** — deferred (moderate ongoing maintenance)