ci(e2e): harden matrix emulator + system-image install against corrupt-zip (extend #389) #446

Merged
JMR-dev merged 1 commits from ci-443-emulator-corrupt-zip-guard into main 2026-07-08 17:24:38 +00:00
JMR-dev commented 2026-07-08 15:40:21 +00:00 (Migrated from github.com)

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.
## 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.
mergify[bot] commented 2026-07-08 17:05:19 +00:00 (Migrated from github.com)

Merge Queue Status

This pull request spent 19 minutes 26 seconds in the queue, including 18 minutes 59 seconds running CI.

Required conditions to merge
<!--- DO NOT EDIT -*- Mergify Payload -*- {"version": 1, "state": "merged", "queue_rule_name": "default", "queued_at": "2026-07-08T17:05:13.676524+00:00", "estimated_time_of_merge": null, "speculative_check_pr": null, "required_conditions": []} -*- Mergify Payload End -*- --> # Merge Queue Status - ✅ **Entered queue** — `2026-07-08 17:05 UTC` · Rule: `default` · triggered by merge protections - ✅ **Checks passed** · on draft #452 - ✅ **Merged** — `2026-07-08 17:24 UTC` · at `5c00a8da7085d9215951b3533d5a5b6919664008` · merge This pull request spent **19 minutes 26 seconds** in the queue, including **18 minutes 59 seconds** running CI. <details> <summary>Required conditions to merge</summary> - `-conflict` - [X] #437 - [X] #446 - `-draft` - [X] #437 - [X] #446 - [X] `base = main` - [X] `check-success = CI passed` - `github-review-approved` [🛡 GitHub repository ruleset rule `main`] - [X] #437 - [X] #446 - `label != broken` - [X] #437 - [X] #446 - [X] any of [🛡 GitHub branch protection]: - [X] `check-success = Debug build` - [ ] `check-neutral = Debug build` - [ ] `check-skipped = Debug build` - [X] any of [🛡 GitHub branch protection]: - [X] `check-success = Unit tests` - [ ] `check-neutral = Unit tests` - [ ] `check-skipped = Unit tests` - [X] any of [🛡 GitHub branch protection]: - [X] `check-success = CI passed` - [ ] `check-neutral = CI passed` - [ ] `check-skipped = CI passed` - [X] any of [🛡 GitHub repository ruleset rule `main`]: - [X] `check-success = @github-actions/CI passed` - [ ] `check-neutral = @github-actions/CI passed` - [ ] `check-skipped = @github-actions/CI passed` </details>
Sign in to join this conversation.