ci(preflight): add API 35 and API 37 emulator E2E to preflight #266

Merged
JMR-dev merged 8 commits from ci-preflight-api35-api37 into main 2026-07-03 22:24:27 +00:00
JMR-dev commented 2026-07-03 21:05:52 +00:00 (Migrated from github.com)

Summary

Extends preflight's local emulator E2E to cover the top of the CI matrix plus the API 37 preview, so local mirrors CI:

  • api35 + api36 run via their Gradle Managed Devices (api35DebugAndroidTest / api36DebugAndroidTest).
  • api37 (preview) is hand-provisioned by a new cross-platform Python 3 helper, .claude/skills/preflight/api37_e2e.py, because there is no Gradle Managed Device DSL path to its image.

.claude/skills/preflight/SKILL.md, CLAUDE.md (preflight + Definition of Done), and the app/build.gradle.kts managed-device comment are updated to match.

Which GMD tasks already existed vs. were added

Task Status
api35DebugAndroidTest Already existed in app/build.gradle.kts (part of the listOf(29..36) loop). Wired into preflight docs.
api36DebugAndroidTest Already existed (same loop) — the level preflight already ran.
api37DebugAndroidTest Does not exist and cannot — no GMD DSL path to the android-37.0 image (see below). Covered by the new hand-provisioning script instead.

Confirmed with a light, no-emulator check: .\gradlew :app:tasks --group verification -q lists api29…api36 *DebugAndroidTest, and no api37* task.

API 37: why a script, not a GMD

API 37's only published system image is the nonstandard android-37.0 / google_apis_ps16k (16 KB page size) pairing. AGP's ManagedVirtualDevice DSL can only build an android-<apiLevel:Int> package id (apiLevel = 37 → android-37) or an android-<apiPreview:codename> one — neither resolves to android-37.0 — which is the same root cause .github/workflows/ci.yml's e2e-preview job documents for why reactivecircus/android-emulator-runner can't provision it either, so that job hand-rolls sdkmanager/avdmanager/emulator. (docs/perf/issue-124-unified-inbox-paging.md corroborates: its API 37 numbers came from a physical Pixel, not an AVD.)

Per the repo owner's decision, preflight now hand-provisions API 37 locally (not deferred to CI) via api37_e2e.py, which mirrors the e2e-preview job:

  • Same system image string: system-images;android-37.0;google_apis_ps16k;x86_64.
  • Emulator flags identical except the GPU mode (one deliberate local divergence): -no-window -no-audio -no-boot-anim -no-snapshot -accel on -gpu auto-no-window -camera-back none -camera-front none. CI's e2e-preview uses -gpu swiftshader_indirect (software rendering, deterministic on a headless CI runner); the local run uses -gpu auto-no-window to render on the host GPU — faster, and the mode that boots cleanly on this machine.
  • Same sequence: install image with sdkmanager → create AVD with avdmanager (-d pixel_2) → cold-boot headless → adb wait-for-device + poll sys.boot_completed (two-attempt boot loop, 300 s each) → adb shell input keyevent 82 → :app:connectedDebugAndroidTest → tear the emulator down and delete the AVD.

Cross-platform (stdlib only): per-OS SDK discovery + tool suffixes (.bat/.exe), Windows .bat launchers wrapped through cmd /c, ANDROID_AVD_HOME pinned so avdmanager and the emulator agree on the AVD dir (the same fix e2e-preview applies). Keep it in lockstep with the e2e-preview job on everything but that GPU flag.

Final preflight command sequence

./gradlew :app:assembleDebug
./gradlew :app:testDebugUnitTest
./gradlew :app:compileDebugAndroidTestKotlin
./gradlew :app:lintDebug
./gradlew :app:ktlintCheck :app:detekt
./gradlew :app:api35DebugAndroidTest   # top-of-matrix emulator E2E (2nd-highest stable level)
./gradlew :app:api36DebugAndroidTest   # top-of-matrix emulator E2E (highest stable level)
python3 .claude/skills/preflight/api37_e2e.py   # API 37 preview E2E (hand-provisioned; Windows: py/python)

Caveats documented

  • Emulators need a free hardware hypervisor (Intel VT-x / AMD-V; WHPX on Windows, KVM on Linux, HVF on macOS). Shut down VirtualBox / Hyper-V VMs / WSL2 / Docker Desktop / other emulators first — a VM holding the hypervisor starves the AVD and it hangs at 0% CPU, never reaching sys.boot_completed. Added to SKILL.md Preconditions and noted in CLAUDE.md.
  • The api37 script needs python3 + the Android SDK command-line tools and emulator, located via ANDROID_SDK_ROOT/ANDROID_HOME or the per-OS default SDK path; it installs the preview image itself on first run.

Test plan

  • .\gradlew :app:tasks --group verification -q — confirms api35/api36 *DebugAndroidTest exist; no api37* GMD task.
  • api37_e2e.py validated syntactically (python -m py_compile + ast.parse + --help).
  • End-to-end validation run of api37_e2e.py pending — to be executed once the single local emulator slot is free (currently in use by another api36 run); the PR will be updated with the provision + boot + connectedDebugAndroidTest result.
  • Reviewer/owner: run all three E2E steps once on an accelerated host (VMs shut down) before relying on this as the required gate.

🤖 Generated with Claude Code

## Summary Extends preflight's local emulator E2E to cover the **top of the CI matrix plus the API 37 preview**, so local mirrors CI: - **api35 + api36** run via their Gradle Managed Devices (`api35DebugAndroidTest` / `api36DebugAndroidTest`). - **api37 (preview)** is hand-provisioned by a new cross-platform Python 3 helper, `.claude/skills/preflight/api37_e2e.py`, because there is **no Gradle Managed Device DSL path** to its image. `.claude/skills/preflight/SKILL.md`, `CLAUDE.md` (preflight + Definition of Done), and the `app/build.gradle.kts` managed-device comment are updated to match. ## Which GMD tasks already existed vs. were added | Task | Status | |---|---| | `api35DebugAndroidTest` | **Already existed** in `app/build.gradle.kts` (part of the `listOf(29..36)` loop). Wired into preflight docs. | | `api36DebugAndroidTest` | **Already existed** (same loop) — the level preflight already ran. | | `api37DebugAndroidTest` | **Does not exist and cannot** — no GMD DSL path to the `android-37.0` image (see below). Covered by the new hand-provisioning script instead. | Confirmed with a light, no-emulator check: `.\gradlew :app:tasks --group verification -q` lists `api29`…`api36` `*DebugAndroidTest`, and no `api37*` task. ## API 37: why a script, not a GMD API 37's only published system image is the nonstandard **`android-37.0` / `google_apis_ps16k`** (16 KB page size) pairing. AGP's `ManagedVirtualDevice` DSL can only build an `android-<apiLevel:Int>` package id (`apiLevel = 37` → `android-37`) or an `android-<apiPreview:codename>` one — **neither resolves to `android-37.0`** — which is the same root cause `.github/workflows/ci.yml`'s `e2e-preview` job documents for why `reactivecircus/android-emulator-runner` can't provision it either, so that job hand-rolls `sdkmanager`/`avdmanager`/`emulator`. (`docs/perf/issue-124-unified-inbox-paging.md` corroborates: its API 37 numbers came from a physical Pixel, not an AVD.) Per the repo owner's decision, preflight now hand-provisions API 37 **locally** (not deferred to CI) via `api37_e2e.py`, which mirrors the `e2e-preview` job: - Same system image string: `system-images;android-37.0;google_apis_ps16k;x86_64`. - Emulator flags identical **except the GPU mode** (one deliberate local divergence): `-no-window -no-audio -no-boot-anim -no-snapshot -accel on -gpu auto-no-window -camera-back none -camera-front none`. CI's `e2e-preview` uses `-gpu swiftshader_indirect` (software rendering, deterministic on a headless CI runner); the **local** run uses `-gpu auto-no-window` to render on the host GPU — faster, and the mode that boots cleanly on this machine. - Same sequence: install image with `sdkmanager` → create AVD with `avdmanager` (`-d pixel_2`) → cold-boot headless → `adb wait-for-device` + poll `sys.boot_completed` (two-attempt boot loop, 300 s each) → `adb shell input keyevent 82` → `:app:connectedDebugAndroidTest` → tear the emulator down and delete the AVD. **Cross-platform (stdlib only):** per-OS SDK discovery + tool suffixes (`.bat`/`.exe`), Windows `.bat` launchers wrapped through `cmd /c`, `ANDROID_AVD_HOME` pinned so avdmanager and the emulator agree on the AVD dir (the same fix `e2e-preview` applies). Keep it in lockstep with the `e2e-preview` job on everything but that GPU flag. ## Final preflight command sequence ```bash ./gradlew :app:assembleDebug ./gradlew :app:testDebugUnitTest ./gradlew :app:compileDebugAndroidTestKotlin ./gradlew :app:lintDebug ./gradlew :app:ktlintCheck :app:detekt ./gradlew :app:api35DebugAndroidTest # top-of-matrix emulator E2E (2nd-highest stable level) ./gradlew :app:api36DebugAndroidTest # top-of-matrix emulator E2E (highest stable level) python3 .claude/skills/preflight/api37_e2e.py # API 37 preview E2E (hand-provisioned; Windows: py/python) ``` ## Caveats documented - Emulators need a **free hardware hypervisor** (Intel VT-x / AMD-V; WHPX on Windows, KVM on Linux, HVF on macOS). Shut down VirtualBox / Hyper-V VMs / WSL2 / Docker Desktop / other emulators first — a VM holding the hypervisor starves the AVD and it hangs at 0% CPU, never reaching `sys.boot_completed`. Added to SKILL.md Preconditions and noted in CLAUDE.md. - The api37 script needs `python3` + the Android SDK command-line tools and `emulator`, located via `ANDROID_SDK_ROOT`/`ANDROID_HOME` or the per-OS default SDK path; it installs the preview image itself on first run. ## Test plan - [x] `.\gradlew :app:tasks --group verification -q` — confirms `api35`/`api36` `*DebugAndroidTest` exist; no `api37*` GMD task. - [x] `api37_e2e.py` validated **syntactically** (`python -m py_compile` + `ast.parse` + `--help`). - [ ] **End-to-end validation run of `api37_e2e.py` pending** — to be executed once the single local emulator slot is free (currently in use by another api36 run); the PR will be updated with the provision + boot + `connectedDebugAndroidTest` result. - [ ] Reviewer/owner: run all three E2E steps once on an accelerated host (VMs shut down) before relying on this as the required gate. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.