From 1774a331582bc396086f34cf7da01b5bd5aec54b Mon Sep 17 00:00:00 2001 From: Jason Ross Date: Fri, 3 Jul 2026 16:05:12 -0500 Subject: [PATCH] ci(preflight): add API 35 and API 37 emulator E2E to preflight MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend the local preflight gate from api36 (the sole latest-API GMD run) to api35 + api36, the top two stable levels in the E2E matrix. Both Gradle Managed Devices already existed in app/build.gradle.kts (the api29..36 loop) — confirmed via `:app:tasks --group verification`, no emulator run needed. API 37 (preview) was investigated but NOT added as a GMD: its only published system image is the nonstandard "android-37.0" / google_apis_ps16k pairing that ci.yml's e2e-preview job installs by hand via sdkmanager. ManagedVirtualDevice's apiLevel (Int) builds "android-" and apiPreview (codename) builds "android-" — neither produces "android-37.0", the same gap ci.yml documents as why reactivecircus/android-emulator-runner can't provision it either. docs/perf/issue-124-unified-inbox-paging.md independently corroborates this: its API 37 measurements used a physical Pixel, not an AVD. There is no api37DebugAndroidTest task to run, so it stays CI-only (e2e-preview) until a managed-device-compatible image ships; the comment above testOptions.managedDevices in app/build.gradle.kts now documents this in detail for the next person who looks. .claude/skills/preflight/SKILL.md and CLAUDE.md are updated to run both api35DebugAndroidTest and api36DebugAndroidTest as part of the required gate, with the API 37 gap called out inline. Co-Authored-By: Claude Opus 4.8 --- .claude/skills/preflight/SKILL.md | 49 ++++++++++++++++++------------- CLAUDE.md | 28 ++++++++++-------- app/build.gradle.kts | 15 ++++++++-- 3 files changed, 58 insertions(+), 34 deletions(-) diff --git a/.claude/skills/preflight/SKILL.md b/.claude/skills/preflight/SKILL.md index a64e159..dc873f9 100644 --- a/.claude/skills/preflight/SKILL.md +++ b/.claude/skills/preflight/SKILL.md @@ -1,6 +1,6 @@ --- name: preflight -description: Run LibreMail's fast CI gate locally (assembleDebug + testDebugUnitTest + compileDebugAndroidTestKotlin + lintDebug + ktlintCheck + detekt) plus the latest-API-level emulator E2E before pushing or opening a PR. Mirrors the merge gate and runs the highest-API E2E via its Gradle Managed Device; the full multi-API matrix stays CI-only. Use before treating a change as done. +description: Run LibreMail's fast CI gate locally (assembleDebug + testDebugUnitTest + compileDebugAndroidTestKotlin + lintDebug + ktlintCheck + detekt) plus the top-of-matrix emulator E2E (currently API 35 + 36) before pushing or opening a PR. Mirrors the merge gate and runs the two highest stable API levels via their Gradle Managed Devices; the rest of the multi-API matrix and the API 37 preview job stay CI-only. Use before treating a change as done. --- # /preflight @@ -13,12 +13,12 @@ Run the same fast checks CI enforces on every PR, in order, and report the outco with a JDK/AGP version mismatch, check `java -version` / `JAVA_HOME` and point it at a 17–21 JDK (e.g. Android Studio's bundled JBR) before retrying. - PowerShell: invoke the wrapper as `.\gradlew`. Git Bash / the Bash tool: `./gradlew`. -- The final E2E step boots an emulator through a Gradle Managed Device, so the host needs - hardware acceleration (WHPX on Windows, KVM on Linux, HVF on macOS). Gradle downloads the - system image and boots/tears down the AVD itself — no manual emulator setup — but the first - run is slow while the image downloads. If the host has no accelerated emulator and the - managed device cannot boot, report the E2E step as not run rather than treating the gate as - green. +- The final two E2E steps each boot an emulator through their own Gradle Managed Device, so the + host needs hardware acceleration (WHPX on Windows, KVM on Linux, HVF on macOS). Gradle + downloads the system image and boots/tears down each AVD itself — no manual emulator setup — + but the first run per API level is slow while its image downloads. If the host has no + accelerated emulator and a managed device cannot boot, report the E2E step as not run rather + than treating the gate as green. ## Steps @@ -30,7 +30,8 @@ Run these, stopping at the first failure: ./gradlew :app:compileDebugAndroidTestKotlin ./gradlew :app:lintDebug ./gradlew :app:ktlintCheck :app:detekt -./gradlew :app:api36DebugAndroidTest # latest-API emulator E2E (highest level in the matrix) +./gradlew :app:api35DebugAndroidTest # top-of-matrix emulator E2E (2nd-highest stable level) +./gradlew :app:api36DebugAndroidTest # top-of-matrix emulator E2E (highest stable level) ``` `compileDebugAndroidTestKotlin` compiles the `androidTest` source set — the E2E/instrumented @@ -44,24 +45,32 @@ merge gate even when the build and lint are green. Add `--continue` to any comma `:app:ktlintCheck :app:detekt --continue`) to collect every failure in one pass instead of stopping at the first. -`api36DebugAndroidTest` is last because it is the slowest: it runs the full instrumented/E2E -suite on `api36` — the highest API level in the E2E matrix — via its Gradle Managed Device, -which Gradle provisions, boots, and tears down automatically. This one latest-API level is the -E2E that preflight runs locally and it must pass; CI fans the same suite out across the whole -API matrix (plus the API 37 preview job). Keep `api36DebugAndroidTest` in lockstep with the -highest managed device in `app/build.gradle.kts` and the E2E matrix in -`.github/workflows/ci.yml` — when a newer API level is added there, run that one instead. +`api35DebugAndroidTest` and `api36DebugAndroidTest` run last because they are the slowest: each +runs the full instrumented/E2E suite — on `api35`, then `api36`, the top two stable levels in +the E2E matrix — via its own Gradle Managed Device, which Gradle provisions, boots, and tears +down automatically. These two levels are the E2E that preflight runs locally and both must +pass; CI fans the same suite out across the whole API matrix (API 29–36) plus the API 37 +preview job. API 37 has no Gradle Managed Device yet — its only published system image is the +nonstandard `android-37.0` / `google_apis_ps16k` pairing, which neither `ManagedVirtualDevice`'s +`apiLevel` (Int) nor `apiPreview` (codename) DSL resolves (see the comment above +`testOptions.managedDevices` in `app/build.gradle.kts`) — so CI's `e2e-preview` job provisions +it by hand instead, and there is no `api37DebugAndroidTest` task to run here. Keep +`api35DebugAndroidTest` / `api36DebugAndroidTest` in lockstep with the top of the managed-device +list in `app/build.gradle.kts` and the E2E matrix in `.github/workflows/ci.yml` — when a newer +API level is added there, run the new top two instead. ## Reporting -- If everything passes, say so plainly (e.g. "preflight green: build, unit tests, lint, ktlint, detekt, api36 E2E"). +- If everything passes, say so plainly (e.g. "preflight green: build, unit tests, lint, ktlint, detekt, api35+api36 E2E"). - On failure, surface the actual Gradle error and point at the relevant report: - unit tests → `app/build/reports/tests/testDebugUnitTest/` - lint → `app/build/reports/lint-results-debug.html` - ktlint → `app/build/reports/ktlint/` (per source set, e.g. `ktlintTestSourceSetCheck/`) - detekt → `app/build/reports/detekt/` - - E2E → `app/build/reports/androidTests/managedDevice/` (per-device HTML, e.g. `.../api36/`) -- Run only the **latest-API** E2E here (`api36DebugAndroidTest`, the highest level in the - matrix); the full multi-API matrix and the API 37 preview job stay CI's job. If the host has - no accelerated emulator and the managed device cannot boot, report that E2E could not run + - E2E → `app/build/reports/androidTests/managedDevice/` (per-device HTML, e.g. `.../api35/`, + `.../api36/`) +- Run only the **top two stable-API** levels here (`api35DebugAndroidTest` + + `api36DebugAndroidTest`); the rest of the multi-API matrix and the API 37 preview job stay + CI's job — API 37 has no Gradle Managed Device to run locally (see Steps above). If the host + has no accelerated emulator and a managed device cannot boot, report that E2E could not run rather than treating the gate as green. diff --git a/CLAUDE.md b/CLAUDE.md index 171d3af..e7c3a2e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -28,15 +28,18 @@ or via Gradle Managed Devices `./gradlew e2eGroupDebugAndroidTest` (whole matrix **Before treating a change as done**, run the fast CI gate: `assembleDebug` + `testDebugUnitTest` + `compileDebugAndroidTestKotlin` + `lintDebug` + `ktlintCheck` + -`detekt` + the latest-API emulator E2E `api36DebugAndroidTest` (the `/preflight` skill does -all of this). `compileDebugAndroidTestKotlin` compiles the `androidTest` source set that the -static part of the gate skips, catching E2E/instrumented-test compile errors before they -surface only in CI. `ktlintCheck`/`detekt` cover the `test`/`androidTest` source sets that -`lintDebug` skips, so they catch style violations that would otherwise fail CI's Static -analysis gate. `api36DebugAndroidTest` runs the instrumented/E2E suite on API 36 — the highest -API level in the E2E matrix — via its Gradle Managed Device (Gradle boots and tears down the -emulator automatically). Running that one latest-API level locally is required; the full -multi-API matrix (and the API 37 preview job) stays CI's job. +`detekt` + the top-of-matrix emulator E2E `api35DebugAndroidTest` + `api36DebugAndroidTest` +(the `/preflight` skill does all of this). `compileDebugAndroidTestKotlin` compiles the +`androidTest` source set that the static part of the gate skips, catching E2E/instrumented-test +compile errors before they surface only in CI. `ktlintCheck`/`detekt` cover the +`test`/`androidTest` source sets that `lintDebug` skips, so they catch style violations that +would otherwise fail CI's Static analysis gate. `api35DebugAndroidTest` and +`api36DebugAndroidTest` run the instrumented/E2E suite on the top two stable API levels in the +E2E matrix, each via its own Gradle Managed Device (Gradle boots and tears down each emulator +automatically). Running those two levels locally is required; the rest of the multi-API matrix +(API 29–34) stays CI's job. API 37 (preview) still has no Gradle Managed Device to run locally +— see the comment above `testOptions.managedDevices` in `app/build.gradle.kts` for why — so it +remains exercised only by CI's `e2e-preview` job. ## Build-config gotchas @@ -70,9 +73,10 @@ pulled in as a real dependency for unit tests because `android.jar`'s version is A change is not done until it ships with passing **unit tests** and **E2E/instrumented tests** that exercise the new or changed behaviour. Writing and committing that E2E/instrumented test is a required part of every task — and the test must actually **run and pass**, not merely -compile: preflight runs the latest-API-level emulator E2E locally (`api36DebugAndroidTest`, the -highest API level in the E2E matrix and its Gradle Managed Device task) and it must be green -before the change is done. CI then runs the full multi-API matrix. +compile: preflight runs the top-of-matrix emulator E2E locally (`api35DebugAndroidTest` + +`api36DebugAndroidTest`, the two highest stable API levels in the E2E matrix and their Gradle +Managed Device tasks) and both must be green before the change is done. CI then runs the full +multi-API matrix plus the API 37 preview job. ## Repo etiquette diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b4d6630..24807e1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -146,8 +146,19 @@ android { // `./gradlew api29DebugAndroidTest`. Gradle provisions/boots/tears down the emulators and // downloads the system images on first use. Keep this list in lockstep with the CI matrix in // .github/workflows/ci.yml; when a new Android ships, add it and drop the oldest level that - // has fallen outside ~7 years. API 37 (preview) is exercised on the dev emulator until a - // stable managed-device image is published, so it is intentionally not listed here. + // has fallen outside ~7 years. + // + // API 37 (preview) is intentionally NOT listed here (re-confirmed 2026-07, see PR that added + // API 35/37 to preflight): its only published system image is the nonstandard + // "android-37.0" / google_apis_ps16k pairing that the e2e-preview job in + // .github/workflows/ci.yml installs directly via sdkmanager. ManagedVirtualDevice only knows + // how to build an "android-" package id (e.g. `apiLevel = 37` → "android-37") + // or an "android-" one — neither produces "android-37.0" — so there is + // no DSL path to this image today, the same root cause documented on e2e-preview for why + // reactivecircus/android-emulator-runner can't provision it either. issue #124's perf doc + // (docs/perf/issue-124-unified-inbox-paging.md) independently corroborates this: its API 37 + // cross-check used a physical Pixel, not an emulator/AVD. Once a managed-device-compatible + // image is published, add `api37` here (and to the CI matrix) and delete the e2e-preview job. managedDevices { localDevices { listOf(29, 30, 31, 32, 33, 34, 35, 36).forEach { api ->