feat(release): target API 36 and upload each tag's bundle to Google Play (M8 step 1) #60

Merged
whitlocktech merged 1 commits from feat/m8-google-play into edge 2026-10-26 09:17:58 -05:00
Member

Step 1 of M8 (docs/android/PLAN.md item 9). Docs: RunicGateway/docs#376 (opened alongside).

What changes

  • Target API 36, which Play has required since 2026-08-31. AGP 8.6.1 can't build against 36, so AGP moves to 8.13.2, the last 8.x (9.x changes the DSL). The wrapper moves to Gradle 8.14.3. Kotlin, KSP and Hilt are unchanged. All three workflows now install platforms;android-36. AGP 8.13 still defaults to build-tools 35.0.0.
  • App bundle. release.yml also runs bundleRelease, signed with the same keystore. The .aab goes on the Gitea release beside the APK, with a line in the changelog footer saying it doesn't install on a phone. This is also where the hand-uploaded first bundle comes from.
  • Play upload. A last step uploads the bundle to the internal track through the Play Developer API v3 edit flow: open an edit, upload the bundle, set the track, commit. The OAuth token comes from a service-account JWT signed with openssl, using only curl, jq and openssl, with no Gradle plugin. The key comes from the new secret PLAY_SERVICE_ACCOUNT_JSON. While that secret is unset, the step warns and skips. The step runs after the Gitea release, so a Play failure fails the run but never holds back the APK.

One choice to check: the AAB is attached to the public Gitea release rather than kept as a private Actions artifact. Nothing in the org uses artifact actions yet, and the bundle is the same signed build as the APK.

How it was checked

  • PR-gate build passes locally: testDebugUnitTest lintDebug assembleDebug.

  • I built a release APK and bundle with a throwaway keystore through the CI env vars, since the real keystore isn't on this machine. Both carry the same certificate, targetSdkVersion 36, versionCode 601.

  • I ran the Play step from release.yml in ubuntu:latest against a mock of the token and Play endpoints. The mock checked the JWT signature against the key's public half. All six cases behaved: success, a rejected token, a refused upload, a refused commit, a mismatched versionCode, and an unset secret. Google's error body is printed, and the key file is removed every time.

  • Walked on an Android 16 emulator (API 36.1): connect, sign in as admin, save notification settings, 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 and connects.

  • Not walked:

    • the push foreground service, because the walk site has no ntfy configured;
    • a signed-in release build, because there is no local HTTPS site.

    The step-2 internal-track install covers both.

Before CI can upload (by hand):

  1. Create a Google Cloud service account with a JSON key, and enable the Play Android Developer API.
  2. In Play Console → Users and permissions, invite the service account with release rights on this app only.
  3. Add the whole JSON file as the PLAY_SERVICE_ACCOUNT_JSON secret.
  4. Upload the first bundle and roll it out by hand (step 2). Until then, Play rejects CI's release with "Only releases with status draft may be created on draft app".
  • AI-assisted: written with Claude Code (Claude Opus 5.5).

🤖 Generated with Claude Code

https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY

Step 1 of M8 (`docs/android/PLAN.md` item 9). Docs: RunicGateway/docs#376 (opened alongside). **What changes** - **Target API 36**, which Play has required since 2026-08-31. AGP 8.6.1 can't build against 36, so AGP moves to **8.13.2**, the last 8.x (9.x changes the DSL). The wrapper moves to **Gradle 8.14.3**. Kotlin, KSP and Hilt are unchanged. All three workflows now install `platforms;android-36`. AGP 8.13 still defaults to build-tools 35.0.0. - **App bundle.** `release.yml` also runs `bundleRelease`, signed with the same keystore. The `.aab` goes on the Gitea release beside the APK, with a line in the changelog footer saying it doesn't install on a phone. This is also where the hand-uploaded first bundle comes from. - **Play upload.** A last step uploads the bundle to the **internal** track through the Play Developer API v3 edit flow: open an edit, upload the bundle, set the track, commit. The OAuth token comes from a service-account JWT signed with `openssl`, using only curl, jq and openssl, with no Gradle plugin. The key comes from the new secret **`PLAY_SERVICE_ACCOUNT_JSON`**. While that secret is unset, the step **warns and skips**. The step runs after the Gitea release, so a Play failure fails the run but never holds back the APK. **One choice to check:** the AAB is attached to the public Gitea release rather than kept as a private Actions artifact. Nothing in the org uses artifact actions yet, and the bundle is the same signed build as the APK. **How it was checked** - PR-gate build passes locally: `testDebugUnitTest lintDebug assembleDebug`. - I built a release APK and bundle with a **throwaway keystore** through the CI env vars, since the real keystore isn't on this machine. Both carry the same certificate, `targetSdkVersion 36`, versionCode 601. - I ran the Play step from `release.yml` in `ubuntu:latest` against a mock of the token and Play endpoints. The mock checked the JWT signature against the key's public half. All six cases behaved: success, a rejected token, a refused upload, a refused commit, a mismatched versionCode, and an unset secret. Google's error body is printed, and the key file is removed every time. - Walked on an **Android 16 emulator (API 36.1)**: connect, sign in as admin, save notification settings, 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 and connects. - **Not walked:** - the push foreground service, because the walk site has no ntfy configured; - a signed-in release build, because there is no local HTTPS site. The step-2 internal-track install covers both. **Before CI can upload (by hand):** 1. Create a Google Cloud service account with a JSON key, and enable the Play Android Developer API. 2. In Play Console → Users and permissions, invite the service account with release rights on this app only. 3. Add the whole JSON file as the `PLAY_SERVICE_ACCOUNT_JSON` secret. 4. Upload the first bundle and roll it out by hand (step 2). Until then, Play rejects CI's release with "Only releases with status draft may be created on draft app". - [x] AI-assisted: written with Claude Code (Claude Opus 5.5). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
wtclaude added 1 commit 2026-10-23 07:04:42 -05:00
feat(release): target API 36 and upload each tag's bundle to Google Play
All checks were successful
PR Checks / android-build (pull_request) Successful in 21m22s
7a8231ba9c
Step 1 of M8 (docs/android/PLAN.md item 9).

- compileSdk/targetSdk 36 (Play's floor since 2026-08-31). AGP 8.6.1
  cannot build against 36, so AGP goes to 8.13.2 (the last 8.x) with the
  Gradle 8.14.3 wrapper. Kotlin, KSP and Hilt are unchanged. All three
  workflows install platforms;android-36; AGP 8.13 still defaults to
  build-tools 35.0.0.
- release.yml also runs bundleRelease, signed with the same keystore,
  and attaches the .aab to the Gitea release beside the APK (the first
  bundle has to be uploaded by hand, and this is where it comes from).
- A last step uploads the bundle to Play's internal track through the
  Play Developer API v3 edit flow. The OAuth token comes from a
  service-account JWT signed with openssl; the key is the new
  PLAY_SERVICE_ACCOUNT_JSON secret. While that secret is unset, the
  step warns and skips. The step runs after the Gitea release, so a
  Play failure never holds back the APK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
whitlocktech merged commit 1579a1982c into edge 2026-10-26 09:17:58 -05:00
whitlocktech deleted branch feat/m8-google-play 2026-10-26 09:17:58 -05:00
Sign in to join this conversation.
No description provided.