ADR-0018: Play Store distribution with Play App Signing
- Status: Accepted
- Date: 2026-08-12
- Deciders: maintainers
- Related: releasing.md, tooling.md
Context
Releases previously stopped at the GitHub release page: build-apk.yml
built assembleRelease and uploaded a debug-signed APK as a release asset.
There was no Play Store distribution, no real signing key, and only an APK
was produced (Google Play now wants AABs for new apps).
Decision
Ship to Google Play via a release-triggered pipeline that keeps secrets out of the repo:
- Play App Signing: Google holds the app signing key; CI signs with a dedicated upload key stored only as GitHub secrets. The upload key can be rotated without breaking installed app signatures.
- AAB for Play, APK for GitHub:
bundleReleasefeeds Play;assembleReleasekeeps the direct-download APK on the GitHub release. - Toggleable upload: the AAB upload step runs only when
vars.ENABLE_PLAY_UPLOAD == 'true', targeting theinternaltrack. Promotion to production stays a manual Play Console step. - Graceful fallback: when the
RELEASE_*env vars/secrets are absent (local/E2E),app/build.gradle.ktsfalls back to debug signing, so the existing local release workflow is unchanged. r0adkll/upload-google-playaction handles the Play API call. Auth is Workload Identity Federation:google-github-actions/authexchanges the GitHub OIDC token for the GCP service account and writes ADC credentials (GOOGLE_APPLICATION_CREDENTIALS), which the upload action consumes. No service-account JSON secret — the service account is invited directly in Play Console.
Consequences
- Release artifacts: signed APK on GitHub + (opt-in) signed AAB on Play internal track.
- Four new GitHub secrets (keystore only) and one toggle variable — see releasing.md for exact setup.
- Play
versionCodemust keep rising; the tag-scheme (MAJ*10000 + MIN*100 + PAT) guarantees monotonicity while SemVer bumps. - Debug-key fallback means a misconfigured CI still produces a working APK rather than failing silently with the wrong signature.
- APK + AAB are built in the same workflow so the two can't drift.
Alternatives considered
- Gradle Play Publisher plugin — rejected: more build wiring, version churn, keeps credentials in Gradle state rather than a dedicated action step.
- Uploading directly to
production— rejected: no human gate between tag and public users. - Storing the keystore in-repo (encrypted) — rejected: encrypted-at-rest files add key-management burden without benefit over GitHub secrets.