mirror of
https://github.com/tiennm99/loto.git
synced 2026-10-11 12:19:05 +00:00
Fresh Kotlin 2.2 + Jetpack Compose app at feature parity with web/ (game logic, settings contract, voice semantics ported one-to-one with twinned tests; 104 unit + 9 instrumentation tests). Fully offline: VIBRATE is the only permission; Media3's ACCESS_NETWORK_STATE is stripped. Voice clips mount from web/static/audio at build time. Same applicationId, signing env contract, and Play alpha track; versionCode 7 / 0.2.0. CI Android job is Gradle-only (lint, test, debug + R8 release APK) and the release workflow fails fast on an unbumped versionCode. BREAKING CHANGE: WebView localStorage (settings, in-progress round) is not migrated; wrapper users start clean.
244 lines
9.9 KiB
Markdown
244 lines
9.9 KiB
Markdown
# Google Play Auto-Publish Setup
|
|
|
|
How to configure GitHub secrets so pushing a `v*.*.*` tag builds a signed
|
|
AAB/APK, attaches both to a GitHub Release, and uploads the AAB to the Play
|
|
Console **closed testing track** automatically. Driven by
|
|
[`.github/workflows/android-release.yml`](../.github/workflows/android-release.yml).
|
|
|
|
Verified working: tag `v0.0.2` (2026-08-05) built, released, and uploaded
|
|
end-to-end. Uploads targeted the internal track until v0.1.1; from v0.1.2 the
|
|
workflow publishes to `alpha`, the closed-testing track.
|
|
|
|
## Prerequisites (one-time, manual)
|
|
|
|
These cannot be automated:
|
|
|
|
1. Google Play Console account ($25 one-time) at [play.google.com/console](https://play.google.com/console/signup)
|
|
2. App entry created with package name `com.miti99.loto`
|
|
3. **First AAB uploaded manually** to the Internal Testing track via the Play
|
|
Console UI — Google requires the first upload to be manual
|
|
4. Store listing completed (icon, screenshots, descriptions, content rating,
|
|
privacy policy URL, data safety form)
|
|
|
|
## Signing secrets (required for any release build)
|
|
|
|
| Secret | Description |
|
|
|--------|-------------|
|
|
| `KEYSTORE_BASE64` | Base64 of the PKCS12 keystore file |
|
|
| `KEYSTORE_PASSWORD` | Keystore password |
|
|
| `KEY_ALIAS` | Key alias (list aliases: `keytool -list -keystore your.p12`) |
|
|
| `KEY_PASSWORD` | Key password |
|
|
|
|
Encode the keystore:
|
|
|
|
```bash
|
|
# bash / Git Bash
|
|
base64 -w0 miti99-apps.p12 | gh secret set KEYSTORE_BASE64 -R tiennm99/loto
|
|
```
|
|
|
|
```powershell
|
|
# PowerShell
|
|
[Convert]::ToBase64String([IO.File]::ReadAllBytes("C:\path\to\miti99-apps.p12")) |
|
|
gh secret set KEYSTORE_BASE64 -R tiennm99/loto
|
|
```
|
|
|
|
Set the remaining three with `gh secret set <NAME> -R tiennm99/loto` (prompts
|
|
for the value interactively, keeping it out of shell history).
|
|
|
|
## Play Store auto-publish: the 5 steps
|
|
|
|
Do this **after** the first manual AAB upload has been accepted.
|
|
|
|
How the pieces fit: the service account lives in a Google Cloud project, but
|
|
its **authority to publish comes from Play Console, not from Cloud IAM**. The
|
|
GCP side only creates an identity and a key; the Play Console side grants the
|
|
actual publishing rights. That is why the service account needs **zero IAM
|
|
roles** in the Cloud project.
|
|
|
|
### Step 1 — Create a GCP project and enable the API
|
|
|
|
1. Open [console.cloud.google.com](https://console.cloud.google.com) and sign
|
|
in with any Google account (it does **not** have to be the Play Console
|
|
owner account).
|
|
2. Project picker (top bar) → **New Project** → name it e.g.
|
|
`loto-play-publishing` → **Create**. Reusing an existing project is fine.
|
|
3. **APIs & Services → Library** → search **Google Play Android Developer
|
|
API** → **Enable**.
|
|
|
|
Or with the `gcloud` CLI:
|
|
|
|
```bash
|
|
gcloud projects create loto-play-publishing
|
|
gcloud config set project loto-play-publishing
|
|
gcloud services enable androidpublisher.googleapis.com
|
|
```
|
|
|
|
### Step 2 — Create the service account (no IAM roles)
|
|
|
|
1. **IAM & Admin → Service Accounts → Create service account**.
|
|
2. Name: e.g. `play-publisher` (email becomes
|
|
`play-publisher@loto-play-publishing.iam.gserviceaccount.com`).
|
|
3. **"Grant this service account access to project" — skip it.** Leave the
|
|
role empty; publishing rights come from Play Console in Step 4. Granting
|
|
Editor/Owner here is a common mistake that only widens the blast radius if
|
|
the key leaks.
|
|
4. **"Grant users access to this service account" — skip it** too. **Done**.
|
|
|
|
```bash
|
|
gcloud iam service-accounts create play-publisher \
|
|
--display-name "Play Store publisher (CI)"
|
|
```
|
|
|
|
### Step 3 — Download a JSON key
|
|
|
|
1. On the service account row → **⋮ → Manage keys** (or the **Keys** tab) →
|
|
**Add Key → Create new key → JSON → Create**. The file downloads once and
|
|
cannot be re-downloaded — treat it like a password.
|
|
2. Keep it **outside the repo** (e.g. `~/secrets/`). It goes into a GitHub
|
|
secret in Step 5 and can be deleted locally afterwards.
|
|
|
|
```bash
|
|
gcloud iam service-accounts keys create service-account.json \
|
|
--iam-account play-publisher@loto-play-publishing.iam.gserviceaccount.com
|
|
```
|
|
|
|
### Step 4 — Add the service account to Play Console
|
|
|
|
Done in Play Console by the **account owner** (or an admin who can manage
|
|
users):
|
|
|
|
1. Copy the service-account **email** from Step 2.
|
|
2. [play.google.com/console](https://play.google.com/console) → **Users and
|
|
permissions** → **Invite new users**.
|
|
3. Paste the service-account email. No invitation email is sent for service
|
|
accounts — access activates as soon as you save.
|
|
4. Under **App permissions** tab → **Add app** → select **Lo To
|
|
(`com.miti99.loto`)** — scope access to this one app instead of
|
|
account-wide permissions.
|
|
5. Tick exactly these two permissions:
|
|
- **View app information and download bulk reports (read-only)** — the
|
|
API needs it to read the app's edit state
|
|
- **Release apps to testing tracks** — enough for the workflow's
|
|
`tracks: alpha` upload
|
|
Leave everything else (production releases, store presence, financial
|
|
data, user management) unchecked. If you later automate production rollout
|
|
(`tracks: production`), come back and add **Release to production, exclude
|
|
devices, and use Play App Signing**.
|
|
6. **Invite user → Send invite**.
|
|
|
|
Propagation is usually instant, but the very first API call can take up to
|
|
~24 h after the account's first-ever manual upload; if the workflow fails
|
|
with a 401/403 right after setup, wait and re-run before changing anything.
|
|
|
|
### Step 5 — Store the JSON as a GitHub secret
|
|
|
|
Name it `PLAY_SERVICE_ACCOUNT_JSON`:
|
|
|
|
```bash
|
|
# bash / Git Bash
|
|
gh secret set PLAY_SERVICE_ACCOUNT_JSON -R tiennm99/loto < path/to/service-account.json
|
|
```
|
|
|
|
```powershell
|
|
# PowerShell — '<' redirection is NOT supported, pipe instead:
|
|
Get-Content -Raw C:\path\to\service-account.json |
|
|
gh secret set PLAY_SERVICE_ACCOUNT_JSON -R tiennm99/loto
|
|
```
|
|
|
|
The workflow is gated on this secret: if it is missing, the Play upload step
|
|
skips silently and the run still produces a GitHub Release. This means tags
|
|
work before Play setup is finished.
|
|
|
|
## Cutting a release
|
|
|
|
1. Bump **both** values in `android/app/build.gradle.kts` — Play rejects
|
|
duplicate `versionCode`s (the release workflow also fails fast if the
|
|
tag's `versionCode` is not higher than the previous release tag's):
|
|
|
|
```kotlin
|
|
versionCode = 8 // must increase every release
|
|
versionName = "0.2.1" // should match the tag
|
|
```
|
|
|
|
2. Commit, push, tag:
|
|
|
|
```bash
|
|
git add android/app/build.gradle.kts
|
|
git commit -m "chore(android): bump version to 0.2.1 (versionCode 8)"
|
|
git push origin main
|
|
git tag v0.2.1
|
|
git push origin v0.2.1
|
|
```
|
|
|
|
3. Watch and verify:
|
|
|
|
```bash
|
|
gh run list -R tiennm99/loto --workflow android-release.yml --limit 1
|
|
gh run watch <run-id> -R tiennm99/loto --exit-status
|
|
gh release view v0.0.3 -R tiennm99/loto
|
|
```
|
|
|
|
Promotion beyond closed testing (open → production) stays manual in the Play
|
|
Console UI, or change `tracks:` in `android-release.yml` to automate further.
|
|
|
|
## Closed testing → production access
|
|
|
|
`com.miti99.loto` ships from a personal developer account created after
|
|
13 Nov 2023, so Play gates production behind a closed test. Organization
|
|
accounts and personal accounts older than that date are exempt.
|
|
|
|
The gate, verbatim from Play Console Help: **at least 12 testers opted in
|
|
to your closed test, continuously, for the 14 days preceding the
|
|
application.**
|
|
|
|
What counts:
|
|
|
|
- "Opted in" means the tester accepted the invite *and* installed the
|
|
build under the matching Google account. Invited-but-never-installed
|
|
does not count.
|
|
- Real devices and genuine accounts only. Emulators and duplicate
|
|
accounts do not count.
|
|
- The 14 days must be unbroken. A tester who opts out mid-window stops
|
|
counting, and re-opting in restarts their 14-day clock. Dropping to 11
|
|
resets the window — 12 is the floor, not a safe target. Recruit spares.
|
|
- Since April 2026 Play also rejects applications for weak engagement.
|
|
The form asks whether testers exercised all features and how their
|
|
usage compared to expected production behaviour, plus a summary of
|
|
their feedback. Collect that feedback as the test runs; it is painful
|
|
to reconstruct afterwards.
|
|
|
|
Review of the production application typically takes up to 7 days.
|
|
|
|
**CI publishes straight to the closed test.** `android-release.yml`
|
|
uploads to `tracks: alpha`, so every tagged release reaches the testers
|
|
whose opt-ins count toward the 14-day window. This matters because
|
|
internal testing is a separate track that does **not** count toward the
|
|
requirement — only a closed test does. Builds tagged before v0.1.2 went
|
|
to the internal track and need promoting by hand if they are wanted in
|
|
the closed test.
|
|
|
|
An unknown track id fails the upload step with `Track(s) "..." could not
|
|
be found. Available tracks are: [...]` rather than publishing somewhere
|
|
unintended, so a typo here is loud, not silent.
|
|
|
|
## Troubleshooting
|
|
|
|
- **Play upload fails with duplicate versionCode** — `versionCode` in
|
|
`android/app/build.gradle.kts` was not bumped before tagging (the
|
|
workflow's guard step should have caught this before the build).
|
|
- **`The '<' operator is reserved for future use`** — PowerShell does not
|
|
support `<` input redirection; use the `Get-Content -Raw ... |` form above.
|
|
- **Play upload step skipped** — `PLAY_SERVICE_ACCOUNT_JSON` secret is not
|
|
set (check `gh secret list -R tiennm99/loto`), or was set after the run
|
|
started.
|
|
- **Play upload fails with 401/403** — either the service account was not
|
|
invited in Play Console (Step 4), the app was not added under its App
|
|
permissions, or the Play Developer API access has not finished propagating
|
|
(can take up to ~24 h after the first-ever manual upload). Verify the
|
|
invite, wait, then re-run the job.
|
|
- **First upload rejected** — Google requires the very first AAB to be
|
|
uploaded manually through the Play Console UI; the API can only publish
|
|
after that.
|
|
|
|
**Never commit** `*.jks`, `*.keystore`, `*.p12`, service-account JSON, or `.env`.
|