Skip to content

ci(release): two-run release pipeline (bump/build/sign, then tag) - #182

Merged
stozo04 merged 4 commits into
mainfrom
ci/release-pipeline
Sep 7, 2026
Merged

stozo04 merged 4 commits into
mainfrom
ci/release-pipeline

Conversation

@stozo04

@stozo04 stozo04 commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Adds the release automation. Two workflows, both workflow_dispatch only — nothing runs on push or PR, and nothing here fires until you dispatch it.

release.yml — run 1

Reads the current versionCode (never accepts it as input), bumps it, opens and merges its own bump PR, then builds and signs the AAB from the resulting merge sha and publishes it plus drafted notes as a run artifact. You download the .aab and upload it to Play.

Guards it enforces rather than assumes:

  • versionName must equal 1.0.<versionCode> before it will bump — if that convention ever breaks it stops instead of guessing a scheme
  • the bump diff must be exactly +2/−2 in app/build.gradle.kts, or the run fails
  • the build sha must be an ancestor of origin/main, and the version at that sha must match what's being released
  • the keystore is opened with keytool before spending fifteen minutes on R8, so bad signing material fails in seconds
  • jarsigner -verify must report jar verified

No pre-PR sweep, deliberately. The bump diff is two lines; nothing in the sweep can be affected by changing an integer, and every commit the release carries was already swept on its own feature PR. What feature PRs never cover is the release variant — R8, resource shrinking, the Firebase guard from #178, signing — so the bundleRelease + jarsigner pair is the gate this flow actually adds. Lesson 040 is detected and reported (dependency-catalog diff since the last tag) rather than silently skipped or falsely claimed.

dry_run: true builds main as it stands with no bump, PR or merge — use it for the first test.

tag.yml — run 2

Dispatched after the Play upload, so the tag keeps meaning this shipped rather than this built. Calls scripts/tag-release.ps1 unmodified — it shells out only to git and gh, and pwsh, gh and the JDK are all preinstalled on ubuntu runners. Passing the release run's id downloads that run's bundle first, which turns the script's "was this actually built" warning into a real check.

When the Play upload is automated later, this becomes the tail of release.yml and the two runs collapse into one. Nothing else about the design has to change.

Before the first run

Secrets: RELEASE_TOKEN, KEYSTORE_BASE64, KEYSTORE_PASSWORD, KEY_ALIAS, KEY_PASSWORD, GOOGLE_SERVICES_JSON.

Known first-run risks

Neither has been executed yet — these are the places I'd expect a first failure:

  1. compileSdk = 37 may not be installable on the runner if 37 is still a preview SDK. If it fails, the fix is an explicit SDK install step, not a compileSdk change.
  2. The baseline profile. baselineProfile(project(":baselineprofile")) is wired in, and if automatic generation during build is on, bundleRelease will want a device the runner doesn't have. The default is off, so this should be fine — but it's the most likely surprise.

Run it with dry_run: true first. That exercises checkout, SDK, signing material, R8 and jarsigner without touching main, so a failure costs nothing but a run.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Miqsf1M14nY5TKiZHzyjNY


Note

High Risk
Introduces automation that uses release signing secrets, can admin-merge version bumps to main, and gates production AAB output—misconfiguration or token scope issues could block releases or merge unintended bumps.

Overview
Adds manual-only GitHub Actions release automation in two dispatched runs, so tags still mean “shipped to Play,” not merely “built.”

Run 1 (release.yml) reads versionCode/versionName from app/build.gradle.kts (no typed version input), enforces the 1.0.<versionCode> convention, optionally bumps via an auto-opened/merged PR with a strict +2/−2 gradle diff check, pins a merge SHA on main, restores signing + google-services.json from secrets, runs :app:bundleRelease, verifies with jarsigner, warns on libs.versions.toml changes (Lesson 040), drafts note files, and uploads the signed .aab plus notes as artifacts. dry_run skips bump/PR/merge and builds current main.

Run 2 (tag.yml) runs after Play upload: optionally downloads the release run’s artifact, checks the bundle name/signature against the tagged SHA, then invokes existing scripts/tag-release.ps1 to create the GitHub release. Shared openloop-release concurrency limits overlapping release work.

Dictionary/cspell entries were added for shell/workflow terms used in the YAML.

Reviewed by Cursor Bugbot for commit 82e4221. Bugbot is set up for automated code reviews on this repo. Configure here.

Run 1 of the two-run release pipeline. Bumps the version, opens and merges
its own bump PR, then builds and signs the AAB from the resulting merge sha
and publishes it as a run artifact for the owner to upload to Play.

Deliberately runs no pre-PR sweep: the bump diff is two lines, and every
commit it carries was already swept by its own feature PR. The release-variant
build (R8, resource shrinking, the Firebase guard, signing) is the gate this
flow adds, because feature PRs only ever build debug.
Run 2 of the two-run release pipeline, dispatched by the owner once the .aab
from run 1 is uploaded to Play — so the tag keeps meaning "this shipped",
exactly as the manual process does today.

Calls scripts/tag-release.ps1 unchanged; pwsh and gh are both preinstalled on
ubuntu runners and the script shells out to nothing else. Passing the run id
downloads that run's bundle first, so the script's "was this actually built"
check is real rather than a warning.
Four terms the release workflow uses: `git diff --numstat` (the +2/-2 bump
guard), setup-java's temurin distribution, keytool's -storepass on the
pre-build keystore check, and bash elif in the Lesson 040 branch.

Each is load-bearing — dropping them would mean a weaker guard or a worse
check, which is the wrong trade against a dictionary entry. The IDE
dictionary moves in the same commit so gate 6d stays green.
Adds elif, numstat, storepass and temurin so scripts/sync-ide-dictionary.py
--check stays green alongside the cspell change.
@stozo04
stozo04 merged commit de0d656 into main Sep 7, 2026
3 checks passed
@stozo04
stozo04 deleted the ci/release-pipeline branch September 7, 2026 20:44

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix is ON, but it could not run because the branch was deleted or merged before autofix could start.

Reviewed by Cursor Bugbot for commit 82e4221. Configure here.

Comment thread .github/workflows/tag.yml
'-Version', '${{ inputs.version }}'
'-Sha', '${{ inputs.sha }}'
)
if ('${{ inputs.title }}') { $tagArgs += @('-Title', '${{ inputs.title }}') }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Title quotes break tag step

Low Severity

The optional title input is interpolated into PowerShell single-quoted strings. An apostrophe in the title (for example a “What’s new” release name) closes the string early and fails the tag step after Play upload.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 82e4221. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant