mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-10-30 15:48:36 +00:00
geode (the standalone relay) and relayBench (the relay benchmark) now live in their own repo, with their own CI, release pipeline (bundles, .deb/.rpm, GHCR image, Homebrew formula sync) and Maven Central publication. - settings: drop :geode and :relayBench; add -PgeodeDir=../geode-relay to build a local checkout instead of the published artifact. - cli (amy serve) and the amethyst unit tests take libs.geode (com.vitorpamplona.geode:geode:1.20.0) and its test fixtures. - CI/release: remove test-geode, build-geode, docker-geode and the geode Homebrew formula sync; asset-name.sh keeps desktop + amy only. - cordn test stack finds geode in a sibling geode-relay checkout (or $GEODE). - Docs and skills point at the new repo; release asset counts drop by the 10 geode assets. geode 1.20.0 must be published to Maven Central before this builds without -PgeodeDir. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T4Hviaxdm4NXDwxWRRsXYs
573 lines
31 KiB
Markdown
573 lines
31 KiB
Markdown
# Release Ops (Amethyst maintainers)
|
||
|
||
This is the **operational checklist the Amethyst team follows to ship a
|
||
release** — the account-specific, push-the-buttons side of cutting a version.
|
||
The *generic* build/release mechanics (how the CI pipeline works, the asset
|
||
naming contract, the secret names a fork must set, desktop packaging) live in
|
||
[`BUILDING.md`](BUILDING.md). Read that first; this doc only covers what is
|
||
specific to shipping the official Amethyst artifacts.
|
||
|
||
> Forks: you do **not** need this file. `BUILDING.md` has everything you need to
|
||
> build and release your own fork. This describes our accounts and channels.
|
||
|
||
---
|
||
|
||
## At a glance
|
||
|
||
A release is one tag push that fans out to these distribution channels:
|
||
|
||
| Channel | Mechanism | Who pushes |
|
||
|---|---|---|
|
||
| **GitHub Releases** | Automatic — the `Create Release Assets` workflow builds + signs everything on the `v*` tag | CI |
|
||
| **Google Play** | **Manual** — download the signed AAB from the GH Release, upload in Play Console | Maintainer |
|
||
| **F-Droid** | **Pull** — F-Droid's build server builds the `fdroid` flavor from source when it sees the new tag | F-Droid (we just maintain the recipe + metadata) |
|
||
| **Zapstore** | `amy releases publish` signs the APK's NIP-82 events (as `zsp` did), reading `zapstore.yaml`, with Amethyst's nsec | Maintainer |
|
||
| **Orchard** | The same `amy releases publish` run signs NIP-82 events naming the GitHub Release's `.flatpak` bundles | Maintainer |
|
||
| **Desktop self-update** | The same `amy releases publish` run signs the NIP-82 events the desktop app updates from | Maintainer |
|
||
| **Flathub** | **Not submitted yet.** Once accepted: a PR to the Flathub app repo with the regenerated source manifest | Maintainer, by hand (Flathub's AI policy) |
|
||
| **Homebrew** | **Autobump** — BrewTestBot opens the `amethyst-nostr` cask and `amy` formula PRs itself; `scripts/bump-homebrew-cask.sh` only verifies | Homebrew (we verify) |
|
||
| **Winget** | `scripts/bump-winget.sh` opens the `winget-pkgs` PR with your `gh` auth | Maintainer |
|
||
|
||
Maven Central (the `quartz` library) also publishes automatically from the same
|
||
workflow — as a *step* at the end of the `deploy-android` job, not a job of its
|
||
own, so don't expect to find it in the run's job list.
|
||
|
||
---
|
||
|
||
## 1. Pre-tag checklist
|
||
|
||
1. **Bump the version** in `gradle/libs.versions.toml` — both keys:
|
||
```toml
|
||
app = "1.12.1" # semver, drives every module + the tag
|
||
appCode = "449" # Android versionCode, monotonic — must increment
|
||
```
|
||
That single edit propagates to Android (`versionName`/`versionCode`),
|
||
Desktop & CLI (`packageVersion`). Nothing else hardcodes the version.
|
||
(quartz and geode are released from their own repos with their own versions.)
|
||
|
||
2. **Write the changelog** as `docs/changelog/vMAJOR.MINOR.PP.md` (zero-padded,
|
||
e.g. `v1.12.01.md`) and add it to `docs/changelog/README.md`. Follow the
|
||
house style: plain text, short verb-first sentences.
|
||
|
||
For the `## Contributors` section, generate the credits from git instead of
|
||
writing them by hand:
|
||
```bash
|
||
scripts/contributors.sh
|
||
```
|
||
It lists the commit authors and `Co-authored-by` trailers since the last
|
||
release tag, resolved to npubs through `docs/changelog/github.json`
|
||
(`mappings` by GitHub username, `aliases` for other names and emails a
|
||
person commits under, `ignore` for AI co-authors and bots). Anyone it can't
|
||
resolve is listed under `UNMAPPED` with a commit count — credit them by hand,
|
||
then add their npub or an alias so future releases pick them up. Pass
|
||
`--from`/`--to` to rebuild an older release's list.
|
||
|
||
For the `## Translations` section, generate the credits instead of writing
|
||
them by hand — no token needed:
|
||
```bash
|
||
scripts/translators.sh
|
||
```
|
||
This runs offline. It reads `docs/changelog/translators.json` (kept next to
|
||
the changelogs), which CI keeps fresh: the Crowdin sync workflow's
|
||
`seed-translators` job records everyone who has translated since the last `v*`
|
||
tag, with their languages, in the file's `sinceLastTag` list, and accumulates
|
||
their npubs in the forever-growing `mappings` registry. The script resolves
|
||
that list to npubs and prints the `## Translations` block, one line per
|
||
translator with all their languages. Usernames under `ignore` (the account
|
||
that uploads machine translations) are left out, so credit translations that
|
||
arrived through git rather than Crowdin by hand.
|
||
Contributors with no npub yet are listed under `UNMAPPED` — credit them by
|
||
hand, then add their npub under `mappings` so future releases pick them up
|
||
automatically. (To re-query Crowdin live as a sanity check, run
|
||
`scripts/translators.sh --seed` with `CROWDIN_PROJECT_ID` /
|
||
`CROWDIN_PERSONAL_TOKEN` set, which refreshes the file.)
|
||
|
||
3. **Publish the release-notes note on Nostr** — *minor releases only.* In
|
||
practice this id has only ever been bumped on `x.y.0` (1.11.0, 1.12.0,
|
||
1.13.0); patch releases leave it pointing at their minor's note. Publish with
|
||
Amethyst's account and paste the event id into `amethyst/build.gradle.kts`:
|
||
```kotlin
|
||
buildConfigField("String", "RELEASE_NOTES_ID", "\"<new-event-id-hex>\"")
|
||
```
|
||
This id is what the in-app drawer's "Release Notes" link and the donation
|
||
card open (`DrawerContent.kt`, `ShowDonationCard.kt`). It must point at the
|
||
note for *this* version, so publish the note **before** tagging and commit
|
||
the new id together with the version bump.
|
||
|
||
<!-- TODO(maintainer): document the exact command/account used to publish the
|
||
release-notes note (which signer, which relays). -->
|
||
|
||
4. **Add the release to the Linux metainfo**: a new first entry in `<releases>`
|
||
of `desktopApp/packaging/flatpak/com.vitorpamplona.amethyst.Amethyst.metainfo.xml`
|
||
(`<release version="1.12.1" date="YYYY-MM-DD">` with a `details` URL to the
|
||
GitHub release, as the existing entries do). If you forget, release CI
|
||
prints a warning and injects a bare entry, but only into the bundle it
|
||
builds: AppStream readers like Flathub take the history from the source
|
||
tree.
|
||
|
||
5. **Sanity-build locally** (optional but cheap): `./gradlew assembleRelease`
|
||
and a desktop `packageDistributionForCurrentOS`, or run the workflow's
|
||
dry-run (see BUILDING.md § Dry-run).
|
||
|
||
---
|
||
|
||
## 2. Cut the release
|
||
|
||
Commit, tag, push — see [`BUILDING.md` § Release runbook](BUILDING.md#release-runbook)
|
||
for the exact commands. The tag must equal `app` from the catalog (the workflow
|
||
asserts this and fails fast otherwise). A clean `vMAJOR.MINOR.PATCH` tag is
|
||
classified **stable** and runs the Homebrew/Winget bump workflows; anything with
|
||
a `-rc`/`-beta`/`-alpha`/`-dev` suffix is a prerelease and skips them.
|
||
|
||
Heads-up on the `git push`: this repo has `git-credential-manager` configured as
|
||
a credential helper, and it blocks on an interactive prompt (a plain
|
||
`GIT_TERMINAL_PROMPT=0` does **not** stop it — the push just hangs). If that
|
||
happens, push using `gh`'s helper for the one command:
|
||
|
||
```bash
|
||
git -c credential.helper= -c credential.helper='!gh auth git-credential' push upstream main
|
||
```
|
||
|
||
When the `Create Release Assets` workflow finishes (~25–30 min) the GH Release
|
||
holds **37 assets**, per the asset-name contract:
|
||
|
||
- **Android (13):** 5 Google Play APKs + 5 F-Droid APKs + 2 AABs + the F-Droid
|
||
`.apks` set for Accrescent
|
||
(`amethyst-googleplay-*-v…apk` / `.aab`, `amethyst-fdroid-*-v…apk` / `.aab` / `.apks`)
|
||
- **Desktop (14):** macOS DMG (**arm64 only** — there is no Intel DMG), Windows
|
||
MSI (x64 only — **no arm64 MSI**) + portable zip (x64, arm64), and Linux
|
||
DEB/RPM/AppImage/flatpak/tar.gz in both x64 and arm64. BUILDING.md § Release
|
||
runbook has the per-leg breakdown and why the two gaps exist.
|
||
- **CLI (10):** the `amy` artifacts — the no-JRE `jvm.tar.gz`, macOS arm64,
|
||
Windows x64 + arm64, and Linux DEB/RPM/tar.gz in both x64 and arm64
|
||
|
||
The `geode` relay (bundles, `.deb`/`.rpm`, the GHCR image, its Homebrew formula and
|
||
the Maven Central library) releases from
|
||
[vitorpamplona/geode-relay](https://github.com/vitorpamplona/geode-relay). Bumping it
|
||
here means bumping `geodeRelay` in `gradle/libs.versions.toml` to a published version.
|
||
|
||
---
|
||
|
||
## 3. Per-channel shipping
|
||
|
||
### GitHub Releases — automatic
|
||
Nothing to do beyond pushing the tag. Verify the asset count (BUILDING.md
|
||
§ Verify). macOS is **arm64-only** — there is no Intel DMG, so a single
|
||
`amethyst-desktop-<version>-macos-arm64.dmg` is the expected, correct result.
|
||
|
||
Two of those assets are the R8 mapping files
|
||
(`amethyst-{googleplay,fdroid}-mapping-<version>.txt.gz`). Do not prune them
|
||
from old releases — they are the only way to read a crash report from a build
|
||
that old (§ 7).
|
||
|
||
### Google Play — manual upload
|
||
1. Download `amethyst-googleplay-<version>.aab` from the GH Release.
|
||
2. Play Console → app `com.vitorpamplona.amethyst` → **Production** (or the
|
||
staged-rollout track we're using) → create release → upload the AAB.
|
||
3. The release notes field can reuse the `docs/changelog` text.
|
||
4. Roll out.
|
||
|
||
### F-Droid — pull / build-from-source
|
||
F-Droid does **not** accept an upload from us. Its build server polls the repo,
|
||
and when it sees the new `v*` tag it builds the **`fdroid` product flavor** from
|
||
source (reproducibly) per the recipe in the separate
|
||
[`fdroiddata`](https://gitlab.com/fdroid/fdroiddata) repo
|
||
(`metadata/com.vitorpamplona.amethyst.yml`), then signs and publishes to the
|
||
F-Droid repo on its own cadence.
|
||
|
||
What we own to keep that working:
|
||
- The **`fdroid` flavor** (`amethyst/src/fdroid/…`) must stay free of
|
||
proprietary deps — it swaps Firebase/Google services for UnifiedPush and
|
||
no-op/open implementations (ML Kit, writing assistant, push). Google-only
|
||
libraries live behind the `play` flavor.
|
||
- The fastlane metadata under `fastlane/metadata/android/` (descriptions,
|
||
images). F-Droid reads per-version changelogs from
|
||
`fastlane/metadata/android/en-US/changelogs/<versionCode>.txt` if present —
|
||
add one (e.g. `449.txt`) when we want a changelog shown on F-Droid; otherwise
|
||
none is displayed.
|
||
- The `AutoUpdateMode`/`UpdateCheckMode` in the fdroiddata recipe tracks tags,
|
||
so a correct `vX.Y.Z` tag + bumped `versionCode` is usually all F-Droid needs.
|
||
|
||
After a release, just confirm F-Droid picked up the new version (it can lag a
|
||
few days): <https://f-droid.org/packages/com.vitorpamplona.amethyst/>.
|
||
|
||
### Nostr releases — `amy releases publish` with Amethyst's nsec
|
||
One command announces the release on Nostr (NIP-82) for three readers, each under its own app id:
|
||
|
||
- **Zapstore** ([zapstore.dev](https://zapstore.dev/)) installs and updates the APK from events
|
||
under `com.vitorpamplona.amethyst`. `amy` writes them tag for tag as
|
||
[`zsp`](https://github.com/zapstore/zsp) did, reading the same [`zapstore.yaml`](zapstore.yaml),
|
||
so installs Zapstore made keep updating.
|
||
- **Orchard** ([github.com/Letdown2491/orchard](https://github.com/Letdown2491/orchard)), a Flatpak
|
||
store over Nostr, installs the GitHub Release's `.flatpak` bundles from events under
|
||
`com.vitorpamplona.amethyst.Amethyst` (§ Orchard).
|
||
- **The desktop app** updates itself from events under `com.vitorpamplona.amethyst.desktop`.
|
||
Until they are out, a desktop user only hears about the new version from GitHub, with a link to
|
||
the download page; the app never installs a file a signed release does not name
|
||
(`desktopApp/plans/2026-10-15-desktop-self-update-study.md`).
|
||
|
||
The three share the key and the event kinds, so they must never share an app id: one publish would
|
||
replace another's release.
|
||
|
||
**Each release**, after the GH Release assets exist, download them into one folder and run from
|
||
the repo root (for `zapstore.yaml`), logged in to amy with Amethyst's nsec:
|
||
|
||
```bash
|
||
gh release download vX.Y.Z --pattern 'amethyst-desktop-*' --pattern 'amethyst-googleplay-arm64-v8a-*.apk' --dir /tmp/release-vX.Y.Z
|
||
amy releases publish /tmp/release-vX.Y.Z --version X.Y.Z --notes-file notes.md --blossom https://cdn.zapstore.dev --dry-run # check
|
||
amy releases publish /tmp/release-vX.Y.Z --version X.Y.Z --notes-file notes.md --blossom https://cdn.zapstore.dev
|
||
```
|
||
|
||
For each app with files in the folder, it signs one kind 3063 asset per file, then the kind 30063
|
||
release that names them, and publishes to that app's relays. `--listing` also (re)publishes the kind 32267 listing: do it when
|
||
`zapstore.yaml` (Android), `FlatpakRelease` (Flatpak) or `DesktopReleaseEvents` (desktop)
|
||
changes, and the first time for the desktop.
|
||
|
||
- **Android:** by default the googleplay arm64-v8a APK, the one `zsp` picked and the only platform
|
||
Zapstore installs for (`--android-flavor`, `--android-abis` choose others). Each APK is checked
|
||
with apksig (signature, package, version) and tagged with `version_code`, the SDK range, its ABIs,
|
||
`apk_certificate_hash`, the `variant` and the `supported_nips` from `zapstore.yaml`.
|
||
`--blossom https://cdn.zapstore.dev` mirrors the APK there as a second URL, and for `--listing`
|
||
uploads the icon and screenshots, as `zsp` did. Relays: `relay.zapstore.dev` (the one the
|
||
Zapstore app reads), `nos.lol`, `nostr.mom` and `vitor.nostr1.com`.
|
||
- **Flatpak:** both `amethyst-desktop-X.Y.Z-linux-*.flatpak` bundles, each checked to be this app
|
||
for its arch by the ref in its header, with its GitHub URL, SHA-256, size, arch and
|
||
`application/vnd.flatpak`, in one release. Relays: `FlatpakRelease.RELAYS` (`relay.orchard.rocks`
|
||
is the one Orchard reads).
|
||
- **Desktop:** every `amethyst-desktop-X.Y.Z-*` file the app installs (DMG, MSI, both zips, both
|
||
AppImages, both tarballs, the .debs and .rpms; not the Flatpak bundles or `.zsync` files), with
|
||
its GitHub URL, SHA-256, size, platform and MIME type. Relays: `DesktopRelease.RELAYS`.
|
||
|
||
It refuses any key but Amethyst's. A prerelease version (anything but `X.Y.Z`, as the release
|
||
workflow classifies tags) goes out on the `beta` channel, which none of the readers installs from.
|
||
`--only android`, `--only flatpak` or `--only desktop` publishes one app.
|
||
|
||
`zsp publish` still works for the APK if `amy` is unavailable: it reads the same `zapstore.yaml`,
|
||
with the relays in the `RELAY_URLS` env var.
|
||
|
||
### Orchard
|
||
Orchard (0.2.0 and later) downloads the bundle from the asset's URL, checks it against the signed
|
||
SHA-256 and installs it with `flatpak install --bundle`; an update installs the new bundle over the
|
||
old one. There is nothing to build or host beyond the GitHub Release. Orchard 0.1.x only installed
|
||
OCI images, which `orchard-publish` pushed to `ghcr.io/vitorpamplona/amethyst-flatpak` for 1.19.0;
|
||
the 1.19.0 events still point there, so leave that package until a later release is out, then it
|
||
can be deleted.
|
||
|
||
The bundle is built on the Flatpak branch `master`; the 1.19.0 image was `stable`. Someone who
|
||
installed 1.19.0 from Orchard gets the bundle as a second branch beside the image's on the next
|
||
update; `flatpak uninstall com.vitorpamplona.amethyst.Amethyst//stable` removes the old one.
|
||
|
||
**One-time setup.** `orchard.txt` at the repo root names Amethyst's npub. Orchard reads it from the
|
||
default branch to show the repository link as verified. Update it if the signing key ever changes.
|
||
|
||
### Flathub — not submitted yet
|
||
|
||
Amethyst is not on Flathub. The source-build manifest that a submission would
|
||
use lives in
|
||
[`desktopApp/packaging/flatpak/flathub/`](desktopApp/packaging/flatpak/flathub/),
|
||
and [its README section](desktopApp/packaging/flatpak/README.md#flathub) lists
|
||
what is still missing (AI disclosure, domain verification, a tag whose metainfo
|
||
has the screenshot). Until then there is nothing to ship here; the steps below
|
||
are for once Flathub has created `flathub/com.vitorpamplona.amethyst.Amethyst`.
|
||
|
||
**Flathub's AI policy.** These files were written with Claude Code. Flathub
|
||
forbids AI tools from opening or automating its PRs and from writing their
|
||
descriptions or review replies, so a person opens each update PR there and
|
||
answers the review. An agent may prepare the files in this repo.
|
||
|
||
**Each release**, after the tag is pushed:
|
||
|
||
1. Regenerate the Maven list from a real online build of the tag, both arches
|
||
in Docker (about 35 minutes; work files go to `build/flathub-regen/`):
|
||
```bash
|
||
desktopApp/packaging/flatpak/flathub/tools/regenerate-gradle-sources.sh vX.Y.Z
|
||
```
|
||
It rewrites `gradle-sources.json` and prints the tag tarball's sha256.
|
||
2. In `flathub/com.vitorpamplona.amethyst.Amethyst.yml`, point the `archive`
|
||
source at the tag's URL and that sha256. When the JetBrains Runtime moves
|
||
(`JBR_BUILD` in the regenerate script and the two `jbrsdk` sources), bump
|
||
both and their sha256s together.
|
||
3. Build and lint it locally (below), commit the three files here, then copy
|
||
the manifest, `flathub-offline.init.gradle` and `gradle-sources.json` into a
|
||
PR to the Flathub app repo. Flathub's update bot can bump a URL, but not
|
||
regenerate the Maven list, so every release is this manual PR.
|
||
|
||
**Testing locally.** `flatpak-builder` needs Linux; on macOS run it in a
|
||
privileged Debian container (`flatpak flatpak-builder elfutils` installed, the
|
||
freedesktop 26.08 Platform and Sdk from Flathub). From the `flathub/` directory:
|
||
|
||
```bash
|
||
flatpak-builder --force-clean --repo=repo --mirror-screenshots-url=https://dl.flathub.org/media/ \
|
||
builddir com.vitorpamplona.amethyst.Amethyst.yml # add --disable-rofiles-fuse in Docker
|
||
ostree commit --repo=repo --canonical-permissions \
|
||
--branch=screenshots/$(flatpak --default-arch) builddir/files/share/app-info/media
|
||
flatpak run --command=flatpak-builder-lint org.flatpak.Builder manifest com.vitorpamplona.amethyst.Amethyst.yml
|
||
flatpak run --command=flatpak-builder-lint org.flatpak.Builder repo repo
|
||
```
|
||
|
||
The build step has no network, so a Maven file missing from
|
||
`gradle-sources.json` fails Gradle's resolution there. Expected noise: built
|
||
with Debian's `flatpak-builder`/AppStream, the repo lint reports
|
||
`appstream-external-screenshot-url` and `appstream-remote-icon-not-mirrored`,
|
||
because that AppStream writes media paths relative to `media_baseurl`, while
|
||
the linter wants the full `dl.flathub.org/media` URLs Flathub's own builder
|
||
writes. Anything else it reports is real. Then install it and run it:
|
||
|
||
```bash
|
||
flatpak remote-add --user --no-gpg-verify amethyst-local repo
|
||
flatpak install --user amethyst-local com.vitorpamplona.amethyst.Amethyst
|
||
flatpak run com.vitorpamplona.amethyst.Amethyst
|
||
```
|
||
|
||
The app should open, and `lib/Amethyst/lib/runtime/lib/libawt_wlawt.so` should
|
||
be in the installed tree (JBR's Wayland toolkit). `builddir/`, `repo/` and
|
||
`.flatpak-builder/` are gitignored.
|
||
|
||
### Homebrew + Winget
|
||
|
||
The in-repo sync workflows (`bump-homebrew.yml`, `bump-homebrew-formula.yml`,
|
||
`bump-winget.yml`) keep the reference
|
||
packaging files current. Upstream:
|
||
|
||
| Channel | Upstream package | State |
|
||
|---|---|---|
|
||
| **Homebrew cask** | `Homebrew/homebrew-cask` → `amethyst-nostr` | **Live, autobumped** — BrewTestBot opens the version PR (v1.17.0: [#292108](https://github.com/Homebrew/homebrew-cask/pull/292108)) |
|
||
| **Homebrew formula** | `Homebrew/homebrew-core` → `amy` | **Live, autobumped** — BrewTestBot opens and merges the version PR (v1.17.0: [#315679](https://github.com/Homebrew/homebrew-core/pull/315679)) |
|
||
| **Winget** | `microsoft/winget-pkgs` → `VitorPamplona.Amethyst` | **Live** — initial [PR #422752](https://github.com/microsoft/winget-pkgs/pull/422752) merged 2026-09-23; bumped per release by `scripts/bump-winget.sh` |
|
||
|
||
**Homebrew is autobumped.** Both packages are on Homebrew's autobump list
|
||
(`autobump: true` in `https://formulae.brew.sh/api/cask/amethyst-nostr.json` and
|
||
`…/api/formula/amy.json`), so BrewTestBot opens each version-bump PR itself,
|
||
roughly every 3 hours after the release, and `brew bump-cask-pr` **refuses** to
|
||
open a competing one. There is nothing to push. What BrewTestBot does not do is
|
||
check our DMG: it computes its own sha256 and never looks at notarization. So
|
||
`scripts/bump-homebrew-cask.sh` now verifies instead of submitting — it
|
||
re-checks the live DMG's sha256 and stapled notarization ticket, finds
|
||
BrewTestBot's PR, and fails if the sha256 in that PR differs from ours. It
|
||
needs no token. Only if Homebrew ever drops the cask from autobump does it fall
|
||
back to `brew bump-cask-pr`, which needs `HOMEBREW_GITHUB_API_TOKEN` (classic
|
||
PAT, `repo` scope) in your shell.
|
||
|
||
The state above is a snapshot; re-check before assuming. Two calls answer it:
|
||
`gh api repos/microsoft/winget-pkgs/contents/manifests/v/VitorPamplona` and
|
||
`curl -s -o /dev/null -w '%{http_code}' https://formulae.brew.sh/api/cask/amethyst-nostr.json`
|
||
(404 = still absent).
|
||
|
||
Two separate faults kept this invisible until v1.13.1, both now fixed:
|
||
|
||
1. **The workflows never ran at all** — for *any* release. They triggered on
|
||
`release: types: [released]`, and GitHub does not raise workflow-triggering
|
||
events for a release created by `GITHUB_TOKEN`, which is exactly how
|
||
`create-release.yml` creates it. They now trigger on `workflow_run` after
|
||
`Create Release Assets` succeeds, which also fixes a latent race — the old
|
||
event fired while assets were still uploading.
|
||
2. **Nothing exists upstream to bump.** `brew bump-cask-pr` and
|
||
`winget-releaser` can only *update* an existing package. The first
|
||
submission is a manual, human-reviewed PR: BUILDING.md § Homebrew cask
|
||
(one-time initial PR) and § Winget (one-time initial submission).
|
||
|
||
CI does the bookkeeping with `GITHUB_TOKEN` only — verifying the artifact,
|
||
computing hashes, and opening an in-repo PR syncing the reference packaging
|
||
files. After merging those sync PRs, a maintainer runs:
|
||
|
||
```bash
|
||
git pull # the scripts read the merged files
|
||
scripts/bump-homebrew-cask.sh v1.19.0 # verify only; BrewTestBot opens the PR
|
||
scripts/bump-winget.sh v1.19.0 # opens the winget-pkgs PR; uses your `gh` auth
|
||
```
|
||
|
||
Both scripts re-verify the published artifact's sha256 first, and the Homebrew
|
||
one also refuses a DMG without a stapled notarization ticket. Run the Homebrew
|
||
one again later if BrewTestBot has not opened its PR yet. See BUILDING.md
|
||
§ Package-manager credentials.
|
||
|
||
All three in-repo sync workflows (`amy` formula, `amethyst-nostr` cask,
|
||
winget manifests) open PRs against *this* repo on every
|
||
release. Merge them to keep the reference packaging files current.
|
||
|
||
---
|
||
|
||
## 4. Operated infrastructure
|
||
|
||
### Push notification server
|
||
|
||
The Google Play (FCM) flavor delivers push through a server we operate at
|
||
`push.amethyst.social`, built from
|
||
[`vitorpamplona/amethyst-push-notif-server`](https://github.com/vitorpamplona/amethyst-push-notif-server).
|
||
It registers devices, watches their NIP-65 inbox / NIP-17 DM relays, and sends
|
||
wake-up pushes.
|
||
|
||
- **`play` flavor** → push via this server (Firebase/FCM).
|
||
- **`fdroid` flavor** → UnifiedPush through a distributor app the user installs
|
||
(e.g. ntfy); it does **not** use our server.
|
||
- Both are complemented by the on-device always-on `NotificationRelayService`
|
||
(see [`PULL_NOTIFICATION.md`](PULL_NOTIFICATION.md)), which keeps the user's
|
||
relay connections alive without any push server at all.
|
||
|
||
The push server has its **own repo, deploy, and release cadence** — a normal app
|
||
release does **not** redeploy it. Coordinate a server deploy only when the app
|
||
changes the registration/push contract (token format, payload, or endpoint), so
|
||
the running server stays compatible with the shipped app.
|
||
|
||
<!-- TODO(maintainer): document the push-server deploy steps + hosting, and
|
||
which app-side changes require a coordinated push-server deploy. -->
|
||
|
||
---
|
||
|
||
## 5. Secrets ownership & rotation
|
||
|
||
The workflow's required secrets and what they sign are inventoried generically
|
||
in [`BUILDING.md` § Secrets](BUILDING.md#secrets-the-ci-needs). Amethyst-specific
|
||
ownership:
|
||
|
||
| Secret(s) | Protects | Rotation |
|
||
|---|---|---|
|
||
| `SIGNING_KEY`, `KEY_ALIAS`, `KEY_STORE_PASSWORD`, `KEY_PASSWORD` | The **Android upload keystore** — losing/leaking it is the worst case; Play app signing identity | Keep the keystore backed up offline; never rotate casually (Play upload key reset is a support process) |
|
||
| `SONATYPE_USERNAME`, `SONATYPE_PASSWORD` | Maven Central namespace `com.vitorpamplona` | On compromise |
|
||
| `SIGNING_PRIVATE_KEY`, `SIGNING_PASSWORD` | The **GPG key** signing Maven artifacts | Per GPG key expiry |
|
||
| *(none for Homebrew/Winget)* | — | Homebrew is autobumped by BrewTestBot, and `scripts/bump-winget.sh` runs on a maintainer's machine with `gh` auth, so neither channel needs a CI secret. See BUILDING.md § Package-manager credentials |
|
||
| `CROWDIN_PERSONAL_TOKEN`, `CROWDIN_PROJECT_ID` | Translation sync | On compromise |
|
||
|
||
Owner assignments and rotation reminders live with the team (issue tracker).
|
||
|
||
<!-- TODO(maintainer): name the owner per secret and where backups live. -->
|
||
|
||
---
|
||
|
||
## 6. Post-release verification
|
||
|
||
- [ ] GH Release: 49 assets, sizes sane, and the asset-name set matches the
|
||
previous release (see the `diff` one-liner in BUILDING.md § Release
|
||
runbook). macOS is arm64-only — do **not** look for an Intel DMG.
|
||
- [ ] Maven Central: `quartz:<version>` resolves (allow tens of minutes of
|
||
propagation; the publish step's log is the authoritative signal).
|
||
- [ ] Play Console: rollout started, no policy rejection.
|
||
- [ ] Zapstore: release event visible.
|
||
- [ ] Orchard: the Amethyst row shows the new version, and its repository link
|
||
wears the check (the `orchard.txt` proof).
|
||
- [ ] F-Droid: new version detected (may lag days).
|
||
- [ ] Flathub (only once Amethyst is on Flathub): the app-repo PR with the
|
||
regenerated `gradle-sources.json` is merged and Flathub's build is green.
|
||
- [ ] Three sync PRs opened against this repo (`amy` formula, `amethyst-nostr`
|
||
cask, winget manifests) — merge them.
|
||
- [ ] Homebrew: `scripts/bump-homebrew-cask.sh vX.Y.Z` passes — DMG verified and
|
||
BrewTestBot's cask PR carries the same sha256 (no token; re-run later if
|
||
the bot has not opened it yet). `amy` needs nothing: BrewTestBot bumps it.
|
||
- [ ] Winget pushed upstream: `scripts/bump-winget.sh vX.Y.Z` (manual, no token —
|
||
uses your `gh` auth).
|
||
- [ ] In-app "Release Notes" link opens the note matching `RELEASE_NOTES_ID`
|
||
(only bumped on minor releases — patches keep pointing at the x.y.0 note).
|
||
- [ ] Push still works on a `play` build (only if the push contract changed —
|
||
see § 4); UnifiedPush still works on an `fdroid` build.
|
||
|
||
If anything ships broken, see [`BUILDING.md` § Incident response](BUILDING.md#incident-response).
|
||
|
||
---
|
||
|
||
## 7. Crash reports & retrace
|
||
|
||
Release builds are minified **and obfuscated** (they have to be: Play Console
|
||
drops apps whose DEX is under 25% optimized or obfuscated out of store surfaces
|
||
— see `amethyst/proguard-rules.pro` for the whole story). So a raw stack trace
|
||
from a release build looks like this:
|
||
|
||
```
|
||
java.lang.IllegalStateException: something blew up
|
||
at onh.B(r8-map-id-12c710927a584543dbe1e2e867db95460bc44482efe53283f86798648c1cfc00:7)
|
||
```
|
||
|
||
That is not lost information, it is encoded information. Paste the report in and
|
||
run it:
|
||
|
||
```bash
|
||
scripts/retrace.sh crash-report.txt
|
||
pbpaste | scripts/retrace.sh # or straight off the clipboard
|
||
```
|
||
|
||
Nothing else to supply. A report's first line names its own build —
|
||
`java.lang.IllegalStateException: 1.16.0-PLAY` — so the script resolves the tag
|
||
(`v1.16.0`) and the flavor (`PLAY` → `googleplay`), downloads that release's
|
||
mapping asset and caches it. For a bare stack trace with no such header, name
|
||
the build yourself with `--release v1.16.0 --flavor play`; for a build you made
|
||
locally, pass its `mapping.txt` directly.
|
||
|
||
```
|
||
java.lang.IllegalStateException: something blew up
|
||
at androidx.compose.foundation.text.input.TextFieldCharSequence.getText(TextFieldCharSequence.kt:58)
|
||
at androidx.compose.foundation.text.input.TextFieldState.getText(TextFieldState.kt:146)
|
||
at com.vitorpamplona.amethyst.ui.screen.loggedIn.home.ShortNotePostViewModel.onMessageChanged(ShortNotePostViewModel.kt:1727)
|
||
```
|
||
|
||
Note that retrace gave back **three** frames where the crash reported one: R8
|
||
had inlined two of them. That is worth internalising — it is the reason a raw
|
||
trace's line number cannot be trusted even in the pre-obfuscation builds, where
|
||
optimization was already inlining. Retracing is not a tax obfuscation imposed;
|
||
it is how you read an optimized build at all.
|
||
|
||
**The wrong mapping is worse than none** — it produces confident, wrong names.
|
||
So the script refuses to guess: the `r8-map-id-<hash>` in the trace *is* the
|
||
`pg_map_id` header of the mapping that built it, and the two are compared before
|
||
anything is printed:
|
||
|
||
```
|
||
error: this mapping did not build this report.
|
||
report: deadbeef...
|
||
mapping: 12c71092... (amethyst-googleplay-mapping-v1.16.0.txt.gz)
|
||
```
|
||
|
||
`--force` overrides if you really mean it. (`googleplay` vs `fdroid` matters —
|
||
the two flavors are separate R8 runs with different mappings.)
|
||
|
||
**Per channel:**
|
||
|
||
| Where the report came from | What to do |
|
||
|---|---|
|
||
| Play Console / Android vitals | Nothing. AGP embeds the mapping in the `.aab` (`BUNDLE-METADATA/com.android.tools.build.obfuscation/proguard.map`), so Play deobfuscates automatically. |
|
||
| A NIP-17 DM from the in-app crash reporter, a GitHub issue, F-Droid, Zapstore, Accrescent | `scripts/retrace.sh <report>` — it reads the build off line 1 and fetches the mapping. |
|
||
| A build you made locally | `scripts/retrace.sh amethyst/build/outputs/mapping/<variant>/mapping.txt report.txt` |
|
||
|
||
`scripts/retrace.sh` downloads the R8 version named in the mapping's own header
|
||
from Google's Maven and caches it, so it needs no pinned tooling and keeps
|
||
working across AGP bumps. It needs no `gh` auth either — release assets on a
|
||
public repo are plain HTTPS downloads.
|
||
|
||
Two details of the report format in `ReportAssembler` exist for this and should
|
||
not be "tidied" away: the headline carries the **fully qualified** exception
|
||
class (a bare `simpleName` obfuscates to `a`, which retrace cannot resolve
|
||
because it has no package), and stack frames are written as ` at <frame>`
|
||
(retrace only rewrites frames it recognises, and it recognises them by the
|
||
leading `at`). The script repairs the missing `at` on reports from older builds,
|
||
but new reports should not need repairing.
|
||
|
||
**Do not delete mapping assets from old releases.** They are the only copy —
|
||
CI's are gone when the job ends, and a mapping cannot be regenerated after the
|
||
fact (it would need a bit-identical rebuild, and R8's renaming is not stable
|
||
across runs).
|
||
|
||
### Did obfuscation break anything?
|
||
|
||
R8 cannot see reflection, so nothing in the build tells you that a keep rule
|
||
stopped matching. `tools/r8-verify/reflection-contract.txt` lists every place
|
||
something outside the DEX resolves a name at runtime — JNI symbols, Jackson DTO
|
||
field names, enum constants persisted in DataStore, WorkManager's stored worker
|
||
class names, the Cast `OptionsProvider` named in a manifest `<meta-data>` value
|
||
— and the release workflow asserts each one against what R8 actually emitted,
|
||
for both flavors, before any asset is collected:
|
||
|
||
```bash
|
||
python3 tools/r8-verify/verify_reflection_contract.py \
|
||
amethyst/build/outputs/mapping/playRelease/
|
||
```
|
||
|
||
Run it on any local minified build too. It takes under a second and needs no
|
||
device.
|
||
|
||
**Adding reflection means adding two things**: the keep rule, and a line in the
|
||
contract. A rule with no contract line is unverified and will rot silently.
|
||
|
||
What this does *not* cover is reflection nobody wrote down. For that the honest
|
||
controls are a staged Play rollout (the crash reporter retraces itself now, so
|
||
a break is legible within hours) and exercising NIP-47 wallet connect, NIP-46
|
||
bunker login, Tor, scheduled posts and a settings round-trip on a minified
|
||
build before shipping.
|