Incident (2026-07-08): the E2E (33) matrix leg deadlocked the Mergify merge queue for ~2h. reactivecircus/android-emulator-runner's un-guarded "Create AVD and generate snapshot" step died with Error on ZipFile unknown archive while installing a corrupt Android Emulator SDK package. #389 added corrupt-zip verify → reject → purge → re-download hardening, but only to the platform/build-tools install (setup_android_sdk.py). The emulator + system-image install performed by android-emulator-runner was unguarded, so a corrupt emulator/system-image zip failed hard (and, since a failed leg never saves its avd-33 snapshot, it re-hit the same corrupt zip every run — deterministic, not a flake).
Fix (issue's preferred option 1)
Pre-install emulator + this leg's system-images;android-<api>;google_apis;x86_64 through the #389-hardenedsetup_android_sdk.py installbefore the android-emulator-runner steps, in the e2e matrix job. A corrupt zip is now self-healed there, and the emulator-runner finds both packages already installed and skips its fragile fetch.
ci.yml (e2e matrix job only)
New step "Pre-install emulator + system image (hardened)" after Enable KVM, before Cache AVD snapshot (so it precedes all three android-emulator-runner invocations):
Runs unconditionally (both AVD-cache hit and miss): the emulator binary + system image live under the SDK root, not the ~/.android AVD-snapshot cache, so they must be present for even a cached AVD to boot.
Updated the now-stale SDK-cache comment ("emulator + system image stay with android-emulator-runner") to reflect the new hardened pre-install.
test_setup_android_sdk.py
Added one assertion pinning the matrix's system-images;android-33;google_apis;x86_64 id to its correct purge path (the existing test only covered the preview google_apis_ps16k variant). setup_android_sdk.py itself is unchanged — its install subcommand + package_dir already handle emulator and system-images;… ids generically.
e2e-preview (API 37) — no change
Already routes emulator + $API37_IMAGE through the same hardened installer (it hand-provisions with avdmanager/emulator, never using android-emulator-runner), so it was already covered. It was the template for this fix.
Why it closes the corrupt-zip gap
Every emulator/system-image download for the matrix legs now flows through the same SHA-verify → reject-corrupt → purge → re-download loop that already protects platform/build-tools. A corrupt emulator zip is purged and re-fetched (up to 4 attempts) instead of dying on android-emulator-runner's bare sdkmanager exit 1.
Risk to existing cache/matrix behavior
SDK cache (android-sdk-v1-*): key + paths unchanged; the emulator/system-image are intentionally not added (keeps the shared key small — mirrors e2e-preview). Re-install is a fast sdkmanager no-op when already present.
AVD snapshot cache (avd-*): untouched — the new step doesn't write ~/.android/avd. Cache-hit legs still restore + boot their cached AVD; the pre-install just guarantees the emulator/image are present under the SDK root.
Matrix: target google_apis / arch x86_64 kept in lockstep with the android-emulator-runner steps.
Deliberate tradeoff: did not add a per-api-level system-image cache (8 × ~0.5–1 GB would risk evicting the more valuable SDK/AVD caches under the 10 GB Actions limit). With the install now hardened, re-downloading is safe; a sysimg cache is a future perf-only follow-up.
Validation
ci.yml parses (PyYAML); new step verified in position via structural assertions; caught + fixed a YAML gotcha where an unquoted #443 in the step name parsed as a comment (issue ref kept in the adjacent comment instead).
python -m py_compile clean; full .github/scripts unit suite green (74 tests, 1 POSIX-only skip); argparse dry-run confirms the new command maps emulator → <root>/emulator and the system-image id → the correct nested purge dir.
Self-validating: this PR's own CI runs the full E2E matrix with the new setup.
## What / why
Closes #443.
**Incident (2026-07-08):** the `E2E (33)` matrix leg deadlocked the Mergify merge queue for ~2h. `reactivecircus/android-emulator-runner`'s un-guarded *"Create AVD and generate snapshot"* step died with `Error on ZipFile unknown archive` while installing a corrupt **Android Emulator** SDK package. #389 added corrupt-zip `verify → reject → purge → re-download` hardening, but only to the platform/build-tools install (`setup_android_sdk.py`). The **emulator + system-image install performed by `android-emulator-runner` was unguarded**, so a corrupt emulator/system-image zip failed hard (and, since a failed leg never saves its `avd-33` snapshot, it re-hit the same corrupt zip every run — deterministic, not a flake).
## Fix (issue's preferred option 1)
Pre-install `emulator` + this leg's `system-images;android-<api>;google_apis;x86_64` through the **#389-hardened** `setup_android_sdk.py install` **before** the `android-emulator-runner` steps, in the `e2e` matrix job. A corrupt zip is now self-healed there, and the emulator-runner finds both packages already installed and skips its fragile fetch.
### `ci.yml` (`e2e` matrix job only)
- New step **"Pre-install emulator + system image (hardened)"** after *Enable KVM*, before *Cache AVD snapshot* (so it precedes all three `android-emulator-runner` invocations):
```
python3 .github/scripts/setup_android_sdk.py install "emulator" "system-images;android-${{ matrix.api-level }};google_apis;x86_64"
```
Runs **unconditionally** (both AVD-cache hit and miss): the emulator binary + system image live under the SDK root, not the `~/.android` AVD-snapshot cache, so they must be present for even a cached AVD to boot.
- Updated the now-stale SDK-cache comment ("emulator + system image stay with android-emulator-runner") to reflect the new hardened pre-install.
### `test_setup_android_sdk.py`
- Added one assertion pinning the matrix's `system-images;android-33;google_apis;x86_64` id to its correct purge path (the existing test only covered the preview `google_apis_ps16k` variant). `setup_android_sdk.py` itself is **unchanged** — its `install` subcommand + `package_dir` already handle `emulator` and `system-images;…` ids generically.
### `e2e-preview` (API 37) — no change
Already routes `emulator` + `$API37_IMAGE` through the same hardened installer (it hand-provisions with `avdmanager`/`emulator`, never using `android-emulator-runner`), so it was already covered. It was the template for this fix.
## Why it closes the corrupt-zip gap
Every emulator/system-image download for the matrix legs now flows through the same SHA-verify → reject-corrupt → purge → re-download loop that already protects platform/build-tools. A corrupt emulator zip is purged and re-fetched (up to 4 attempts) instead of dying on `android-emulator-runner`'s bare `sdkmanager` exit 1.
## Risk to existing cache/matrix behavior
- **SDK cache (`android-sdk-v1-*`):** key + paths unchanged; the emulator/system-image are intentionally **not** added (keeps the shared key small — mirrors e2e-preview). Re-install is a fast `sdkmanager` no-op when already present.
- **AVD snapshot cache (`avd-*`):** untouched — the new step doesn't write `~/.android/avd`. Cache-hit legs still restore + boot their cached AVD; the pre-install just guarantees the emulator/image are present under the SDK root.
- **Matrix:** target `google_apis` / arch `x86_64` kept in lockstep with the `android-emulator-runner` steps.
- **Deliberate tradeoff:** did not add a per-api-level system-image cache (8 × ~0.5–1 GB would risk evicting the more valuable SDK/AVD caches under the 10 GB Actions limit). With the install now hardened, re-downloading is safe; a sysimg cache is a future perf-only follow-up.
## Validation
- `ci.yml` parses (PyYAML); new step verified in position via structural assertions; caught + fixed a YAML gotcha where an unquoted `#443` in the step name parsed as a comment (issue ref kept in the adjacent comment instead).
- `python -m py_compile` clean; full `.github/scripts` unit suite green (74 tests, 1 POSIX-only skip); argparse dry-run confirms the new command maps `emulator` → `<root>/emulator` and the system-image id → the correct nested purge dir.
- **Self-validating:** this PR's own CI runs the full E2E matrix with the new setup.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What / why
Closes #443.
Incident (2026-07-08): the
E2E (33)matrix leg deadlocked the Mergify merge queue for ~2h.reactivecircus/android-emulator-runner's un-guarded "Create AVD and generate snapshot" step died withError on ZipFile unknown archivewhile installing a corrupt Android Emulator SDK package. #389 added corrupt-zipverify → reject → purge → re-downloadhardening, but only to the platform/build-tools install (setup_android_sdk.py). The emulator + system-image install performed byandroid-emulator-runnerwas unguarded, so a corrupt emulator/system-image zip failed hard (and, since a failed leg never saves itsavd-33snapshot, it re-hit the same corrupt zip every run — deterministic, not a flake).Fix (issue's preferred option 1)
Pre-install
emulator+ this leg'ssystem-images;android-<api>;google_apis;x86_64through the #389-hardenedsetup_android_sdk.py installbefore theandroid-emulator-runnersteps, in thee2ematrix job. A corrupt zip is now self-healed there, and the emulator-runner finds both packages already installed and skips its fragile fetch.ci.yml(e2ematrix job only)android-emulator-runnerinvocations):~/.androidAVD-snapshot cache, so they must be present for even a cached AVD to boot.test_setup_android_sdk.pysystem-images;android-33;google_apis;x86_64id to its correct purge path (the existing test only covered the previewgoogle_apis_ps16kvariant).setup_android_sdk.pyitself is unchanged — itsinstallsubcommand +package_diralready handleemulatorandsystem-images;…ids generically.e2e-preview(API 37) — no changeAlready routes
emulator+$API37_IMAGEthrough the same hardened installer (it hand-provisions withavdmanager/emulator, never usingandroid-emulator-runner), so it was already covered. It was the template for this fix.Why it closes the corrupt-zip gap
Every emulator/system-image download for the matrix legs now flows through the same SHA-verify → reject-corrupt → purge → re-download loop that already protects platform/build-tools. A corrupt emulator zip is purged and re-fetched (up to 4 attempts) instead of dying on
android-emulator-runner's baresdkmanagerexit 1.Risk to existing cache/matrix behavior
android-sdk-v1-*): key + paths unchanged; the emulator/system-image are intentionally not added (keeps the shared key small — mirrors e2e-preview). Re-install is a fastsdkmanagerno-op when already present.avd-*): untouched — the new step doesn't write~/.android/avd. Cache-hit legs still restore + boot their cached AVD; the pre-install just guarantees the emulator/image are present under the SDK root.google_apis/ archx86_64kept in lockstep with theandroid-emulator-runnersteps.Validation
ci.ymlparses (PyYAML); new step verified in position via structural assertions; caught + fixed a YAML gotcha where an unquoted#443in the step name parsed as a comment (issue ref kept in the adjacent comment instead).python -m py_compileclean; full.github/scriptsunit suite green (74 tests, 1 POSIX-only skip); argparse dry-run confirms the new command mapsemulator→<root>/emulatorand the system-image id → the correct nested purge dir.Merge Queue Status
2026-07-08 17:05 UTC· Rule:default· triggered by merge protections2026-07-08 17:24 UTC· at5c00a8da7085d9215951b3533d5a5b6919664008· mergeThis pull request spent 19 minutes 26 seconds in the queue, including 18 minutes 59 seconds running CI.
Required conditions to merge
-conflict-draftbase = maincheck-success = CI passedgithub-review-approved[🛡 GitHub repository ruleset rulemain]label != brokencheck-success = Debug buildcheck-neutral = Debug buildcheck-skipped = Debug buildcheck-success = Unit testscheck-neutral = Unit testscheck-skipped = Unit testscheck-success = CI passedcheck-neutral = CI passedcheck-skipped = CI passedmain]:check-success = @github-actions/CI passedcheck-neutral = @github-actions/CI passedcheck-skipped = @github-actions/CI passed