From 45fb8bd4a1570db0a247c2e2d2965f12bd11b4c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 22:57:39 +0000 Subject: [PATCH 01/10] ci: wait for emulator services to be ready post-snapshot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sys.boot_completed` flips as soon as zygote is alive, which on a snapshot restore happens before `system_server` finishes binding the `settings`, `package`, and `activity` services. The previous wait-for-device loop returned almost instantly because the property was already set, and the next `adb shell settings put` then crashed with `cmd: Failure calling service settings: Broken pipe (32)`. Replace it with a probe that polls each service we're about to call until it actually responds, with a 120s ceiling. One retry on each `settings put` covers a residual race where `list` succeeds but a write transaction loses to first-time service initialisation. `disable-animations: false` is unchanged — emulator-runner's own `input keyevent 82` path crashes the emulator on this image, which is why the workflow drives the settings directly. --- .github/workflows/android-tests.yml | 42 ++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/.github/workflows/android-tests.yml b/.github/workflows/android-tests.yml index fbb45f5..6218d5a 100644 --- a/.github/workflows/android-tests.yml +++ b/.github/workflows/android-tests.yml @@ -194,18 +194,36 @@ jobs: -noaudio -no-boot-anim disable-animations: false script: | - # Wait for system services after snapshot restore — - # sys.boot_completed can flip before the `input` service publishes, - # which makes emulator-runner's own `input keyevent 82` crash the - # emulator. We run this loop ourselves and skip the keyevent - # (disable-animations: false). - adb wait-for-device shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 1; done' - # Disable animations via settings (more reliable than input keyevent) - adb shell settings put global window_animation_scale 0.0 - adb shell settings put global transition_animation_scale 0.0 - adb shell settings put global animator_duration_scale 0.0 - # -i prints per-test STARTED/PASSED/FAILED so a hanging test is - # identifiable + # On snapshot restore, sys.boot_completed flips before + # system_server finishes binding settings/package/activity, so + # the next `adb shell settings put` can fail with "Broken pipe". + # Poll each service until it actually answers. + wait_for_emulator() { + local deadline=$((SECONDS + 120)) + while [[ $SECONDS -lt $deadline ]]; do + # tr -d '\r' because adb echoes CRLF. + bc=$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') + if [[ "$bc" == "1" ]] \ + && adb shell 'pm path android' >/dev/null 2>&1 \ + && adb shell 'cmd settings list global' >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + echo "::error::Emulator services did not become ready within 120s" >&2 + adb shell getprop | grep -E 'boot|svc' >&2 || true + return 1 + } + wait_for_emulator + # disable-animations is left `false` because emulator-runner's + # `input keyevent` path crashes on this image; drive the + # settings directly. One retry covers a write-transaction race + # against first-time service init. + for s in window_animation_scale transition_animation_scale animator_duration_scale; do + adb shell settings put global "$s" 0.0 \ + || { sleep 2; adb shell settings put global "$s" 0.0; } + done + # -i so a hanging test is identifiable from per-test STARTED/PASSED/FAILED. cd apps/example/android && ./gradlew :comapeo-core-react-native:connectedDebugAndroidTest :app:connectedDebugAndroidTest --no-daemon -i -PreactNativeArchitectures=x86_64 - name: Upload test results From b6ede966db72cadc43909b2d8e9702eb7f145c92 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 23:26:57 +0000 Subject: [PATCH 02/10] ci: rewrite emulator-readiness probe in POSIX shell reactivecircus/android-emulator-runner v2 invokes the script with \`/usr/bin/sh\` (dash on Ubuntu), not bash. The previous version used \`[[ ]]\`, \`local\`, and \`\$SECONDS\` which dash refuses to parse with: /usr/bin/sh: 1: Syntax error: end of file unexpected (expecting \"}\") Replace with POSIX-only constructs: \`[ ]\` tests, \`\$(date +%s)\` for elapsed-time tracking, an inline loop with a \`ready\` flag instead of a function. Verified parses + runs under dash locally. --- .github/workflows/android-tests.yml | 40 ++++++++++++++--------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/.github/workflows/android-tests.yml b/.github/workflows/android-tests.yml index 6218d5a..a7b8485 100644 --- a/.github/workflows/android-tests.yml +++ b/.github/workflows/android-tests.yml @@ -197,27 +197,27 @@ jobs: # On snapshot restore, sys.boot_completed flips before # system_server finishes binding settings/package/activity, so # the next `adb shell settings put` can fail with "Broken pipe". - # Poll each service until it actually answers. - wait_for_emulator() { - local deadline=$((SECONDS + 120)) - while [[ $SECONDS -lt $deadline ]]; do - # tr -d '\r' because adb echoes CRLF. - bc=$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') - if [[ "$bc" == "1" ]] \ - && adb shell 'pm path android' >/dev/null 2>&1 \ - && adb shell 'cmd settings list global' >/dev/null 2>&1; then - return 0 - fi - sleep 1 - done + # Poll each service until it actually answers. POSIX-only — + # the action runs this with /bin/sh (dash on Ubuntu). + start=$(date +%s) + while [ "$(($(date +%s) - start))" -lt 120 ]; do + # tr -d '\r' because adb echoes CRLF. + bc=$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') + if [ "$bc" = "1" ] \ + && adb shell 'pm path android' >/dev/null 2>&1 \ + && adb shell 'cmd settings list global' >/dev/null 2>&1; then + ready=1 + break + fi + sleep 1 + done + if [ "${ready:-0}" != "1" ]; then echo "::error::Emulator services did not become ready within 120s" >&2 - adb shell getprop | grep -E 'boot|svc' >&2 || true - return 1 - } - wait_for_emulator - # disable-animations is left `false` because emulator-runner's - # `input keyevent` path crashes on this image; drive the - # settings directly. One retry covers a write-transaction race + adb shell getprop 2>/dev/null | grep -E 'boot|svc' >&2 || true + exit 1 + fi + # disable-animations is left `false` so we drive the settings + # directly here; one retry covers a write-transaction race # against first-time service init. for s in window_animation_scale transition_animation_scale animator_duration_scale; do adb shell settings put global "$s" 0.0 \ From 1034542d7b491744d00eb8ed3ce98fdffc492f4a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 8 May 2026 05:46:32 +0000 Subject: [PATCH 03/10] ci: move emulator-readiness probe + test runner to scripts/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit reactivecircus/android-emulator-runner v2 invokes the workflow's `script:` line by line through `sh -c`, not as a single multi-line script — so any function definition, while loop, or other multi-line construct is split across separate shell invocations and fails to parse (the previous attempts hit "expecting }" / "expecting done"). Move the entire body to scripts/run-android-instrumented-tests.sh and have the workflow invoke it as a single line. The script can use bash freely (proper shebang, set -euo pipefail). --- .github/workflows/android-tests.yml | 33 +--------------- scripts/run-android-instrumented-tests.sh | 46 +++++++++++++++++++++++ 2 files changed, 47 insertions(+), 32 deletions(-) create mode 100755 scripts/run-android-instrumented-tests.sh diff --git a/.github/workflows/android-tests.yml b/.github/workflows/android-tests.yml index a7b8485..9a2998e 100644 --- a/.github/workflows/android-tests.yml +++ b/.github/workflows/android-tests.yml @@ -193,38 +193,7 @@ jobs: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim disable-animations: false - script: | - # On snapshot restore, sys.boot_completed flips before - # system_server finishes binding settings/package/activity, so - # the next `adb shell settings put` can fail with "Broken pipe". - # Poll each service until it actually answers. POSIX-only — - # the action runs this with /bin/sh (dash on Ubuntu). - start=$(date +%s) - while [ "$(($(date +%s) - start))" -lt 120 ]; do - # tr -d '\r' because adb echoes CRLF. - bc=$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') - if [ "$bc" = "1" ] \ - && adb shell 'pm path android' >/dev/null 2>&1 \ - && adb shell 'cmd settings list global' >/dev/null 2>&1; then - ready=1 - break - fi - sleep 1 - done - if [ "${ready:-0}" != "1" ]; then - echo "::error::Emulator services did not become ready within 120s" >&2 - adb shell getprop 2>/dev/null | grep -E 'boot|svc' >&2 || true - exit 1 - fi - # disable-animations is left `false` so we drive the settings - # directly here; one retry covers a write-transaction race - # against first-time service init. - for s in window_animation_scale transition_animation_scale animator_duration_scale; do - adb shell settings put global "$s" 0.0 \ - || { sleep 2; adb shell settings put global "$s" 0.0; } - done - # -i so a hanging test is identifiable from per-test STARTED/PASSED/FAILED. - cd apps/example/android && ./gradlew :comapeo-core-react-native:connectedDebugAndroidTest :app:connectedDebugAndroidTest --no-daemon -i -PreactNativeArchitectures=x86_64 + script: ./scripts/run-android-instrumented-tests.sh - name: Upload test results if: always() diff --git a/scripts/run-android-instrumented-tests.sh b/scripts/run-android-instrumented-tests.sh new file mode 100755 index 0000000..985c882 --- /dev/null +++ b/scripts/run-android-instrumented-tests.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Driver for the Android instrumented test job. Lives in a file (rather +# than inline in the workflow) because reactivecircus/android-emulator-runner +# runs the workflow's `script:` line by line through `sh -c`, which breaks +# any multi-line construct (function definition, while loop, etc.). + +# On snapshot restore, sys.boot_completed flips before system_server +# finishes binding settings/package/activity, so the next +# `adb shell settings put` can fail with "Broken pipe". Poll each service +# until it actually answers. +wait_for_emulator_services() { + local deadline=$((SECONDS + 120)) + while (( SECONDS < deadline )); do + local bc + # tr -d '\r' because adb echoes CRLF. + bc=$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') + if [[ "$bc" == "1" ]] \ + && adb shell 'pm path android' >/dev/null 2>&1 \ + && adb shell 'cmd settings list global' >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + echo "::error::Emulator services did not become ready within 120s" >&2 + adb shell getprop 2>/dev/null | grep -E 'boot|svc' >&2 || true + return 1 +} + +wait_for_emulator_services + +# disable-animations is left `false` so we drive the settings directly +# here; one retry covers a write-transaction race against first-time +# service init. +for s in window_animation_scale transition_animation_scale animator_duration_scale; do + adb shell settings put global "$s" 0.0 \ + || { sleep 2; adb shell settings put global "$s" 0.0; } +done + +# -i so a hanging test is identifiable from per-test STARTED/PASSED/FAILED. +cd apps/example/android +exec ./gradlew \ + :comapeo-core-react-native:connectedDebugAndroidTest \ + :app:connectedDebugAndroidTest \ + --no-daemon -i -PreactNativeArchitectures=x86_64 From 8d22bb1b84e185c7421174d16c890b26cd17c723 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 8 May 2026 06:01:03 +0000 Subject: [PATCH 04/10] ci: keep only the readiness probe in scripts/, inline the rest Scope the script down to just the multi-line wait-for-emulator-services loop (the only part that can't survive the action's per-line `sh -c`). Animation settings and the gradle invocation move back inline as single-line statements; the `||` retry uses a one-line brace group, which dash parses fine. --- .github/workflows/android-tests.yml | 7 +++- scripts/run-android-instrumented-tests.sh | 46 ----------------------- scripts/wait-for-emulator.sh | 28 ++++++++++++++ 3 files changed, 34 insertions(+), 47 deletions(-) delete mode 100755 scripts/run-android-instrumented-tests.sh create mode 100755 scripts/wait-for-emulator.sh diff --git a/.github/workflows/android-tests.yml b/.github/workflows/android-tests.yml index 9a2998e..1e486df 100644 --- a/.github/workflows/android-tests.yml +++ b/.github/workflows/android-tests.yml @@ -193,7 +193,12 @@ jobs: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim disable-animations: false - script: ./scripts/run-android-instrumented-tests.sh + script: | + ./scripts/wait-for-emulator.sh + adb shell settings put global window_animation_scale 0.0 || { sleep 2; adb shell settings put global window_animation_scale 0.0; } + adb shell settings put global transition_animation_scale 0.0 || { sleep 2; adb shell settings put global transition_animation_scale 0.0; } + adb shell settings put global animator_duration_scale 0.0 || { sleep 2; adb shell settings put global animator_duration_scale 0.0; } + cd apps/example/android && ./gradlew :comapeo-core-react-native:connectedDebugAndroidTest :app:connectedDebugAndroidTest --no-daemon -i -PreactNativeArchitectures=x86_64 - name: Upload test results if: always() diff --git a/scripts/run-android-instrumented-tests.sh b/scripts/run-android-instrumented-tests.sh deleted file mode 100755 index 985c882..0000000 --- a/scripts/run-android-instrumented-tests.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -# Driver for the Android instrumented test job. Lives in a file (rather -# than inline in the workflow) because reactivecircus/android-emulator-runner -# runs the workflow's `script:` line by line through `sh -c`, which breaks -# any multi-line construct (function definition, while loop, etc.). - -# On snapshot restore, sys.boot_completed flips before system_server -# finishes binding settings/package/activity, so the next -# `adb shell settings put` can fail with "Broken pipe". Poll each service -# until it actually answers. -wait_for_emulator_services() { - local deadline=$((SECONDS + 120)) - while (( SECONDS < deadline )); do - local bc - # tr -d '\r' because adb echoes CRLF. - bc=$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') - if [[ "$bc" == "1" ]] \ - && adb shell 'pm path android' >/dev/null 2>&1 \ - && adb shell 'cmd settings list global' >/dev/null 2>&1; then - return 0 - fi - sleep 1 - done - echo "::error::Emulator services did not become ready within 120s" >&2 - adb shell getprop 2>/dev/null | grep -E 'boot|svc' >&2 || true - return 1 -} - -wait_for_emulator_services - -# disable-animations is left `false` so we drive the settings directly -# here; one retry covers a write-transaction race against first-time -# service init. -for s in window_animation_scale transition_animation_scale animator_duration_scale; do - adb shell settings put global "$s" 0.0 \ - || { sleep 2; adb shell settings put global "$s" 0.0; } -done - -# -i so a hanging test is identifiable from per-test STARTED/PASSED/FAILED. -cd apps/example/android -exec ./gradlew \ - :comapeo-core-react-native:connectedDebugAndroidTest \ - :app:connectedDebugAndroidTest \ - --no-daemon -i -PreactNativeArchitectures=x86_64 diff --git a/scripts/wait-for-emulator.sh b/scripts/wait-for-emulator.sh new file mode 100755 index 0000000..82518f5 --- /dev/null +++ b/scripts/wait-for-emulator.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Lives in a file (rather than inline in the workflow) because +# reactivecircus/android-emulator-runner runs the workflow's `script:` +# line by line through `sh -c`, which breaks any multi-line construct +# (function definition, while loop, etc.). +# +# On snapshot restore, sys.boot_completed flips before system_server +# finishes binding settings/package/activity, so the next +# `adb shell settings put` can fail with "Broken pipe". Poll each service +# until it actually answers. + +deadline=$((SECONDS + 120)) +while (( SECONDS < deadline )); do + # tr -d '\r' because adb echoes CRLF. + bc=$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') + if [[ "$bc" == "1" ]] \ + && adb shell 'pm path android' >/dev/null 2>&1 \ + && adb shell 'cmd settings list global' >/dev/null 2>&1; then + exit 0 + fi + sleep 1 +done + +echo "::error::Emulator services did not become ready within 120s" >&2 +adb shell getprop 2>/dev/null | grep -E 'boot|svc' >&2 || true +exit 1 From 42cedd62b018c4be5bbb216151e3e708015186ee Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Mon, 11 May 2026 13:24:58 +0100 Subject: [PATCH 05/10] clean up changes --- .github/workflows/android-tests.yml | 9 ++++++--- scripts/wait-for-emulator.sh | 5 ----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/android-tests.yml b/.github/workflows/android-tests.yml index 1e486df..1d86e42 100644 --- a/.github/workflows/android-tests.yml +++ b/.github/workflows/android-tests.yml @@ -195,9 +195,12 @@ jobs: disable-animations: false script: | ./scripts/wait-for-emulator.sh - adb shell settings put global window_animation_scale 0.0 || { sleep 2; adb shell settings put global window_animation_scale 0.0; } - adb shell settings put global transition_animation_scale 0.0 || { sleep 2; adb shell settings put global transition_animation_scale 0.0; } - adb shell settings put global animator_duration_scale 0.0 || { sleep 2; adb shell settings put global animator_duration_scale 0.0; } + # Disable animations via settings (more reliable than input keyevent) + adb shell settings put global window_animation_scale 0.0 + adb shell settings put global transition_animation_scale 0.0 + adb shell settings put global animator_duration_scale 0.0 + # -i prints per-test STARTED/PASSED/FAILED so a hanging test is + # identifiable cd apps/example/android && ./gradlew :comapeo-core-react-native:connectedDebugAndroidTest :app:connectedDebugAndroidTest --no-daemon -i -PreactNativeArchitectures=x86_64 - name: Upload test results diff --git a/scripts/wait-for-emulator.sh b/scripts/wait-for-emulator.sh index 82518f5..87477e6 100755 --- a/scripts/wait-for-emulator.sh +++ b/scripts/wait-for-emulator.sh @@ -1,11 +1,6 @@ #!/usr/bin/env bash set -euo pipefail -# Lives in a file (rather than inline in the workflow) because -# reactivecircus/android-emulator-runner runs the workflow's `script:` -# line by line through `sh -c`, which breaks any multi-line construct -# (function definition, while loop, etc.). -# # On snapshot restore, sys.boot_completed flips before system_server # finishes binding settings/package/activity, so the next # `adb shell settings put` can fail with "Broken pipe". Poll each service From 2050b8aafe6ad363f93daaddf0b49c9db2bc2cde Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Mon, 11 May 2026 15:12:39 +0100 Subject: [PATCH 06/10] ci: instrument emulator-readiness probe to log per-check state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run each readiness check independently each tick so we capture pm and settings exit codes separately, then log t=Xs bc='…' pm_rc=… cmd_rc=… every ~10s and dump init.svc.* every ~30s. Drop set -e so an intermittent adb hiccup can't silently kill the probe. Diagnostic step — next CI run should show whether the timeout is a genuinely-broken emulator or a probe bug. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/wait-for-emulator.sh | 38 +++++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/scripts/wait-for-emulator.sh b/scripts/wait-for-emulator.sh index 87477e6..6425cfa 100755 --- a/scripts/wait-for-emulator.sh +++ b/scripts/wait-for-emulator.sh @@ -1,20 +1,48 @@ #!/usr/bin/env bash -set -euo pipefail +set -uo pipefail # On snapshot restore, sys.boot_completed flips before system_server # finishes binding settings/package/activity, so the next # `adb shell settings put` can fail with "Broken pipe". Poll each service # until it actually answers. +# +# -e intentionally omitted: an intermittent adb hiccup must not kill the +# probe — we want the loop to keep polling. + +start=$SECONDS +deadline=$((start + 120)) +# Negative seeds so the first iteration always logs. +last_report=-10 +last_svc_dump=-30 -deadline=$((SECONDS + 120)) while (( SECONDS < deadline )); do # tr -d '\r' because adb echoes CRLF. bc=$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') - if [[ "$bc" == "1" ]] \ - && adb shell 'pm path android' >/dev/null 2>&1 \ - && adb shell 'cmd settings list global' >/dev/null 2>&1; then + + adb shell 'pm path android' >/dev/null 2>&1 + pm_rc=$? + + adb shell 'cmd settings list global' >/dev/null 2>&1 + cmd_rc=$? + + if [[ "$bc" == "1" && $pm_rc -eq 0 && $cmd_rc -eq 0 ]]; then + echo "[wait-for-emulator] ready after $(( SECONDS - start ))s" >&2 exit 0 fi + + elapsed=$(( SECONDS - start )) + + if (( elapsed - last_report >= 10 )); then + echo "[wait-for-emulator] t=${elapsed}s bc='${bc}' pm_rc=${pm_rc} cmd_rc=${cmd_rc}" >&2 + last_report=$elapsed + fi + + if (( elapsed - last_svc_dump >= 30 )); then + echo "[wait-for-emulator] init.svc.* snapshot at t=${elapsed}s:" >&2 + adb shell getprop 2>/dev/null | grep -E '^\[init\.svc\.' >&2 || true + last_svc_dump=$elapsed + fi + sleep 1 done From 5ae7915f5c9d1b05b9ee7ee44fb9dc122b350ca5 Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Mon, 11 May 2026 17:05:14 +0100 Subject: [PATCH 07/10] ci: bust stale AVD cache and verify emulator before snapshot save Previous CI runs showed a clear pattern: the cached snapshot booted but zygote immediately fell into a restart loop (init.svc.zygote = restarting for the full 120s, pm/cmd returning DEAD_OBJECT). The warning "Please update the emulator to one that supports the feature(s): Vulkan" plus a forced "Increasing RAM size to 2048MB" pointed at incompatibility between the cached snapshot and the emulator GitHub now installs. Two changes: - Cache key suffix `-v2` so the bad snapshot is abandoned. - Run wait-for-emulator.sh in the snapshot-creation step so we only save a snapshot from a fully-healthy userland (the action saves on emulator shutdown, after the script returns). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/android-tests.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/android-tests.yml b/.github/workflows/android-tests.yml index 1d86e42..46fadc6 100644 --- a/.github/workflows/android-tests.yml +++ b/.github/workflows/android-tests.yml @@ -165,7 +165,9 @@ jobs: path: | ~/.android/avd/* ~/.android/adb* - key: avd-api${{ matrix.api-level }}-google_atd-x86_64 + # Bump suffix to invalidate stale snapshots when emulator + # platform changes (Vulkan profile, RAM minimums) break restore. + key: avd-api${{ matrix.api-level }}-google_atd-x86_64-v2 - name: Create AVD and generate snapshot if: steps.avd-cache.outputs.cache-hit != 'true' @@ -178,7 +180,9 @@ jobs: ram-size: 4096M emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim disable-animations: false - script: echo "Generated AVD snapshot for caching" + # Wait until userland is fully up before letting the action + # shut down the emulator — that's when the snapshot gets saved. + script: ./scripts/wait-for-emulator.sh - name: Run instrumented tests timeout-minutes: 15 From 19c9b921cfe195293712d85b3651bb64e2697826 Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Mon, 11 May 2026 17:25:22 +0100 Subject: [PATCH 08/10] re-run CI From de7674e57aa91344df6079f568ea7d53921f440f Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Mon, 11 May 2026 17:58:11 +0100 Subject: [PATCH 09/10] ci: drop unreliable emulator snapshot caching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cross-runner snapshot restore produces a corpse: snapshot loads, sys.boot_completed=1, but system_server is dead on first adb call (DeadSystemException). Same snapshot restores fine within the job that saved it, so the rot is portability — likely the captured Vulkan/RAM guest state is coupled to the host emulator's ICD. Cold-booting the emulator every run trades ~60-90s for reliability. The wait-for-emulator probe still gates the test command. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/android-tests.yml | 29 ++++------------------------- 1 file changed, 4 insertions(+), 25 deletions(-) diff --git a/.github/workflows/android-tests.yml b/.github/workflows/android-tests.yml index 46fadc6..1a479e8 100644 --- a/.github/workflows/android-tests.yml +++ b/.github/workflows/android-tests.yml @@ -158,31 +158,10 @@ jobs: cd apps/example/android ./gradlew :comapeo-core-react-native:assembleDebugAndroidTest :app:assembleDebugAndroidTest --no-daemon -PreactNativeArchitectures=x86_64 - - name: Cache AVD snapshot - uses: actions/cache@v4 - id: avd-cache - with: - path: | - ~/.android/avd/* - ~/.android/adb* - # Bump suffix to invalidate stale snapshots when emulator - # platform changes (Vulkan profile, RAM minimums) break restore. - key: avd-api${{ matrix.api-level }}-google_atd-x86_64-v2 - - - name: Create AVD and generate snapshot - if: steps.avd-cache.outputs.cache-hit != 'true' - uses: reactivecircus/android-emulator-runner@v2 - with: - api-level: ${{ matrix.api-level }} - target: google_atd - arch: x86_64 - force-avd-creation: false - ram-size: 4096M - emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim - disable-animations: false - # Wait until userland is fully up before letting the action - # shut down the emulator — that's when the snapshot gets saved. - script: ./scripts/wait-for-emulator.sh + # Snapshot caching was tried but is not portable across GitHub + # runner machines on this google_atd image: restored snapshots + # boot but system_server is dead-on-arrival (DeadSystemException + # on the first adb input call). Cold boot every run instead. - name: Run instrumented tests timeout-minutes: 15 From 01d962e65c96599e1ba3f568546354ce247afd7d Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Mon, 11 May 2026 22:01:47 +0100 Subject: [PATCH 10/10] ci: drop wait-for-emulator probe; rely on action's built-in wait MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The probe was added to work around a snapshot-restore race where sys.boot_completed=1 came from the saved property store before live services were bound. With snapshot caching dropped, the action's own wait on sys.boot_completed=1 is sufficient — on cold boot that property is only set after system_server posts BOOT_COMPLETED, so core services are already bound. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/android-tests.yml | 2 -- scripts/wait-for-emulator.sh | 51 ----------------------------- 2 files changed, 53 deletions(-) delete mode 100755 scripts/wait-for-emulator.sh diff --git a/.github/workflows/android-tests.yml b/.github/workflows/android-tests.yml index 1a479e8..15f14b9 100644 --- a/.github/workflows/android-tests.yml +++ b/.github/workflows/android-tests.yml @@ -177,8 +177,6 @@ jobs: -noaudio -no-boot-anim disable-animations: false script: | - ./scripts/wait-for-emulator.sh - # Disable animations via settings (more reliable than input keyevent) adb shell settings put global window_animation_scale 0.0 adb shell settings put global transition_animation_scale 0.0 adb shell settings put global animator_duration_scale 0.0 diff --git a/scripts/wait-for-emulator.sh b/scripts/wait-for-emulator.sh deleted file mode 100755 index 6425cfa..0000000 --- a/scripts/wait-for-emulator.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash -set -uo pipefail - -# On snapshot restore, sys.boot_completed flips before system_server -# finishes binding settings/package/activity, so the next -# `adb shell settings put` can fail with "Broken pipe". Poll each service -# until it actually answers. -# -# -e intentionally omitted: an intermittent adb hiccup must not kill the -# probe — we want the loop to keep polling. - -start=$SECONDS -deadline=$((start + 120)) -# Negative seeds so the first iteration always logs. -last_report=-10 -last_svc_dump=-30 - -while (( SECONDS < deadline )); do - # tr -d '\r' because adb echoes CRLF. - bc=$(adb shell getprop sys.boot_completed 2>/dev/null | tr -d '\r') - - adb shell 'pm path android' >/dev/null 2>&1 - pm_rc=$? - - adb shell 'cmd settings list global' >/dev/null 2>&1 - cmd_rc=$? - - if [[ "$bc" == "1" && $pm_rc -eq 0 && $cmd_rc -eq 0 ]]; then - echo "[wait-for-emulator] ready after $(( SECONDS - start ))s" >&2 - exit 0 - fi - - elapsed=$(( SECONDS - start )) - - if (( elapsed - last_report >= 10 )); then - echo "[wait-for-emulator] t=${elapsed}s bc='${bc}' pm_rc=${pm_rc} cmd_rc=${cmd_rc}" >&2 - last_report=$elapsed - fi - - if (( elapsed - last_svc_dump >= 30 )); then - echo "[wait-for-emulator] init.svc.* snapshot at t=${elapsed}s:" >&2 - adb shell getprop 2>/dev/null | grep -E '^\[init\.svc\.' >&2 || true - last_svc_dump=$elapsed - fi - - sleep 1 -done - -echo "::error::Emulator services did not become ready within 120s" >&2 -adb shell getprop 2>/dev/null | grep -E 'boot|svc' >&2 || true -exit 1