Releasing¶
Immortal is hosted from its own GitHub repo. Two files drive what devices see.
Cutting a release (the one safe command)¶
scripts/cut-release.sh <versionName> "<release notes>"
# e.g.
scripts/cut-release.sh 1.44 "Adds the home-header remote button and a tidier remote layout."
That single command does the whole release and fails fast at every gate, so the two
things that historically broke releases — forgetting the version.json bump, and misnaming
the APK — can't happen:
- Preflight —
ghauthenticated; a clean tree on an up-to-datemain; the signing key present; Android build-tools found; the tag still free. - Bumps
versionCode(auto, current + 1) /versionName/notesin bothapp/build.gradle.ktsandversion.jsontogether, then re-checks they agree. - Builds the signed APK and verifies it —
aaptconfirms the APK's version matches what was just written (no stale build);apksignerconfirms it's signed with the same key as the currently-publishedimmortal.apk(a different key silently breaks self-update). - Builds
portal-kit.zipfrom the committedprovisioning/tree (git archive— only tracked files, never local junk or secrets). - Commits, tags, pushes; creates the Release as a draft, uploads exactly **
immortal.apk portal-kit.zip**, then publishes (drafts aren't "latest", solatest/downloadkeeps resolving to the previous release until the new assets are in place).- Verifies both
latest/download/URLs resolve and the publishedimmortal.apkreports theversionCodeshipped.
Requires gh (write access), the Android SDK build-tools, and keystore.properties (see
Signing). A release attaches exactly two assets: immortal.apk and
portal-kit.zip — nothing else, no versioned APK copies.
Drift is blocked in CI too
release-guard.yml
runs scripts/check-version-sync.sh on every change to version.json or
app/build.gradle.kts: if their versionCode/versionName disagree (or apkUrl isn't
the stable URL, or notes is empty), the check fails before it can reach a release.
version.json — the self-update manifest¶
Immortal polls
version.json; when it
advertises a higher versionCode, the device downloads and installs the new build over itself
(UpdateManager). No cable, no laptop.
It advertises the build by versionCode, points devices at the stable
releases/latest/download/immortal.apk, and carries the release notes. Don't hand-edit it to
cut a release — cut-release.sh writes it (in lockstep
with gradle) and the release guard keeps it honest.
The release asset must be named immortal.apk
The manifest's apkUrl (and the store catalog) point at the stable
releases/latest/download/immortal.apk. If a release attaches only a versioned name, that
URL 404s and breaks self-update for every device — which is exactly what happened once
(a release shipped only immortal-1.42.apk). cut-release.sh always uploads the asset as
immortal.apk and then verifies the URL resolves, so this can't recur.
catalog.json — the app-store catalog¶
The App Store reads
catalog.json. Edit and
commit; clients pick it up on next open (a bundled copy ships as the offline fallback). Every
PR that touches it is validated by CI.
Signing¶
Release builds must be signed with the same key every time — in-place self-update is signature-checked, so a different key means devices can no longer update.
Signing is configured via keystore.properties, which the build looks for first at the repo
root (git-ignored) and then at ~/.immortal-signing/keystore.properties — the recommended home,
since nothing in a git working tree can be considered safe from cleanup.
Back up the signing key
Keep the key backed up safely (e.g. iCloud). Losing it means devices can no longer self-update. CI guards against publishing an unsigned release, but it can't recover a lost key.