ci(preflight): hand-provision API 37 preview E2E locally

Per the repo owner's decision, preflight now runs the API 37 preview
emulator locally instead of leaving it to CI. Since there is no Gradle
Managed Device DSL path to the nonstandard android-37.0 /
google_apis_ps16k image, add a stdlib-only, cross-platform Python 3
helper (.claude/skills/preflight/api37_e2e.py) that mirrors CI's
e2e-preview job EXACTLY: same system image string
(system-images;android-37.0;google_apis_ps16k;x86_64), same emulator
flags, same provisioning/boot sequence. It installs the image via
sdkmanager, creates the AVD via avdmanager, cold-boots headless, waits
for sys.boot_completed, runs :app:connectedDebugAndroidTest, then tears
the emulator + AVD down. Cross-platform: per-OS tool discovery/suffixes
and cmd /c wrapping for Windows .bat launchers.

Update SKILL.md + CLAUDE.md so preflight runs api35 + api36 (GMDs) +
api37 (this script), and the app/build.gradle.kts managed-devices
comment now points at the script. Add a caveat that emulators need a
free hardware hypervisor (VT-x/WHPX) — shut down VirtualBox/other VMs
first or the AVD hangs at 0% CPU.

Validated syntactically only (python -m py_compile + ast.parse +
argparse --help); no emulator was booted and no build was run, to avoid
contending with an in-progress api36 run.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-03 16:22:31 -05:00
co-authored by Claude Opus 4.8
parent 1774a33158
commit 5ecc2401e3
4 changed files with 345 additions and 45 deletions
+45 -27
View File
@@ -1,6 +1,6 @@
---
name: preflight
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.
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 via Gradle Managed Devices, then API 37 preview via the hand-provisioning script) before pushing or opening a PR. Mirrors the merge gate; the rest of the multi-API matrix stays CI-only. Use before treating a change as done.
---
# /preflight
@@ -13,12 +13,20 @@ 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 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.
- The final three E2E steps each boot an emulator, so the host needs a **free hardware
hypervisor** (Intel VT-x / AMD-V, exposed as WHPX on Windows, KVM on Linux, HVF on macOS).
Shut down VirtualBox, Hyper-V-based VMs, WSL2, Docker Desktop, or any other emulator first — a
VM holding the hypervisor starves the AVD, and it hangs at 0% CPU and never reaches
`sys.boot_completed`. The api35/api36 steps run through Gradle Managed Devices (Gradle
downloads the image and boots/tears down each AVD itself); the api37 step is hand-provisioned
by `api37_e2e.py` (see Steps). The first run per API level is slow while its system image
downloads. If the host has no accelerated emulator and a device cannot boot, report the E2E
step as not run rather than treating the gate as green.
- The api37 step is a stdlib-only, cross-platform **Python 3** script and needs `python3` plus
the Android SDK command-line tools (`sdkmanager`/`avdmanager`) and `emulator` on the machine,
located via `ANDROID_SDK_ROOT`/`ANDROID_HOME` (or the per-OS default:
`%LOCALAPPDATA%\Android\Sdk` on Windows, `~/Library/Android/sdk` on macOS, `~/Android/Sdk` on
Linux). The script installs the preview system image itself on first run.
## Steps
@@ -32,6 +40,7 @@ Run these, stopping at the first failure:
./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 # api37 preview E2E (hand-provisioned; on Windows: py or python)
```
`compileDebugAndroidTestKotlin` compiles the `androidTest` source set — the E2E/instrumented
@@ -45,32 +54,41 @@ 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.
`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
The three E2E steps run last because they are the slowest. `api35DebugAndroidTest` and
`api36DebugAndroidTest` run the full instrumented/E2E suite on `api35`, then `api36` — the top
two stable levels in the E2E matrix — each via its own Gradle Managed Device, which Gradle
provisions, boots, and tears down automatically.
`api37_e2e.py` then runs the same suite on the **API 37 preview** emulator. API 37 has no Gradle
Managed Device — 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 there is no `api37DebugAndroidTest` task. The script hand-provisions
it exactly the way CI's `e2e-preview` job does — installs the image with `sdkmanager`, creates
the AVD with `avdmanager`, cold-boots it headless with the same emulator flags, waits for
`sys.boot_completed`, runs `:app:connectedDebugAndroidTest`, then kills the emulator and deletes
the AVD. Because it mirrors `e2e-preview`, local preflight == CI on API 37.
All three levels are the E2E that preflight runs locally and all three must pass; CI fans the
same suite out across the whole matrix (API 29–36 in `e2e`, plus API 37 in `e2e-preview`). 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.
list in `app/build.gradle.kts`, and keep `api37_e2e.py` in lockstep with the `e2e-preview` job in
`.github/workflows/ci.yml` (same image string + emulator flags) — when a newer API level is added
there, run the new top levels instead.
## Reporting
- If everything passes, say so plainly (e.g. "preflight green: build, unit tests, lint, ktlint, detekt, api35+api36 E2E").
- If everything passes, say so plainly (e.g. "preflight green: build, unit tests, lint, ktlint, detekt, api35+api36+api37 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. `.../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.
- api35/api36 E2E → `app/build/reports/androidTests/managedDevice/` (per-device HTML, e.g.
`.../api35/`, `.../api36/`)
- api37 E2E → `app/build/reports/androidTests/connected/` (the `connectedDebugAndroidTest`
report the script drives); the emulator's own boot log is at the temp path the script prints.
- Run the **top two stable-API** levels (`api35DebugAndroidTest` + `api36DebugAndroidTest`) plus
the **API 37 preview** via `api37_e2e.py`; the rest of the multi-API matrix (API 29–34) stays
CI's job. If the host has no accelerated emulator and a device cannot boot (see the hypervisor
note in Preconditions), report that E2E could not run rather than treating the gate as green.
+275
View File
@@ -0,0 +1,275 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-3.0-or-later
"""Hand-provision an API 37 (Android 17, preview) emulator, run LibreMail's instrumented/E2E
suite against it, then tear the emulator and AVD down. Invoked by the /preflight skill as the
third (api37) E2E step, alongside the api35/api36 Gradle Managed Devices.
WHY THIS SCRIPT EXISTS
----------------------
API 37's only published system image is the nonstandard "android-37.0" / google_apis_ps16k
(16 KB page size) pairing. AGP's Gradle Managed Device 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" -- so there is NO
api37DebugAndroidTest task to run. This script custom-provisions the emulator with
sdkmanager / avdmanager / emulator directly, mirroring CI's `e2e-preview` job in
.github/workflows/ci.yml EXACTLY (same system image string, same emulator flags, same
provisioning/boot sequence) so that local preflight == CI. Keep the two in lockstep: when a
stable, GMD-compatible API 37 image ships, delete this script, fold api37 into
testOptions.managedDevices, and fold 37 into CI's `e2e` matrix (dropping the `e2e-preview` job).
Pure standard library, cross-platform (Windows / Linux / macOS): tool paths and executable
suffixes are resolved per-OS, and `.bat` launchers are wrapped through `cmd /c` on Windows.
HYPERVISOR REQUIREMENT
----------------------
The emulator boots with `-accel on`, so it needs a FREE hardware hypervisor (Intel VT-x / AMD-V,
exposed as WHPX on Windows, KVM on Linux, HVF on macOS). If VirtualBox, Hyper-V, WSL2, Docker
Desktop, or another emulator is holding it, `-accel on` fails or the AVD hangs at 0% CPU and never
reaches sys.boot_completed. Shut those down before running preflight.
JDK: the final Gradle step needs a JDK 17-21 daemon (AGP 9.2 fails on JDK 25+), same as the rest
of preflight -- point JAVA_HOME at a 17-21 JDK before invoking.
"""
from __future__ import annotations
import argparse
import os
import platform
import subprocess
import sys
import tempfile
import time
from pathlib import Path
# Defaults mirror .github/workflows/ci.yml e2e-preview (env.API37_IMAGE, env.ANDROID_PLATFORM,
# env.ANDROID_BUILD_TOOLS) and its avdmanager invocation. Do not diverge without updating ci.yml.
API37_IMAGE = "system-images;android-37.0;google_apis_ps16k;x86_64"
PLATFORM_PKG = "platforms;android-37.0"
BUILD_TOOLS = "build-tools;37.0.0"
AVD_NAME = "api37"
DEVICE_PROFILE = "pixel_2"
BOOT_TIMEOUT = 300
IS_WINDOWS = os.name == "nt"
BAT = ".bat" if IS_WINDOWS else ""
EXE = ".exe" if IS_WINDOWS else ""
def cmd(tool: str, *args: str) -> list[str]:
"""Build an argv list, wrapping Windows `.bat`/`.cmd` launchers through `cmd /c`."""
if IS_WINDOWS and tool.lower().endswith((".bat", ".cmd")):
return ["cmd", "/c", tool, *args]
return [tool, *args]
def find_sdk_root() -> str:
candidates = [os.environ.get("ANDROID_SDK_ROOT"), os.environ.get("ANDROID_HOME")]
system = platform.system()
if system == "Windows":
local = os.environ.get("LOCALAPPDATA")
if local:
candidates.append(os.path.join(local, "Android", "Sdk"))
elif system == "Darwin":
candidates.append(os.path.expanduser("~/Library/Android/sdk"))
else:
candidates.append(os.path.expanduser("~/Android/Sdk"))
for candidate in candidates:
if candidate and os.path.isdir(candidate):
return os.path.abspath(candidate)
raise RuntimeError(
"Android SDK not found. Set ANDROID_SDK_ROOT (or ANDROID_HOME) to your SDK location."
)
def resolve_tool(sdk_root: str, rel_paths: list[list[str]], name: str) -> str:
for rel in rel_paths:
path = os.path.join(sdk_root, *rel)
if os.path.isfile(path):
return path
raise RuntimeError(
f"Could not find {name} under {sdk_root}. "
"Install the Android SDK command-line tools + emulator."
)
def run_sdkmanager(sdkmanager: str, args: list[str]) -> None:
# Feed a stream of "y" so any unaccepted (incl. preview) license prompt is auto-accepted; this
# is the non-interactive equivalent of the CI runner having licenses pre-accepted.
result = subprocess.run(cmd(sdkmanager, *args), input="y\n" * 50, text=True, check=False)
if result.returncode != 0:
raise RuntimeError(f"sdkmanager failed (exit {result.returncode}) for: {' '.join(args)}")
def create_avd(avdmanager: str, emulator: str) -> None:
print(f"Creating AVD '{AVD_NAME}' from {API37_IMAGE} (device: {DEVICE_PROFILE})...")
# "no" answers avdmanager's "create a custom hardware profile?" prompt, mirroring CI.
result = subprocess.run(
cmd(avdmanager, "create", "avd", "-n", AVD_NAME, "-k", API37_IMAGE,
"-d", DEVICE_PROFILE, "--force"),
input="no\n", text=True, check=False,
)
if result.returncode != 0:
raise RuntimeError(f"avdmanager create avd failed (exit {result.returncode}).")
print("AVDs visible to the emulator:")
subprocess.run(cmd(emulator, "-list-avds"), check=False)
def start_emulator(emulator: str, emu_log: Path, attempt: int) -> subprocess.Popen:
print(f"Starting API 37 emulator (attempt {attempt})...")
# Flags mirror .github/workflows/ci.yml e2e-preview EXACTLY (cold headless boot, SwiftShader
# GPU, hardware accel required, no cameras). Keep in lockstep with that job.
flags = [
"-avd", AVD_NAME,
"-no-window", "-no-audio", "-no-boot-anim", "-no-snapshot", "-accel", "on",
"-gpu", "swiftshader_indirect", "-camera-back", "none", "-camera-front", "none",
]
log = open(emu_log, "wb") # noqa: SIM115 - handed to the child; closed in the parent below
try:
proc = subprocess.Popen(cmd(emulator, *flags), stdout=log, stderr=subprocess.STDOUT)
finally:
log.close() # the child has inherited its own fd; the parent's copy is no longer needed
return proc
def wait_for_boot(adb: str, proc: subprocess.Popen, timeout: int) -> bool:
subprocess.run(cmd(adb, "start-server"), check=False)
deadline = time.monotonic() + timeout
# Phase 1 -- the literal `adb wait-for-device`, bounded so a dead emulator can't hang the run
# (returns as soon as the emulator registers). Mirrors CI's `adb wait-for-device`.
try:
subprocess.run(cmd(adb, "wait-for-device"), timeout=max(1, deadline - time.monotonic()),
check=False)
except subprocess.TimeoutExpired:
return False
if proc.poll() is not None:
return False
# Phase 2 -- poll sys.boot_completed until it flips to 1 (mirrors CI's getprop loop).
while time.monotonic() < deadline:
if proc.poll() is not None:
print(f"Emulator process exited during boot (code {proc.returncode}).",
file=sys.stderr)
return False
out = subprocess.run(cmd(adb, "shell", "getprop", "sys.boot_completed"),
capture_output=True, text=True, check=False)
if out.stdout.strip() == "1":
return True
time.sleep(2)
return False
def stop_emulator(adb: str | None, proc: subprocess.Popen | None) -> None:
if adb:
subprocess.run(cmd(adb, "emu", "kill"), check=False,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
time.sleep(2)
if proc and proc.poll() is None:
proc.terminate()
try:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
def tail(path: Path, lines: int = 80) -> None:
try:
with open(path, "r", errors="replace") as handle:
content = handle.readlines()[-lines:]
print("--- emulator.log (tail) ---")
print("".join(content))
except OSError:
pass
def main() -> int:
parser = argparse.ArgumentParser(
description="Hand-provision + run the API 37 preview E2E suite.")
parser.add_argument("--boot-timeout", type=int, default=BOOT_TIMEOUT,
help="Seconds to wait for the emulator to reach sys.boot_completed.")
args = parser.parse_args()
sdk_root = find_sdk_root()
print(f"Using Android SDK at: {sdk_root}")
sdkmanager = resolve_tool(sdk_root, [
["cmdline-tools", "latest", "bin", "sdkmanager" + BAT],
["cmdline-tools", "bin", "sdkmanager" + BAT],
["tools", "bin", "sdkmanager" + BAT],
], "sdkmanager")
avdmanager = resolve_tool(sdk_root, [
["cmdline-tools", "latest", "bin", "avdmanager" + BAT],
["cmdline-tools", "bin", "avdmanager" + BAT],
["tools", "bin", "avdmanager" + BAT],
], "avdmanager")
# Pin ANDROID_AVD_HOME so avdmanager (writes it) and the emulator (reads it) agree on the AVD
# dir -- the same fix CI's e2e-preview applies to avoid "Unknown AVD name [api37]".
avd_home = Path.home() / ".android" / "avd"
avd_home.mkdir(parents=True, exist_ok=True)
os.environ["ANDROID_AVD_HOME"] = str(avd_home)
emu_log = Path(tempfile.gettempdir()) / "libremail-api37-emulator.log"
adb: str | None = None
proc: subprocess.Popen | None = None
test_exit = 1
try:
# 1. Install the SDK platform, build-tools, platform-tools, emulator, and preview image.
print(f"Installing SDK packages + API 37 preview system image ({API37_IMAGE})...")
run_sdkmanager(sdkmanager, ["--licenses"])
run_sdkmanager(sdkmanager, [PLATFORM_PKG, BUILD_TOOLS, "platform-tools", "emulator",
API37_IMAGE])
# adb + emulator are only guaranteed present after the install above.
adb = resolve_tool(sdk_root, [["platform-tools", "adb" + EXE]], "adb")
emulator = resolve_tool(sdk_root, [["emulator", "emulator" + EXE]], "emulator")
# 2. Create the AVD, mirroring CI.
create_avd(avdmanager, emulator)
# 3. Cold-boot headless, retrying once (mirrors CI's two-attempt boot loop).
booted = False
for attempt in (1, 2):
proc = start_emulator(emulator, emu_log, attempt)
if wait_for_boot(adb, proc, args.boot_timeout):
booted = True
break
print(f"API 37 emulator did not boot within {args.boot_timeout}s (attempt {attempt}).",
file=sys.stderr)
tail(emu_log)
stop_emulator(adb, proc)
proc = None
time.sleep(5)
if not booted:
raise RuntimeError("API 37 preview emulator failed to boot after 2 attempts.")
# 4. Dismiss the keyguard, then run the instrumented/E2E suite against the booted emulator.
subprocess.run(cmd(adb, "shell", "input", "keyevent", "82"), check=False)
repo_root = Path(__file__).resolve().parents[3]
gradlew = repo_root / ("gradlew.bat" if IS_WINDOWS else "gradlew")
print(f"Running :app:connectedDebugAndroidTest against {AVD_NAME}...")
test_exit = subprocess.run(
cmd(str(gradlew), ":app:connectedDebugAndroidTest", "--stacktrace"),
cwd=str(repo_root), check=False,
).returncode
except Exception as exc: # noqa: BLE001 - top-level guard so teardown always runs
print(f"ERROR: {exc}", file=sys.stderr)
test_exit = 1
finally:
# 5. Always tear the emulator down and delete the AVD, even on failure.
print("Tearing down API 37 emulator and AVD...")
stop_emulator(adb, proc)
subprocess.run(cmd(avdmanager, "delete", "avd", "-n", AVD_NAME), check=False,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
if test_exit != 0:
print(f"api37 connectedDebugAndroidTest failed (exit {test_exit}).", file=sys.stderr)
return test_exit
print("api37 E2E passed.")
return 0
if __name__ == "__main__":
sys.exit(main())
+20 -16
View File
@@ -28,18 +28,21 @@ 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 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.
`detekt` + the top-of-matrix emulator E2E `api35DebugAndroidTest` + `api36DebugAndroidTest` +
the API 37 preview E2E via `python3 .claude/skills/preflight/api37_e2e.py` (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). API 37 (preview) has no Gradle Managed Device — its
only image is the nonstandard `android-37.0` / `google_apis_ps16k` pairing (see the comment above
`testOptions.managedDevices` in `app/build.gradle.kts`) — so `api37_e2e.py` hand-provisions it,
mirroring CI's `e2e-preview` job (same image + emulator flags), boots it headless, runs
`connectedDebugAndroidTest`, and tears it down. Running all three levels locally is required; the
rest of the multi-API matrix (API 29–34) stays CI's job. Emulators need a free hardware
hypervisor (VT-x/WHPX), so shut down VirtualBox/other VMs first or the AVD hangs at 0% CPU.
## Build-config gotchas
@@ -73,10 +76,11 @@ 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 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.
compile: preflight runs the top-of-matrix emulator E2E locally — `api35DebugAndroidTest` +
`api36DebugAndroidTest` (Gradle Managed Devices) plus the API 37 preview via
`api37_e2e.py` (hand-provisioned, mirroring CI's `e2e-preview` job) — and all three must be
green before the change is done. CI then runs the full multi-API matrix plus the API 37 preview
job.
## Repo etiquette
+5 -2
View File
@@ -157,8 +157,11 @@ android {
// 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.
// cross-check used a physical Pixel, not an emulator/AVD. Locally, preflight covers API 37
// by hand-provisioning it with .claude/skills/preflight/api37_e2e.py, which mirrors the
// e2e-preview job (same image + emulator flags). Once a managed-device-compatible image is
// published, add `api37` here (and to the CI matrix), delete that script, and drop the
// e2e-preview job.
managedDevices {
localDevices {
listOf(29, 30, 31, 32, 33, 34, 35, 36).forEach { api ->