Releasing
How releases work
AutoBudget uses semantic-release on main (conventional commits) plus a
release-triggered artifact build:
flowchart LR
A[push to main] --> B[release.yml: semantic-release]
B -->|analyze commits| C[version bump + CHANGELOG.md]
C --> D[tag vX.Y.Z]
D --> N[notes job: OpenCode rewrites release title/body + play_notes]
N --> E[build job: assembleRelease + bundleRelease]
E --> G[upload AutoBudget-X.Y.Z.apk + .aab to release]
E --> H{ENABLE_PLAY_UPLOAD?}
H -->|yes| I[upload AAB to Play, internal track<br>whatsnew = AI play_notes]
J[manual workflow_dispatch, tag input] --> N
- Conventional commit types drive the bump:
feat:→ minor,fix:→ patch, breaking change /BREAKING CHANGE:→ major. See.releaserc.json. CHANGELOG.mdis generated by the release workflow — don't edit by hand. Release notes use theangularpreset; theconventionalcommitspreset package (v9+) breaks the bundledrelease-notes-generatorand yields empty changelog/release-note bodies.- Commits must follow conventional format or no release is cut.
- On published releases, an AI Release Notes job rewrites the GitHub release title/body for end users and produces the Play "What's new" text — see AI release notes below.
AI release notes
When a release is published, build-artifacts.yml runs an AI Release
Notes job before building artifacts:
- It checks out the new tag with full history and installs
opencode+jq+gh(via thesetup-toolsaction fromzharif/reusable-workflows). - The
run-opencode-taskaction sends the prompt in.github/prompts/release-notes.mdto OpenCode. The agent diffsvPREV..vNEWand must answer with one JSON block:{title, notes_markdown, play_notes}. - A plain bash step (not the agent — it has no GitHub token) validates the
JSON and applies it:
gh release edit --title --notes-file, and exposesplay_notesas the job output that feeds Play'swhatsnew-en-US.
Properties worth knowing:
- Fallback is automatic: if OpenCode returns no usable JSON (model down,
bad output), the release keeps semantic-release's notes and Play falls back
to the version's
CHANGELOG.mdsection. The job runs withcontinue-on-error: true, so the artifact build never blocks on the LLM. - Model/keys reuse the existing
CI_AI_MODEL_NAMEvar andCI_AI_*secrets wired in.github/workflows/_opencode.yml— nothing new to set up. - Manual rebuilds run the LLM too: the notes job has no event gate —
only its final release edit step checks
event_name == 'release'. Aworkflow_dispatchre-run regeneratesplay_notes(and spends tokens) but leaves the GitHub release untouched. - Tweaking tone/structure = editing the prompt file via a normal PR; it is versioned at the tag being released.
Versioning
versionName / versionCode are computed from the tag:
v1.2.3 → versionName=1.2.3, versionCode = 1*10000 + 2*100 + 3 = 10203
Set explicitly in the build: -PversionName=1.2.3 -PversionCode=10203.
Play requires versionCode to keep rising — it never goes backwards.
Signing: Play App Signing + upload key
Google holds the app signing key. CI signs with an upload key (rotatable without breaking installed app signatures).
One-time Play Console setup
- Create the upload keystore (local machine, never commit it):
keytool -genkey -v -keystore upload-keystore.jks \
-keyalg RSA -keysize 2048 -validity 10000 \
-alias upload -storepass 'CHANGE_ME' -keypass 'CHANGE_ME'
- In Play Console → app → Setup → App integrity → App signing:
- either let Google generate the app signing key, or upload your own;
- register the upload certificate from
upload-keystore.jks. - Grant the upload key the
uploadrole on the app.
Play upload auth: Workload Identity Federation
CI authenticates to Play without a service-account JSON key, via WIF
(Google Cloud google-github-actions/auth → ADC). The build job exchanges
its GitHub OIDC token for a GCP service account, then
r0adkll/upload-google-play reads the credentials file from
steps.auth.outputs.credentials_file_path (passed as the serviceAccountJson
input).
- In Play Console → Users and permissions → invite
gha-runner@zharif.iam.gserviceaccount.comwith the app's Release (upload) role. This is the same GCP account WIF impersonates — no JSON key file ever leaves Google. - Minimum app permissions for an internal-track upload: View app
information, Upload app bundles and APKs, Create and release draft
releases. Not needed: Create and approve production releases (only for
track: production) and Manage Play App Signing (CI signs with the upload key; Play re-signs automatically). - The WIF pool/provider + service account are already configured
org-wide; override per-repo with the
GCP_WORKLOAD_IDENTITY_PROVIDER/GCP_SERVICE_ACCOUNTvars if needed.
Required GitHub secrets (Actions → Secrets → Actions)
| Secret | Value |
|---|---|
RELEASE_KEYSTORE_BASE64 |
base64 -w0 upload-keystore.jks |
RELEASE_STORE_PASSWORD |
keystore password |
RELEASE_KEY_ALIAS |
key alias (upload) |
RELEASE_KEY_PASSWORD |
key password |
No Play credentials secret needed — the WIF auth step covers it.
Toggle
| Variable | Value | Effect |
|---|---|---|
ENABLE_PLAY_UPLOAD |
true |
build-artifacts.yml authenticates via WIF and uploads the AAB to Play internal track |
Off by default. When unset (or any RELEASE_* secret missing) the release APK
is still built and signed with the debug key, as before — so nothing breaks.
Note: the Play upload action requires the credentials passed explicitly via its
serviceAccountJsoninput. Merely exportingGOOGLE_APPLICATION_CREDENTIALS(as the WIF auth step does) is not enough — without the input the action fails immediately withUnknown error occurred.
Rebuild / re-upload an existing version
build-artifacts.yml accepts a manual workflow_dispatch with a required
tag input (e.g. v0.1.9). Use it to rebuild the APK/AAB for an existing tag
and re-upload to the GitHub release and/or Play (when ENABLE_PLAY_UPLOAD is
set) after a failed upload. The --clobber flag overwrites existing release
assets.
- Actions → Build Release Artifacts → Run workflow.
- Enter the existing tag in the
tagfield. - Trigger. Artifacts are rebuilt from that tag and (re)uploaded.
Manual release checklist
- Tests green:
./gradlew test lint ktlintCheck. - Smoke-test the debug APK on an emulator with the fixture script
(
android-app/scripts/post_fake_notifications.sh --broadcast). - Context audit:
bash scripts/docs-guard.shgreen; skim the CONTEXT.md glossary againstlsofcommon/,ui/components/,listener/for renames; rerun one or two evals if the context architecture changed this cycle. - Verify 16 KB alignment:
zipalign -c -P 16 app-release.apk. - (Optional) verify Play signing locally:
cd android-app
export RELEASE_KEYSTORE_FILE="$(pwd)/upload-keystore.jks"
export RELEASE_STORE_PASSWORD='...' RELEASE_KEY_ALIAS='upload' RELEASE_KEY_PASSWORD='...'
./gradlew assembleRelease
- Set
ENABLE_PLAY_UPLOAD=truebefore releasing if you want the AAB on Play. - Merge conventional commits to
main; semantic-release tags the version. - Confirm
build-artifacts.ymluploadedAutoBudget-<version>.apkand.aabto the release, and (if enabled) the AAB landed in Play Console → internal testing with the version's changelog as release notes (fromCHANGELOG.md, truncated to 500 chars). If the Play upload failed, re-run the workflow manually with the tag input. - Promote the internal release to production manually in Play Console when ready, after the release gates below.
Production promotion gates (before internal → production)
- Pre-launch report — run it on the internal track; triage any crashes, ANR, or security findings with a written rationale before proceeding.
- 16 KB alignment —
zipalign -c -P 16 app-release.apk(also in the manual checklist) and confirm the R8-shrunk release APK installs on an API 26 device. - Fixture smoke —
android-app/scripts/post_fake_notifications.sh --broadcastthen--duplicateto prove capture + dedupe work on the release build. - Android vitals baseline — record crash/ANR baseline on the internal track (no crash-reporting SDK; rely on Play vitals + pre-launch report).
- Closed testing — keep the build on a closed track for ≥1 week with no P0/P1 issues before promoting.
- Promote internal → production (step 8 above).
Notes
- Workflows:
android-tests.yml(PR),docs-guard.yml(docs PRs),build-artifacts.yml(release),release.yml(main), anddocs-screenshots.yml(currently disabled) — marketplace actions SHA-pinned (org reusable workflows tag-pinned), least-privilege permissions. Details in docs/architecture/tooling.md. - The release workflow uses
zharif/reusable-workflows(see docs/agents/issue-tracker.md for repo conventions).