ci(release): add tag-triggered signed-release and store-publish workflow #105

Merged
JMR-dev merged 3 commits from feat-release-workflow into main 2026-07-02 04:15:52 +00:00
JMR-dev commented 2026-07-02 03:02:05 +00:00 (Migrated from github.com)

Part of #19

Rewrites .github/workflows/release.yml (previously: manual-dispatch debug-key APK + GitHub release only) into the tag-driven publication pipeline from the issue, plus docs/release.md. Store accounts/keys (#16/#17/#18) do not exist yet, so every credentialed stage is gated on its CI secrets and skips with a ::notice:: explaining what to configure — the pipeline is fully exercisable today and lights up stage-by-stage as secrets are added. ci.yml is untouched.

Issue scope → what's here

  • Release workflow: signed AAB/APK on tag, keys only in CI secrets — triggers on v* tags and workflow_dispatch (with dry_run, optional tag for re-releases, Play track/rollout inputs). The build job decodes RELEASE_KEYSTORE_BASE64 into the runner temp dir and writes the git-ignored secrets.properties that app/build.gradle.kts already reads — no Gradle change needed, local builds unaffected. Without signing secrets it falls back to *-unsigned artifacts (debug-key placeholder) so the workflow still runs.
  • Google Play — r0adkll/upload-google-play@v1.1.5 (SHA-pinned like all actions here), AAB + R8 mapping.txt + generated whatsnew-en-US; default track internal; staged rollout on production via play_rollout_fraction (inProgress status). Gated on PLAY_SERVICE_ACCOUNT_JSON + signing.
  • Galaxy Store — documented stub job: Samsung has no maintained action and its Content Publish API is mid-breaking-change (binaryList removal, July 2026), so wiring it now would be untestable dead code; the job prints the manual Seller-Portal path and the GALAXY_* secret names are reserved. Rationale + future wiring plan in docs/release.md#galaxy-store.
  • F-Droid path — no push step by design: F-Droid builds from the pushed tag + fastlane metadata (#18). Documented.
  • S3 archive — AWS CLI with --endpoint-url (any S3-compatible store), versioned keys releases/<tag>/… with SHA256SUMS.txt alongside; bucket-versioning guidance in docs. Gated on ARCHIVE_S3_*.
  • Release notes / changelog — generated from Conventional-Commit history since the previous tag (grouped: breaking/feat/fix/perf/maintenance/other + compare link), used as the GitHub-release body and truncated into the Play what's-new. SHA-256 checksums for all artifacts; source archives attached (GPL §6 convenience).
  • Existing CI gate as prerequisite — fast-gate job mirrors ci.yml's fast jobs (assembleDebug, testDebugUnitTest, ktlint/detekt) plus lintDebug; the E2E emulator matrix is deliberately not duplicated (it gated every PR that reached the tag). An aggregating release-summary job mirrors ci-passed and writes a per-stage table to the run summary.

Secrets (all optional; each absence just skips its stage with a clear notice)

Secret Activates
RELEASE_KEYSTORE_BASE64, RELEASE_KEYSTORE_PASSWORD, RELEASE_KEY_ALIAS, RELEASE_KEY_PASSWORD Release signing (otherwise *-unsigned artifacts, pre-release-flagged)
PLAY_SERVICE_ACCOUNT_JSON Google Play publish (also needs signing)
GALAXY_SERVICE_ACCOUNT_ID, GALAXY_PRIVATE_KEY, GALAXY_CONTENT_ID Reserved for Galaxy Store automation (job is a documented manual-path stub for now)
ARCHIVE_S3_BUCKET, ARCHIVE_S3_ACCESS_KEY_ID, ARCHIVE_S3_SECRET_ACCESS_KEY (+ optional ARCHIVE_S3_ENDPOINT, ARCHIVE_S3_REGION) S3 archive

Setup instructions for each: docs/release.md.

Validated without secrets

  • actionlint (v1.7.12, with shellcheck integration): clean.
  • ./gradlew :app:bundleRelease / :app:assembleRelease with no secrets.properties: unsigned AAB/APK produced.
  • Full signing path end-to-end with a throwaway keystore: ran the workflow's exact base64→keystore→secrets.properties script locally, assembleRelease produced an APK whose v2 signature verifies as the test cert (apksigner verify --print-certs). Confirms no app/build.gradle.kts change is required.
  • Changelog, artifact-staging/checksum, publish-gating, and Play-rollout scripts each executed against real repo history/outputs (tag build, -rc prerelease, unsigned, dry-run rehearsal, re-release-at-tag, rollout-fraction validation cases).
  • Fast CI gate + :app:compileDebugAndroidTestKotlin pass locally.

Needs real accounts/secrets to verify (blocked on #16/#17/#18): an actual Play upload, S3 upload, and a real tag run creating a GitHub release. Note for then: bump versionCode/versionName before tagging (the workflow warns on tag/versionName mismatch — see docs/release.md#cutting-a-release).

🤖 Generated with Claude Code

Part of #19 Rewrites `.github/workflows/release.yml` (previously: manual-dispatch debug-key APK + GitHub release only) into the tag-driven publication pipeline from the issue, plus `docs/release.md`. Store accounts/keys (#16/#17/#18) do not exist yet, so **every credentialed stage is gated on its CI secrets and skips with a `::notice::` explaining what to configure** — the pipeline is fully exercisable today and lights up stage-by-stage as secrets are added. `ci.yml` is untouched. ## Issue scope → what's here - [x] **Release workflow: signed AAB/APK on tag, keys only in CI secrets** — triggers on `v*` tags and `workflow_dispatch` (with `dry_run`, optional `tag` for re-releases, Play track/rollout inputs). The build job decodes `RELEASE_KEYSTORE_BASE64` into the runner temp dir and writes the git-ignored `secrets.properties` that `app/build.gradle.kts` *already* reads — no Gradle change needed, local builds unaffected. Without signing secrets it falls back to `*-unsigned` artifacts (debug-key placeholder) so the workflow still runs. - [x] **Google Play** — `r0adkll/upload-google-play@v1.1.5` (SHA-pinned like all actions here), AAB + R8 `mapping.txt` + generated `whatsnew-en-US`; default track `internal`; staged rollout on `production` via `play_rollout_fraction` (`inProgress` status). Gated on `PLAY_SERVICE_ACCOUNT_JSON` + signing. - [x] **Galaxy Store** — documented stub job: Samsung has no maintained action and its Content Publish API is mid-breaking-change (`binaryList` removal, July 2026), so wiring it now would be untestable dead code; the job prints the manual Seller-Portal path and the `GALAXY_*` secret names are reserved. Rationale + future wiring plan in `docs/release.md#galaxy-store`. - [x] **F-Droid path** — no push step by design: F-Droid builds from the pushed tag + fastlane metadata (#18). Documented. - [x] **S3 archive** — AWS CLI with `--endpoint-url` (any S3-compatible store), versioned keys `releases/<tag>/…` with `SHA256SUMS.txt` alongside; bucket-versioning guidance in docs. Gated on `ARCHIVE_S3_*`. - [x] **Release notes / changelog** — generated from Conventional-Commit history since the previous tag (grouped: breaking/feat/fix/perf/maintenance/other + compare link), used as the GitHub-release body and truncated into the Play what's-new. SHA-256 checksums for all artifacts; source archives attached (GPL §6 convenience). - [x] **Existing CI gate as prerequisite** — `fast-gate` job mirrors ci.yml's fast jobs (assembleDebug, testDebugUnitTest, ktlint/detekt) plus `lintDebug`; the E2E emulator matrix is deliberately not duplicated (it gated every PR that reached the tag). An aggregating `release-summary` job mirrors `ci-passed` and writes a per-stage table to the run summary. ## Secrets (all optional; each absence just skips its stage with a clear notice) | Secret | Activates | | --- | --- | | `RELEASE_KEYSTORE_BASE64`, `RELEASE_KEYSTORE_PASSWORD`, `RELEASE_KEY_ALIAS`, `RELEASE_KEY_PASSWORD` | Release signing (otherwise `*-unsigned` artifacts, pre-release-flagged) | | `PLAY_SERVICE_ACCOUNT_JSON` | Google Play publish (also needs signing) | | `GALAXY_SERVICE_ACCOUNT_ID`, `GALAXY_PRIVATE_KEY`, `GALAXY_CONTENT_ID` | Reserved for Galaxy Store automation (job is a documented manual-path stub for now) | | `ARCHIVE_S3_BUCKET`, `ARCHIVE_S3_ACCESS_KEY_ID`, `ARCHIVE_S3_SECRET_ACCESS_KEY` (+ optional `ARCHIVE_S3_ENDPOINT`, `ARCHIVE_S3_REGION`) | S3 archive | Setup instructions for each: `docs/release.md`. ## Validated without secrets - `actionlint` (v1.7.12, with shellcheck integration): clean. - `./gradlew :app:bundleRelease` / `:app:assembleRelease` with no `secrets.properties`: unsigned AAB/APK produced. - **Full signing path end-to-end with a throwaway keystore**: ran the workflow's exact base64→keystore→`secrets.properties` script locally, `assembleRelease` produced an APK whose v2 signature verifies as the test cert (`apksigner verify --print-certs`). Confirms no `app/build.gradle.kts` change is required. - Changelog, artifact-staging/checksum, publish-gating, and Play-rollout scripts each executed against real repo history/outputs (tag build, `-rc` prerelease, unsigned, dry-run rehearsal, re-release-at-tag, rollout-fraction validation cases). - Fast CI gate + `:app:compileDebugAndroidTestKotlin` pass locally. **Needs real accounts/secrets to verify** (blocked on #16/#17/#18): an actual Play upload, S3 upload, and a real tag run creating a GitHub release. Note for then: bump `versionCode`/`versionName` before tagging (the workflow warns on tag/versionName mismatch — see `docs/release.md#cutting-a-release`). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.