Superseded — historical record only⚠️ SUPERSEDED — historical record only. Android has since shipped (Play internal testing) and the blockers below are resolved (the app record exists; release signing now uses a real upload key via
plugins/withAndroidUploadSigning.js, not debug signing). The living Android release flow is releasing.html §5 — follow that, not this. Kept only for the first-time context.
The iOS side is automated (see memory project-ios-release-pipeline). Android
has never shipped, so it needs a one-time setup. Three things block it today:
android/app/build.gradle:112-115 — release { signingConfig signingConfigs.debug },
with the template's own warning still in place. Play rejects debug-signed
uploads. This is the real blocker..aab (Android App Bundle), not the .apk we build for
emulator testing. And eas.json's submit.production.android is {} — no
track configured.These need a human with the Google account — no API key can substitute (same shape as the iOS "ASC app record needs an Apple ID" limitation).
| # | Step | Notes |
|---|---|---|
| 1 | Google Play Console account — https://play.google.com/console | One-time $25 fee. Skip if the account already exists. |
| 2 | Create the app — "Create app" → name Games for Group, language, "App" (not Game — or Game if you prefer the category), Free | The package name com.karimdev.gamesforgroup is set on first upload, not here. It is permanent once uploaded. |
| 3 | Accept the Play App Signing terms when prompted | Recommended: let Google hold the app signing key; you keep only an upload key. If the upload key is ever lost, Google can reset it. Losing a self-managed app signing key means you can never update the app again. |
| 4 | Fill the mandatory declarations | Privacy policy URL, content rating questionnaire, target audience, data safety, ads declaration. Play blocks release until these are done — they take longer than the build. |
Pick one before any real build:
Option A — EAS manages the keystore (simplest).
eas build --platform android --profile production generates and stores an
upload keystore in your Expo account, and eas submit can push it to Play.
Costs EAS cloud quota (the same 15/month pool you're protecting for other apps).
Option B — local keystore, zero quota (matches the iOS pipeline).
Generate an upload key once, keep it in the login keychain / a password manager,
wire it into android/app/build.gradle via ~/.gradle/gradle.properties (never
commit it), and build ./gradlew bundleRelease locally. Free, but the key is
yours to back up — lose it and you cannot ship updates.
Recommendation: Option B, to stay consistent with the no-quota iOS route — but pair it with Play App Signing (step 3), which removes the catastrophic-loss risk: Google holds the real signing key, and a lost upload key can be reset.
keytool if you'd rather it
never touches this machine's agent).signingConfig to it, keeping secrets in
~/.gradle/gradle.properties (gitignored) rather than the repo.versionCode and build the .aab:
cd apps/app/android && ./gradlew bundleReleaseversionCode, and — importantly — that
assets/guest-dist has all 70 entries (the LAN browser-guest client; see
the 2026-07-16 nightlog for the aapt underscore bug that silently stripped
them, and the release-build guard that now prevents a repeat).eas.json submit.production.android with a Play service account
so future submits are one command.Karim: Play account → create app → accept Play App Signing → declarations
↓
Claude: keystore + signingConfig + versionCode
↓
Claude: ./gradlew bundleRelease → app-release.aab
↓
Karim (first time only): upload the .aab by hand in Play Console → Internal testing
↓ (this first upload is what locks in the package name)
Claude: wire a Play service account → future uploads are automated
The first upload must be manual — Play's API cannot create the initial release for a brand-new app, exactly like ASC's app-record limitation on iOS. After that it can be scripted.
versionCode must be higher than any previous upload (starts at 1).-PreactNativeArchitectures=arm64-v8a for speed. A real release must keep
all four ABIs (the gradle.properties default) or use an app bundle, which
splits per-device automatically. bundleRelease handles this — don't pass the
single-ABI flag for a Play build.guest-dist is missing
(modules/expo-lan-server/android/build.gradle). If it fails, run
cd apps/app && bun run export:lan-guest and rebuild.