solo-android-release Β· diff
v1.0.0 to v1.1.0
68 added, 2 removed. Audit A to A.
---
name: android-release
- description: Ship an Android app to testers or to Google Play. Use when the user says "make the Android app available to testers", "internal/open testing link", "publish to Play", "release the APK/AAB", or "Play Console". Leads with the MINIMAL tester-link workflow (fastest), then the FULL production listing. Carries the exact Play Console browser steps, signing/keystore setup, the testing-track comparison, required assets, and the gotchas (lintVitalRelease crash, 16 KB alignment) so you don't re-learn them.
+ description: Ship an Android app to testers or to Google Play, or push an update to an app already listed there. Use when the user says "make the Android app available to testers", "internal/open testing link", "publish to Play", "release the APK/AAB", "Play Console", "upload a new version", or hits a Play policy deadline ("app doesn't meet target API level requirements"). Leads with the MINIMAL tester-link workflow (fastest), then the FULL production listing, then UPDATE for an existing app. Carries the exact Play Console browser steps, signing/keystore setup, the testing-track comparison, required assets, and the gotchas (lintVitalRelease crash, 16 KB alignment, target API deadline) so you don't re-learn them.
license: MIT
metadata:
author: fortunto2
- version: "1.0.0"
+ version: "1.1.0"
openclaw:
emoji: "π€"
---
# android-release β ship Android to testers, then to Play
**Start with MINIMAL** (a working tester link in minutes). Do FULL only when the user wants the
public Play listing. Sibling skill: **`ios-release`** for iPhone/TestFlight.
Golden rules:
- **Never hit final publish/"Send for review" without the user's OK** β it's outward-facing. Filling
drafts is fine; an internal-testing rollout is low-risk + reversible, so a prior "set it all up"
covers it β but state what you did.
- **Back up the keystore + password** β losing it = can't update the same Play listing.
- Browser work: **the user logs in** (never touch their password/2FA); you drive the forms after.
+ Playwright MCP runs its **own persistent Chromium profile**, NOT the user's Chrome β a Chrome that
+ is "already logged in" is invisible to it, and you can't attach unless it was started with
+ `--remote-debugging-port` (check `curl -s localhost:9222/json/version` before promising anything).
+ So: open the Google sign-in page, hand the window over, wait. The login then sticks for later sessions.
+ - **The app may live in someone else's developer account** (client/agency who granted you access).
+ `play.google.com/console/developers` shows a **"Choose developer account"** list β one Google login
+ can hold several. Before concluding "the app isn't here", check every developer account in that list,
+ not just the app list of the first one. Newly granted access appears there after a reload.
---
## Which track? (who can use the link)
| Track | Who can install via link | Setup needed |
|-------|--------------------------|--------------|
| **Internal testing** | only Google accounts on your list (β€100) | almost none β no review |
| **Closed testing** | accounts on a list or a Google Group | + content rating, data safety |
| **Open testing** | **anyone with the link** | + full store listing, content rating, target audience, data safety, **review** |
| **GitHub APK (sideload)** | **literally anyone**, no Google account | none β but "unknown sources" prompt |
Most "available to testers" asks β **Internal testing** (fast) or the **GitHub APK link** (instant,
zero-restriction). "Anyone with a link, via Play" β **Open testing** (heavier: listing + review).
---
## A. MINIMAL β get a tester link
**A1. Fastest: sideload APK via a GitHub Release** (no Play account, ~2 min, works for anyone)
```bash
JAVA_HOME=<jdk17> ./gradlew :composeApp:assembleRelease # universal signed APK
cp composeApp/build/outputs/apk/release/*-release.apk app-<ver>.apk
gh release create v<ver> app-<ver>.apk --repo <owner>/<repo> \
--title "<App> <ver> β Android tester build" --notes-file notes.md
# testers download the .apk from the release page β tap β allow "install from this source"
```
**A2. Play Internal Testing** (opt-in link, installs via Play, no review). Signed **AAB** required
(`:composeApp:bundleRelease`). Browser steps (Playwright β user logs in first):
1. `play.google.com/console` β **Create app**: name (β€30), package `com.β¦`, **Check availability**,
App, Free, tick both declarations (Program Policies + US export laws) β **Create app**.
2. **Testing β Internal testing** β **Create new release**.
3. Leave Play App Signing on ("Releases are signed by Google Play").
4. **Upload** the `.aab` (click Upload β `browser_file_upload` with the absolute path). Wait for
"optimized for distribution". Release name auto-fills; add notes inside `<en-US>β¦</en-US>`.
5. **Next β Save and publish** (confirm dialog). Benign warnings: "no deobfuscation file", "no
native debug symbols", "no testers yet".
6. **Testers** tab β tick an email list (or **Create email list** + paste emails) β **Save** β
track flips to **Active**. Copy the link: `https://play.google.com/apps/internaltest/<trackId>`.
Testers open it signed into a listed Google account, tap "Become a tester", install from Play.
**A3. Upgrade internal β open** later: **Testing β Open testing β Promote release** (or create a new
open release). Requires the FULL section below (listing + content rating) + review before it goes live.
---
## B. FULL β production / open-testing listing
Complete before "Send for review":
- **Store listing**: app name, short desc (β€80), full desc (β€4000), **app icon 512Γ512**,
**feature graphic 1024Γ500**, β₯2 phone screenshots (1080Γ1920..2160; longer side β€ 2Γ shorter).
- **App content** (all required): Privacy policy URL, Ads, App access, Content rating
(questionnaire), Target audience & children, Data safety, plus Government/Financial/Health if relevant.
- **Countries/regions** + Free/Paid.
- Production (or Open testing) track β create release β upload AAB β **Send for review** (confirm first).
- New personal accounts: Google may require **β₯12 testers for 14 days** on closed testing before
production access β plan for that.
### Order that actually unblocks "Send app for review" (Publishing overview)
The **Send app for review** button stays locked until the **Dashboard "Set up your app" checklist is
100%**. Complete, in this order:
1. **Store listing** (Grow users β Store presence β Store listings): name, short/full desc, upload
icon 512 + feature 1024Γ500 + β₯2 phone screenshots. (Tablet-screenshot slots show `*` but are NOT
required for a phone app.)
2. **Store settings** (Store presence β **Store settings**) β EASY TO MISS, it's a separate checklist
item "Select an app category and provide contact details" that needs THREE things, all required to
mark it complete: **App category** + **β₯1 Tag** (Manage tags β NOT optional; a missing tag silently
keeps the whole task incomplete and "Send app for review" locked, with no obvious error) + a
**contact Email**. Verify each value persists after save (a filled-looking field can save blank).
3. **App content** (Policy and programs β **App content β overview**) β do every "Start declaration":
Privacy policy URL Β· App access (Sign-in details) Β· Ads Β· **Content ratings** (IARC questionnaire) Β·
**Target audience** Β· **Data safety** Β· Advertising ID Β· Government apps Β· Financial features Β· Health.
Yes/No order is NOT consistent across pages β read each before clicking.
4. **Open testing track**: Countries/regions (select all β Save) β **Create release** β Add bundle
**from library** (reuse the internal AAB) β notes β Next β **Publishing overview β Send app for review**.
### Content rating (IARC) β for a utility/"All Other App Types" app
Category "All Other App Types"; email for the certificate. Answer content questions **No** (violence/
sex/language/drugs/gambling). If the app has user-posted content (photos/notes shared with others),
"User Content Sharing" = **Yes** β then the sub-questions (nudity/violence/block/report/moderation)
are all **No** for a benign app; rating stays Everyone/PEGI 3.
### Data safety β the long one
Q1 collects data? **Yes** β encrypted in transit **Yes** β account creation method (anonymous app =
"My app does not allow users to create an account") β external login **No** β data-deletion request:
**Yes** needs a "Delete data URL" (use the privacy-policy URL) or answer **No** (it's Optional).
Pick **data types** (LocationβPrecise, Photos, Personal infoβName/Email/User IDs, App activityβOther
UGC), then per type open its modal: **Collected** (your backend is not "Shared" with third parties) β
not ephemeral β **optional** ("users can choose") β purpose **App functionality** β Save.
### Browser-automation gotchas (Playwright on Play Console)
- **Angular Material radios/checkboxes ignore JS `.click()`** β must use a real `browser_click`. Target
them by the accessibility ref, or `question:has-text("<unique question text>") >> role=radio[name="No"]`.
Radio labels ("Yes"/"No") are sibling text, so `role=radio[name=...]` often has an EMPTY name β click
by the visible label text or the ref instead.
- **Sticky footer / overlay panes intercept clicks** on library-Upload / "Add" / "Create release"
buttons β click via `page.evaluate(() => document.querySelector('button[debug-id="..."]').click())`
(debug-ids: `upload-button`, `add-to-content-button`, `create-android-release-button`).
- **File upload**: side-panel Upload button β `browser_file_upload` with the absolute path β the asset
lands in the library β select it β click **Add** (footer-intercepted; JS-click it).
- **Text fields**: type with `slowly`/pressSequentially and VERIFY `input.value` before Save β a filled-
looking field can be empty (wrong element), leaving "Save" disabled or saving blank (bit me on the
contact email β took two tries).
- **Direct URLs to `/app-content/*` sub-pages redirect to Home** on hard-navigation; reach them by
clicking the in-app "App content β overview" link (SPA nav), or navigate to `/app-content/overview`.
+ Deep track URLs (`/tracks/app-bundle-explorer`, `/tracks/<id>/releases/β¦`) are worse β a cold load
+ dies with **"An unexpected error has occurred (64146155)"**. Land on `/app-dashboard` or
+ `/test-and-release`, then click through the SPA nav. Handy trick: `document.querySelectorAll('a[href]')`
+ on `/test-and-release` dumps every track URL incl. the numeric closed-testing track id.
+ - **Read the page with `browser_evaluate`, not full snapshots** β Console pages are enormous. Grab
+ `document.body.innerText` sliced around an anchor phrase, or map `[role="row"]` β `innerText`, and
+ wrap it in `new Promise(res => setTimeout(β¦, 4000-6000))` because the SPA renders after navigation
+ reports done. Cheap, and it survives the re-renders that invalidate accessibility refs.
+ - **Refs go stale constantly** (Console re-renders, plus `?pli=1` redirects on first load). Prefer
+ text-based selectors (`button:has-text("Save")`) or find-by-innerText + `.click()` inside
+ `browser_evaluate` over refs captured more than one action ago.
- **"Send app for review" locked with everything seemingly filled?** It's almost never a UI lag β
a required sub-field is silently blank. The dashboard task name understates its requirements (e.g.
"Select an app categoryβ¦" also needs **Tags**). When a checklist item won't turn green, open it and
fill EVERY field, including the ones that look optional, before assuming it's a glitch.
---
+ ## C. UPDATE β new version of an app already on Play
+
+ The common ask (policy deadline, bugfix, new build). Nothing from section B is re-done β listing and
+ declarations are already approved. Order:
+
+ 1. **Bump `versionCode`** (strictly greater than anything ever uploaded to ANY track β Play rejects
+ duplicates permanently, even for a track you never rolled out) + `versionName`. Rebuild the signed
+ AAB with the **same upload key**, or the update won't install over the existing app.
+ 2. Find the live track first: **Dashboard** tells you what's actually active (e.g. "Production β
+ Inactive" means the app has never gone to prod, no matter what the repo history suggests). Ship into
+ the track that's Active; **Test and release β Latest releases and bundles** lists them all.
+ 3. Track page β **Create new release** β **Upload** the AAB β wait ~40 s for "optimized for
+ distribution" β the table then shows **Version / API levels / Target SDK** for the new bundle next
+ to the previous one. **Read that row** β it's the cheapest proof the build is the one you meant
+ (right versionCode, right targetSdk).
+ 4. Release name auto-fills as `<code> (<name>)`; notes go inside `<en-US>β¦</en-US>`. Verify both
+ `input.value`s before continuing.
+ 5. **Next** β review screen: check the **device-support diff** ("Devices no longer supported" must be
+ 0 unless you intended it) and the warnings. Benign: *"contains native code, no debug symbols"*,
+ *"no deobfuscation file"*. Staged roll-out defaults to 100%.
+ 6. **Save** β this does NOT publish. It parks the change in **Publishing overview** ("Your change has
+ been saved⦠go to Publishing overview"). Draft stage, safe to reach without asking.
+ 7. **Publishing overview β "Submit N changes for review" β "Send changes for review"** (confirm dialog).
+ β **outward-facing: needs the user's explicit OK.** Quick checks run first (up to ~14 min), then the
+ status becomes **"Changes in review"**. Review is typically β€7 days.
+ 8. **Managed publishing off** = it goes live to that track's audience the moment review passes. If the
+ user wants to gate the moment, turn managed publishing ON *before* submitting.
+
+ **Production may be locked** even for a long-lived app: personal accounts must run a closed test with
+ **β₯12 opted-in testers for 14 days** before "Apply for production" unlocks. The Dashboard shows the
+ live counter ("N testers currently opted-in"). Until it's met, updates can only go to testing tracks β
+ say so early instead of hunting for a missing button.
+
+ ---
+
## Assets β generate what's missing
- **Play icon 512** β resize the app's 1024 icon (e.g. the iOS `AppIcon` `icon_1024.png`) β 512.
- **Feature graphic 1024Γ500** β brand gradient + icon + wordmark (PIL). Keep emoji OUT of Arial
captions (they render as tofu boxes).
- **Framed screenshots** β compose raw device captures onto a caption band; template:
`docs/store/compose_play.py` (caretta-friends). Play phone size 1080Γ2160 (longer β€ 2Γ shorter).
## Signing (once per app)
```bash
keytool -genkeypair -v -keystore keystore/upload.jks -alias <a> -keyalg RSA -keysize 2048 \
-validity 10000 -storepass β¦ -keypass β¦ -dname "CN=β¦, O=β¦, C=β¦"
```
`build.gradle.kts`: `signingConfigs { create("release") { β¦ read keystore.properties β¦ } }` +
`buildTypes.release.signingConfig`. **Gitignore** `*.jks`, `keystore/`, `keystore.properties`, `*.aab`, `*.apk`.
Play App Signing (on by default) re-signs with Google's key; your keystore is the **upload** key.
## Gotchas
- **`lintVitalRelease` crashes** on KMP/Compose ("Unexpected failure during lint analysis of
MainActivity.kt") and fails `assembleRelease` though the APK/AAB packaged fine β
`android { lint { checkReleaseBuilds = false; abortOnError = false } }`.
- **16 KB page alignment** β Play requirement for `targetSdk 35+`. Native `.so` LOAD segments must
have `p_align = 0x4000`. For MapLibre use **11.13.x** + force `androidx.graphics:graphics-path:1.0.1`.
Verify by parsing the AAB's ELF PT_LOAD headers for `p_align >= 16384`.
- **AAB β installable** β testers need an **APK** (`assembleRelease`); AAB is Play-only.
- **Package name** locks at Create-app (Check availability); it's the applicationId forever.
+ - **Target API deadline** β Play requires new uploads to target an API level within ~1 year of the
+ latest Android release, and emails "your app doesn't meet Google Play's target API level
+ requirements" ahead of the cutoff (Android 15 / API 35 β 31 Aug 2025; Android 16 / API 36 β
+ 31 Aug 2026; expect the pattern to continue). Fix = raise `compileSdk` **and** `targetSdk`, bump
+ versionCode, re-upload. An old AGP refuses to be quiet about a newer compileSdk but still builds
+ fine: `android.suppressUnsupportedCompileSdk=<sdk>` in `gradle.properties` beats an AGP upgrade as
+ the first move (verified: AGP 8.7.3 + `compileSdk 36`). Confirm the result β the merged manifest
+ under `androidApp/build/intermediates/**/AndroidManifest.xml` must say
+ `android:targetSdkVersion="<sdk>"`; don't trust the Gradle file alone.
+ - **Behaviour changes ride along with a targetSdk bump** β enforced edge-to-edge, orientation/resize
+ overrides on large screens, predictive back. Worth a device/emulator pass on a UI-heavy app before
+ submitting, and worth telling the user it's untested if there's no device at hand.