Skip to content

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.md is generated by the release workflow — don't edit by hand. Release notes use the angular preset; the conventionalcommits preset package (v9+) breaks the bundled release-notes-generator and 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:

  1. It checks out the new tag with full history and installs opencode + jq + gh (via the setup-tools action from zharif/reusable-workflows).
  2. The run-opencode-task action sends the prompt in .github/prompts/release-notes.md to OpenCode. The agent diffs vPREV..vNEW and must answer with one JSON block: {title, notes_markdown, play_notes}.
  3. 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 exposes play_notes as the job output that feeds Play's whatsnew-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.md section. The job runs with continue-on-error: true, so the artifact build never blocks on the LLM.
  • Model/keys reuse the existing CI_AI_MODEL_NAME var and CI_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'. A workflow_dispatch re-run regenerates play_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

  1. 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'
  1. In Play Console → app → Setup → App integrityApp signing:
  2. either let Google generate the app signing key, or upload your own;
  3. register the upload certificate from upload-keystore.jks.
  4. Grant the upload key the upload role 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).

  1. In Play Console → Users and permissions → invite gha-runner@zharif.iam.gserviceaccount.com with the app's Release (upload) role. This is the same GCP account WIF impersonates — no JSON key file ever leaves Google.
  2. 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).
  3. The WIF pool/provider + service account are already configured org-wide; override per-repo with the GCP_WORKLOAD_IDENTITY_PROVIDER / GCP_SERVICE_ACCOUNT vars 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 serviceAccountJson input. Merely exporting GOOGLE_APPLICATION_CREDENTIALS (as the WIF auth step does) is not enough — without the input the action fails immediately with Unknown 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.

  1. Actions → Build Release ArtifactsRun workflow.
  2. Enter the existing tag in the tag field.
  3. Trigger. Artifacts are rebuilt from that tag and (re)uploaded.

Manual release checklist

  1. Tests green: ./gradlew test lint ktlintCheck.
  2. Smoke-test the debug APK on an emulator with the fixture script (android-app/scripts/post_fake_notifications.sh --broadcast).
  3. Context audit: bash scripts/docs-guard.sh green; skim the CONTEXT.md glossary against ls of common/, ui/components/, listener/ for renames; rerun one or two evals if the context architecture changed this cycle.
  4. Verify 16 KB alignment: zipalign -c -P 16 app-release.apk.
  5. (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
  1. Set ENABLE_PLAY_UPLOAD=true before releasing if you want the AAB on Play.
  2. Merge conventional commits to main; semantic-release tags the version.
  3. Confirm build-artifacts.yml uploaded AutoBudget-<version>.apk and .aab to the release, and (if enabled) the AAB landed in Play Console → internal testing with the version's changelog as release notes (from CHANGELOG.md, truncated to 500 chars). If the Play upload failed, re-run the workflow manually with the tag input.
  4. Promote the internal release to production manually in Play Console when ready, after the release gates below.

Production promotion gates (before internal → production)

  1. Pre-launch report — run it on the internal track; triage any crashes, ANR, or security findings with a written rationale before proceeding.
  2. 16 KB alignmentzipalign -c -P 16 app-release.apk (also in the manual checklist) and confirm the R8-shrunk release APK installs on an API 26 device.
  3. Fixture smokeandroid-app/scripts/post_fake_notifications.sh --broadcast then --duplicate to prove capture + dedupe work on the release build.
  4. Android vitals baseline — record crash/ANR baseline on the internal track (no crash-reporting SDK; rely on Play vitals + pre-launch report).
  5. Closed testing — keep the build on a closed track for ≥1 week with no P0/P1 issues before promoting.
  6. Promote internal → production (step 8 above).

Notes

  • Workflows: android-tests.yml (PR), docs-guard.yml (docs PRs), build-artifacts.yml (release), release.yml (main), and docs-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).