M5: Local release task — GHCR image, signed APK, GitHub Release (ENG-272) #35

Merged demi.dev
targeting main from stoopkidddd/eng-272-m5-local-release-task-ghcr-image-signed-apk-github-release
Merged opened by and assigned to demi.dev
targeting main from stoopkidddd/eng-272-m5-local-release-task-ghcr-image-signed-apk-github-release

Linear: https://linear.app/engineereddev/issue/ENG-272

What's in this PR (6 files, plus a lead commit):

  • scripts/release.ts as mise run release <version> [--dry-run]: preflight (on main, clean, main = origin/main after fetch, semver newer than the last v* tag, mise run check, a printed reminder to confirm Tangled CI); multi-arch docker buildx (linux/amd64,linux/arm64, --build-arg VERSION, :latest except for -rc); signed assembleRelease with -PreleaseVersionName/-PreleaseVersionCode (§16.8: major·10000 + minor·100 + patch); apksigner verify --print-certs must match the fingerprint in docs/RELEASING.md before dist/release/pneumo-<v>.apk + SHA256SUMS are written; tag, push tag to Tangled, push main + tag to the mirror, gh release create (--prerelease for -rc). Every login/push sits behind one dry-run boundary.
  • Secrets: the GHCR token only flows gh auth token → docker login --password-stdin; keystore passwords come from ~/.config/pneumo/release.env (mode 600, validated) into the Gradle child's environment only; Gradle output is captured; errors print only messages.
  • apps/android/app/build.gradle.kts: a release signingConfig created only when all four ANDROID_* env vars are present (otherwise unsigned); version name/code from Gradle properties (dev defaults unchanged).
  • mise.toml: release task; e2e:docker accepts IMAGE= (scripts/e2e/docker.ts). scripts/ci/check.sh type-checks release.ts.
  • docs/RELEASING.md: prerequisites, the one-time signing setup, cutting a release, test-only overrides, the backup warning.
  • Lead commit 233b1a3: the real keystore was created on the owner's Mac with the documented script (passwords never printed); its public certificate SHA-256 0E:FF:…:03:E5 is recorded in docs/RELEASING.md.

Verify:

  • Implementer: check:infra, check:android, e2e:docker and IMAGE=pneumo:eng-267 e2e:docker exit 0; release 0.1.0-rc.1 --dry-run with a throwaway key in an isolated clean main checkout: exit 0, both arches built, APK signed and matched, SHA256SUMS written, 0 publication calls, 0 password leaks; wrong fingerprint → exit 1 with nothing written; unsigned assembleRelease stays unsigned with dev versions; all preflight refusals tested.
  • Lead (on 1e4f66d): check:infra exit 0, check:android exit 0.
  • After merge the lead runs the real mise run release 0.1.0-rc.1 (needs the owner's gh auth refresh -s write:packages).

🤖 Generated with Claude Code

Commits 0