This is the one-time setup that turns the manual
download/unzip/upload/verify/install loop into: merge to main → your phone
auto-updates via the Play Store.
.github/workflows/android-release.yml builds a signed release AAB and
publishes it to the internal testing track. Three ways to trigger it:
| Trigger | Result |
|---|---|
Push / merge to main (touching apps/android/**) |
Auto-publishes to internal testing |
A PR labelled deploy-beta |
Publishes that branch to your phone (opt-in) |
| Actions → Android Release → Run workflow | Publishes any branch, on demand |
Every run also uploads the AAB as a workflow artifact (app-release-aab).
It's fail-soft: until the secrets below exist, the job still runs — it just
builds an unsigned AAB and skips the publish step. So nothing goes red while you
work through this list. The debug-APK workflow (android.yml) is untouched.
1. Generate upload keystore → 2. Add signing secrets → 3. First CI AAB
→ 4. Seed the FIRST release by hand → 5. Create service account
→ 6. Done: everything is automatic
The reason for the hand-upload in step 4: the Play Publishing API refuses to create an app's first-ever release. One manual upload seeds it; the API takes over after that.
You only need Java for this one command (keytool ships with any JDK/JRE —
it is not the Android SDK). If you don't have Java, a lightweight JDK install
is enough; nothing else here needs it.
keytool -genkeypair -v \
-keystore upload-keystore.jks \
-alias pbbls-upload \
-keyalg RSA -keysize 2048 -validity 10000It prompts for a keystore password, a key password (you can reuse the same one), and a name/organisation (press Enter to accept defaults — it doesn't matter for an upload key).
This is your upload key, not the app-signing key. With Play App Signing (step 4) Google holds the real signing key; this upload key just proves the AAB came from you. If it's ever lost, Google can reset it — but still keep the
.jksfile and its passwords in your password manager, and never commit them (.gitignorealready blocks*.jks).
Base64-encode the keystore so it can live as a secret:
# macOS
base64 -i upload-keystore.jks | pbcopy
# Linux
base64 -w0 upload-keystore.jksThen add these under repo Settings → Secrets and variables → Actions → New repository secret:
| Secret | Value |
|---|---|
KEYSTORE_BASE64 |
the base64 blob from above |
KEYSTORE_PASSWORD |
the keystore password |
KEY_ALIAS |
pbbls-upload |
KEY_PASSWORD |
the key password |
SUPABASE_URL and SUPABASE_ANON_KEY already exist from the debug-APK setup —
the release build reuses them.
Trigger the release workflow once — the simplest is Actions → Android Release
→ Run workflow on this branch (or merge to main). When it's green, open the
run and download the app-release-aab artifact. It's signed with your
upload key. (Publish is still skipped — no service account yet. Expected.)
- Play Console → your app (
app.pbbls.android) → Testing → Internal testing → Create new release. - If asked about Play App Signing, accept the default (Use a
Google-generated key). Upload the
app-release.aabfrom step 3 — Google registers your upload certificate from it automatically. - Add a release name/notes → Save → Review release → Roll out to internal testing.
- Testers: Internal testing → Testers tab → create an email list that includes your Google account → save. Copy the “Copy link” opt-in URL.
- On your phone: open the opt-in link, tap Become a tester, then open the Play Store link and install. From now on the Play Store auto-updates it — that's the whole point.
Internal testing is the least-gated track, so you usually don't need the full store listing / content rating / data-safety form to roll out. The Console may still prompt for a few app-level declarations before the first rollout — just follow its prompts.
This is the credential CI uses to publish.
- Play Console → Setup → API access. If prompted, link/create a Google Cloud project.
- Google Cloud Console → IAM & Admin → Service Accounts → Create service
account (e.g.
github-play-publisher). No project-level roles needed. Then Keys → Add key → JSON and download the file. - Enable the Play API in that project: Google Cloud Console → APIs & Services → Library → search "Google Play Android Developer API" → Enable. Miss this and the publish step fails with "…API has not been used in project … or it is disabled" — reusing an existing Cloud project (e.g. your Supabase one) does not enable it for you.
- Back in Play Console → Users and permissions → Invite new users → enter
the service account's email → grant app access to
app.pbbls.androidwith permission to Release to testing tracks (Release manager is fine). - Add the entire JSON file contents as a repo secret named
PLAY_SERVICE_ACCOUNT_JSON.
Give it a few minutes to propagate.
- Merge to
main→ auto-publishes to internal testing → phone updates. - Label a PR
deploy-beta→ that branch goes to your phone (remove the label or merge tomainto revert). Create the label once: repo → Issues or Pull requests → Labels → New label →deploy-beta. - Actions → Android Release → Run workflow → any branch, on demand.
| Symptom | Cause / fix |
|---|---|
| Upload rejected: "not signed" / "unsigned" | A KEYSTORE_* secret is missing or mistyped — check the run's Decode upload keystore and Assemble signed release AAB steps ran. |
| "Version code N has already been used" | That versionCode already reached Play. Re-running a run reuses its run_number, so push a new commit for a higher one. (Re-running a run whose publish failed before it reached Play is fine — the code was never consumed.) |
| Publish step: "Google Play Android Developer API has not been used in project … or it is disabled" | Enable the Google Play Android Developer API in the Cloud project the service account belongs to (the error links straight to it), wait a few minutes, then re-run. See step 5.3. |
| First upload errors about "existing users can't upgrade" | The manual seed (step 4) hasn't happened yet — the API can't create the first release. |
| Publish step: "caller does not have permission" | Service account not granted app access in Play Console (step 5.4), or still propagating — wait a few minutes. |
| Nothing publishes, job green | PLAY_SERVICE_ACCOUNT_JSON not set yet — the publish step self-skips by design. |
- The upload keystore and its passwords: keep in a password manager, never
commit (
*.jksis git-ignored). - The service account JSON can publish to your Play account — treat it like
a password. It only ever lives as the
PLAY_SERVICE_ACCOUNT_JSONGitHub secret; don't download it into the repo.