Merge pull request 'docs(android): M8 step 1 built — API 36, the bundle and the Play upload' (#376) from docs/android-m8-app-ci into main

Reviewed-on: #376
This commit is contained in:
2026-10-23 21:42:39 +00:00

View File

@@ -494,7 +494,7 @@ Native, Android-only:
| Live feed | OkHttp SSE (`EventSource`) for `/public/shard/stream` |
| Images | Coil |
| Min SDK | **Android 10 (API 29)** — ~95% device reach with a modern baseline (biometric, storage, TLS) and no compat shims |
| Target/compile SDK | Latest stable (35) |
| Target/compile SDK | Latest stable (36, Android 16: Google Play's floor since 2026-08-31; M8) |
| Telemetry | **None in v1** — no crash/analytics SDK (privacy-first). Revisit self-hosted crash reporting later. |
| Localization | **Strings externalized from day one** (`res/values/strings.xml`); English is the only bundled locale, but the structure invites community translations. No hardcoded UI strings. |
| Web hand-off | Chrome Custom Tabs — opens the website for registration / invite / password reset / SSO (§4.2) |
@@ -966,6 +966,30 @@ push, and Play (M6–M8) follow the designed app.
**Order of work:**
1. **App + CI** (one PR into `edge`): API 36 + AGP bump, AAB, Play upload step. Walked on the
emulator and the S22 before the PR.
**Built 2026-10-23.** AGP 8.13.2 (the last 8.x; 9.x changes the DSL) with the Gradle 8.14.3
wrapper; Kotlin, KSP and Hilt unchanged. AGP 8.13 still defaults to build-tools 35.0.0, so
the workflows only add `platforms;android-36`. The code had none of Android 16's hotspots:
no edge-to-edge opt-out, no `onBackPressed` override (Navigation Compose takes predictive
back), no orientation lock. The bundle goes on the Gitea release as well, which is where
step 2's hand upload takes it from. Checks: the debug gate (unit tests, lint, debug build)
passes; a release APK and bundle built with a throwaway keystore carry the same
certificate, `targetSdkVersion 36`, versionCode 601; the Play step ran against a mock of
the token and Play endpoints in `ubuntu:latest` (JWT signature checked against the key's
public half; success, a rejected token, a refused upload, a refused commit, a mismatched
versionCode, and an unset secret). Walked on an Android 16 emulator (API 36.1): connect,
sign in as admin, notification settings saved, edge-swipe back, Rust servers, Dashboard,
no crash; the minified release build refuses `http` and survives a TLS round trip. On the
`s22_ultra` emulator (API 35) the build updates in place, connects and opens the menu. Not
walked: the push foreground service (the walk site has no ntfy), and a signed-in release
build (no local HTTPS site); the internal-track install in step 2 covers both.
**Before the first CI upload (org lead, by hand):** in Google Cloud, a service account
with a JSON key, and the Google Play Android Developer API enabled; in Play Console →
Users and permissions, invite the service account's address with release rights on this
app only; then the whole JSON file as the `PLAY_SERVICE_ACCOUNT_JSON` secret on
`RunicGateway/Android-app`. Until a release has been rolled out by hand (step 2), Play
refuses CI's upload with "Only releases with status draft may be created on draft app".
2. **First bundle**, uploaded by hand from the next tag, with the signing key enrolled.
3. **Console declarations**: target audience 18+ (runicgateway.com D31), advertising ID none,
data safety from runicgateway.com's generated `PLAY_DATA_SAFETY.md`, and sign-in details
@@ -2051,6 +2075,14 @@ repos use), on a bare `ubuntu:latest` container.
Gitea release (mirrors `link/`'s release job). The **keystore is a base64 Gitea Actions secret**
decoded in CI; store/key passwords are secrets. The keystore never lives in the repo. Keep the
signing identity stable from the first release (Play later requires consistency).
- **Google Play** (M8, same workflow): the keystore also signs an app bundle (`bundleRelease`),
attached to the Gitea release beside the APK. After the Gitea release exists, the bundle goes to
Play's **internal** track through the Play Developer API v3 edit flow (open an edit → upload the
bundle → set the internal track → commit), with an OAuth token from a service-account JWT signed
by `openssl`. The key is the `PLAY_SERVICE_ACCOUNT_JSON` secret; while it is unset, the step
warns and skips. The step runs last, so a Play failure fails the run without holding back the
APK. Play rejects a `completed` release on an app that has never rolled one out ("Only releases
with status draft may be created on draft app"); the first bundle is therefore uploaded by hand.
- Semantic `versionName` + monotonic `versionCode`; tag releases.
### 12.1 Code quality — SonarQube (`Runic-Gateway-Android-app`)
@@ -2084,7 +2116,7 @@ Compose/nav cognitive-complexity + hoisted-callback param-count smells (idiomati
**Decided (recorded here for context):** single shard per install (§3); native auth is
password+TOTP only, with registration/invite/reset/SSO **handled by the website** (§4); **password
reset built on backend + web first**, before app work (§8); minSdk 29, compile/target 35 (§2); no
reset built on backend + web first**, before app work (§8); minSdk 29, compile/target 36 (§2, raised from 35 at M8); no
telemetry in v1 (§2); strings externalized from day one, English-only bundled (§2); **text-only** game
data in v1, pretty paperdoll is future (§6.3); **no offline cache in v1** (§7); push via self-hosted
ntfy / UnifiedPush (§11) with the **distributor embedded in the app — no second app to install**