The whole flow on one page
One command, two stores, one build number. The left half already exists and ships today; the right half is what this plan builds.
+ READ BACK above is a scar, not decoration.Why one number for both stores
Your call on 2026-09-17: shared. Build 231 means the same commit on both stores.
Apple needs CFBundleVersion to rise per upload; Play needs versionCode to rise per upload.
A single counter satisfies both, and makes "which build is this" answerable in one place instead of two.
Cost of the choice, stated plainly: if one store rejects an upload and the other accepts it, the number is consumed on both. We skip a number rather than reuse one. Numbers are free; ambiguity is not.
What exists today, and what does not
Someone built the Android build half deliberately on 2026-09-09. That work is real and it holds up. What is missing is a key, an account, and the whole distribution half.
| Piece | State | Evidence |
|---|---|---|
| Android project (35 files, real Kotlin plugins) | EXISTS | app/android/ β CameraX, ML Kit text + face |
| Release signing wired into Gradle | EXISTS | app/android/app/build.gradle.kts reads key.properties |
| Missing key β debug fallback so any machine can build | EXISTS | Deliberate: nobody is blocked by not holding the key |
| Key present but incomplete β hard failure | EXISTS | Refuses to silently produce a debug-signed build that looks official |
| Key template committed, no real values | EXISTS | app/android/key.properties.example |
| Signing documentation | EXISTS | docs/ANDROID_RELEASE_SIGNING.md, written 09-09 |
| applicationId | EXISTS | com.hoop.hoop β permanent at first upload |
| The keystore itself | YOURS | By design, only you generate and hold it |
| Google Play developer account | YOURS | No trace anywhere in the repo |
| Android OAuth client for Google sign-in | MISSING β BREAKS LOGIN | See the warning below |
| Android build script | TO BUILD | Nothing exists |
| Play upload / distribution automation | TO BUILD | Nothing exists |
| Android row in the deployment table | TO BUILD | CLAUDE.md has no Android row at all |
The login screen offers Google sign-in (
login_screen.dart:338, account_screen.dart:330).
app/lib/api/google_signin.dart holds an iOS client ID and a Web client ID.
There is no Android client anywhere in the repo, and no google-services.json.
On Android, Google sign-in needs its own OAuth client registered against the package name plus the
SHA-1 of the certificate that signs the app.The trap inside the trap. With Play App Signing enabled, the app a tester installs is signed by Google's certificate, not by your upload key. So the SHA-1 to register is the app signing certificate from Play Console β Setup β App integrity β not the one from the keystore you generate. Register the wrong one and it works on a build you sideload and fails for every tester coming through Play, with an unhelpful error.
This is why the SHA-1 step in β’ sits after the first upload: it cannot be done before Play has generated that certificate. Left undiscovered, this surfaces as "testers cannot log in" only after everything else is finished.
Why the deployment table matters more than it looks
The web version once sat 20 days and a hundred versions stale because there was no row for it in the
deployment table in CLAUDE.md. Nobody was lazy β people follow the table, and what is not in the
table does not get remembered. Android has no row today. Adding one is task 4.2 and it is not paperwork.
One-time setup β who does what, and where
Four places. Parts 1 and 2 block everything else, so start them first; the account approval clock is the long pole and it is outside our control.
Part 1 Β· Your Mac, Terminal
Back up the file and its password to two separate places. Lose either and this app can never be updated again β there is no recovery path, only a new listing with zero users.
Part 2 Β· Google Play Console play.google.com/console
com.hoop.hoop β permanentPart 3 Β· Google Cloud Console console.cloud.google.com
| # | Step | Why |
|---|---|---|
| 3.1 | Create or pick a project | Reuse the one owning client ID 584364169898 so all HOOP OAuth lives together |
| 3.2 | Enable the Google Play Android Developer API | Without it, uploads must be done by hand |
| 3.3 | Create a service account, create a JSON key, download it | The pipeline's credentials |
| 3.4 | Note the service account email | Needed to grant it access in Play Console |
| 3.5 | Create an Android OAuth client: com.hoop.hoop + SHA-1 |
Fixes Google sign-in. SHA-1 comes from Play Console β Setup β App integrity, after the first upload. Optionally add a second client with the upload key's SHA-1 so sign-in also works on sideloaded builds. |
Part 4 Β· Back in Play Console β linking the two
| # | Step | Note |
|---|---|---|
| 4.1 | Setup β API access β link the Cloud project | Spans both consoles; easy to miss |
| 4.2 | Grant the service account release-to-testing-tracks permission | Do not grant production release rights yet |
Part 5 Β· NOVA's build machine
| # | Step | Note |
|---|---|---|
| 5.1 | Put the .jks outside the repo (~/.hoop-keys/) | Never inside app/android/ |
| 5.2 | Create app/android/key.properties, four values | Never enters git |
| 5.3 | Put the service account JSON outside the repo | Same rule |
| 5.4 | flutter doctor --android-licenses |
The mac mini reports these unaccepted today; NOVA's machine needs its own check |
| 5.5 | Confirm JDK 17 | Gradle config targets 17 |
The build β what NOVA does, and what it prints
One command, detached from the session. The iOS pipeline exists because builds outlive the agent conversation that started them: packages once sat finished and unsent for six hours because the step that said "now upload it" was still attached to a session that had ended.
What is typed
nohup setsid. macOS has no setsid; that command prints one line
and the pipeline never starts. It has already cost one build. start_new_session=True, then
verify the process is alive and the log is growing β "I launched it" is not "it is running".What the log looks like when it goes well
What it looks like when one store fails
The two announcements
The tester β first install, then every update after
This is the half that decides whether the whole thing was worth building. Requirement 5 was "testers receive subsequent builds through the same mechanism" β that rules out sending APK files, and it is why Play internal testing wins over every alternative.
Build plan β 4 phases, 18 tasks
Each task is separately trackable and has one objective. Nothing here is started. Phase 0 is yours and blocks the rest; phases 1β3 are mine.
| # | Task | Done when |
|---|---|---|
| 0.1 YOU | Generate the keystore, back it up twice, keep it outside the repo | Two independent backups exist, one of them not on your Mac |
| 0.2 YOU | Google Play developer account, US$25, identity verification | Console lets you create an app |
| 0.3 YOU | Create the app Β· Play App Signing ON Β· confirm com.hoop.hoop |
Both permanent choices made deliberately, not clicked past |
| 0.4 YOU | App content declarations, privacy policy URL | Internal testing track can accept a release |
| 0.5 YOU | Cloud project Β· enable Play Developer API Β· service account + JSON key | JSON key downloaded |
| 0.6 YOU | Link Cloud project in Play Console, grant the service account testing-track rights | Not production rights β testing only |
| # | Task | Done when |
|---|---|---|
| 1.1 | Place keystore + key.properties + service account JSON on NOVA's machine;
accept SDK licences; confirm JDK 17 |
:app:signingReport shows Config: release pointing at the real key |
| 1.2 | First real signed .aab and .apk |
The .aab is signed by the real key, not debug β verified from the artifact |
| 1.3 RISK | Install on a real Android phone and use it | App opens, camera works, a message sends. Nothing in this project has ever run on Android hardware. |
| 1.4 BLOCKER | Android OAuth client with the Play app signing SHA-1; add the client ID to
google_signin.dart |
Google sign-in works on a build installed from Play, not just sideloaded |
| 1.5 | First upload to the internal track, by hand | One tester installs from Play and opens it |
| # | Task | Done when |
|---|---|---|
| 2.1 | play_versions.py β highest versionCode on the track, pick next |
Mirrors asc_builds.py; agrees with the iOS counter |
| 2.2 | cut_android.sh β build, verify versionCode out of the artifact,
reuse both CHANGELOG gates |
A deliberately wrong build number is caught by unzipping the .aab, not by trusting the log |
| 2.3 | play_publish.py β upload, notes, rollout, each with read-back |
Every write is followed by a read that proves it landed |
| 2.4 | Merge into one command: iOS then Android, sequential | They never run concurrently in one checkout |
| 2.5 | Per-store announcements; General only when the log proves both | Forcing a half-failure produces the half-failure message, not a green tick |
| # | Task | Done when |
|---|---|---|
| 3.1 | Android row in the CLAUDE.md deployment table |
Someone who only reads the table still ships Android |
| 3.2 | Update docs/ANDROID_RELEASE_SIGNING.md for the automated path |
Same commit as the code β a doc that lags is a doc that lies |
| 3.3 | Deliberately broken runs: wrong build number, bad key, revoked service account | Each fails loudly at the right step. An untested gate is not a gate. |
| 3.4 | End-to-end rehearsal, then a second build | A tester receives the update without being told to do anything |
Deliberately not scheduled
- Closed / open testing and production release. Those need review and more declarations. Internal testing is the TestFlight equivalent and is where this stops.
- Firebase App Distribution. Ruled out on 2026-09-17 β building distribution twice to save a wait.
- CI-hosted builds. Android could build on
ubuntu-latestwhere iOS cannot, but splitting the two across machines breaks the shared build number and the single command. Not worth it yet. - In-app purchases / Play Billing. Separate signing, separate webhooks, doubles the work
(
IAP_GREEN_ENERGY.mdΒ§9). Out of scope.
Decisions on record
| # | Decision | Decided by | Date |
|---|---|---|---|
| 1 | Distribution is Play internal testing, not Firebase, not raw APK | Navi4 recommended Β· Leong accepted | 2026-09-17 |
| 2 | One build number shared across iOS and Android | Leong | 2026-09-17 |
| 3 | No Firebase bridge while the Play account clears β we wait | Leong | 2026-09-17 |
| 4 | One command, sequential iOS then Android, per-store reporting | Navi4 Β· technical constraint | 2026-09-17 |
| 5 | The keystore is generated and held by Leong only | Pre-existing, 2026-09-09 | 2026-09-09 |
| 6 | Missing key β debug fallback; partial key β hard failure | Pre-existing, 2026-09-09 | 2026-09-09 |
Open questions
| Question | Blocks | Who |
|---|---|---|
| Who has an Android phone for task 1.3? | Phase 1, and therefore everything after it | Leong |
| Does a privacy policy URL already exist? | Task 0.4 | Leong |
Keep com.hoop.hoop or move to a company-domain id? | Task 0.3 β permanent once uploaded | Leong |
Risks, stated plainly
origin/master.