Skip to content

ADR-0018: Play Store distribution with Play App Signing

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: bundleRelease feeds Play; assembleRelease keeps the direct-download APK on the GitHub release.
  • Toggleable upload: the AAB upload step runs only when vars.ENABLE_PLAY_UPLOAD == 'true', targeting the internal track. Promotion to production stays a manual Play Console step.
  • Graceful fallback: when the RELEASE_* env vars/secrets are absent (local/E2E), app/build.gradle.kts falls back to debug signing, so the existing local release workflow is unchanged.
  • r0adkll/upload-google-play action handles the Play API call. Auth is Workload Identity Federation: google-github-actions/auth exchanges 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 versionCode must 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.