From ffa831a126fe7558a9282c1511575b026d9b54b1 Mon Sep 17 00:00:00 2001 From: 283375 Date: Sat, 29 Aug 2026 20:00:06 +0800 Subject: [PATCH 1/7] ci: fix connected test report upload losing core module reports The KMP migration (222b0d8) switched the workflow to 'connectedDebugAndroidTest' and archive only app/shared report dirs. Since :app: has product flavors and :shared: uses the KMP android target, that aggregate task only matched :core:, whose reports were never archived. The artifact directory stayed empty and upload-artifact silently skipped uploading (default warn). - Archive :core: reports alongside app/shared ones - Upload with if-no-files-found: error so an empty archive fails the job instead of silently succeeding --- .github/scripts/connected-android-test.sh | 3 ++- .github/workflows/connected-android-test.yml | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/scripts/connected-android-test.sh b/.github/scripts/connected-android-test.sh index 85ae957a..a46bdd9d 100755 --- a/.github/scripts/connected-android-test.sh +++ b/.github/scripts/connected-android-test.sh @@ -17,9 +17,10 @@ archive_reports() { local target_dir="$OUTPUT_ROOT/$stage" echo "=== Archiving $stage test reports ===" - mkdir -p "$target_dir/app" "$target_dir/shared" + mkdir -p "$target_dir/app" "$target_dir/core" "$target_dir/shared" cp -r app/build/reports/androidTests/connected/* "$target_dir/app/" 2>/dev/null || true + cp -r core/build/reports/androidTests/connected/* "$target_dir/core/" 2>/dev/null || true cp -r shared/build/reports/androidTests/connected/* "$target_dir/shared/" 2>/dev/null || true } diff --git a/.github/workflows/connected-android-test.yml b/.github/workflows/connected-android-test.yml index 3d6c49aa..5400fb9b 100644 --- a/.github/workflows/connected-android-test.yml +++ b/.github/workflows/connected-android-test.yml @@ -73,3 +73,4 @@ jobs: with: name: test-results-api${{ matrix.api-level }}-${{ matrix.target }} path: ${{ steps.ui_tests.outputs.reports-path }} + if-no-files-found: error From 24324602dbe22efa289dee6592707fb71415f960 Mon Sep 17 00:00:00 2001 From: 283375 Date: Sat, 29 Aug 2026 20:09:42 +0800 Subject: [PATCH 2/7] ci: run all module connected tests with explicit task names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 'connectedDebugAndroidTest' only matched :core: — :app: has product flavors and :shared: is a KMP android target, so their device-test tasks have different names and never ran on CI (#48, #49). - app: connectedUnstableDebugAndroidTest (unstable flavor only for now; tests are identical across flavors, and unstable is the main line) - core: connectedDebugAndroidTest - shared: connectedAndroidDeviceTest (no test sources yet, kept so future shared device tests run automatically) Build step assembles the same explicit set before the emulator starts. --- .github/scripts/connected-android-test.sh | 4 ++-- .github/workflows/connected-android-test.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/scripts/connected-android-test.sh b/.github/scripts/connected-android-test.sh index a46bdd9d..29e879a3 100755 --- a/.github/scripts/connected-android-test.sh +++ b/.github/scripts/connected-android-test.sh @@ -26,7 +26,7 @@ archive_reports() { echo "=== Starting Portrait Tests ===" set_orientation 0 -if ! ./gradlew connectedDebugAndroidTest --stacktrace; then +if ! ./gradlew app:connectedUnstableDebugAndroidTest core:connectedDebugAndroidTest shared:connectedAndroidDeviceTest --stacktrace; then echo "❌ Portrait tests failed!" TEST_FAILED=1 fi @@ -34,7 +34,7 @@ archive_reports "portrait" echo "=== Starting Landscape Tests ===" set_orientation 1 -if ! ./gradlew connectedDebugAndroidTest --stacktrace; then +if ! ./gradlew app:connectedUnstableDebugAndroidTest core:connectedDebugAndroidTest shared:connectedAndroidDeviceTest --stacktrace; then echo "❌ Landscape tests failed!" TEST_FAILED=1 fi diff --git a/.github/workflows/connected-android-test.yml b/.github/workflows/connected-android-test.yml index 5400fb9b..baadfdb2 100644 --- a/.github/workflows/connected-android-test.yml +++ b/.github/workflows/connected-android-test.yml @@ -46,7 +46,7 @@ jobs: # Avoid building the application when AVD is running, so Gradle can use more RAM - name: Build Application - run: ./gradlew assembleDebugAndroidTest --stacktrace + run: ./gradlew app:assembleUnstableDebugAndroidTest core:assembleDebugAndroidTest shared:assembleAndroidDeviceTest --stacktrace # android-emulator-runner configurations start here # See also https://github.com/ReactiveCircus/android-emulator-runner#usage--examples From 08dc443990426e8d75ab457084c0830465f3e1c4 Mon Sep 17 00:00:00 2001 From: 283375 Date: Sat, 29 Aug 2026 20:47:03 +0800 Subject: [PATCH 3/7] ci: harden connected test script against silent report loss MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - set -euo pipefail so unexpected failures (e.g. adb unavailable) abort the script instead of limping on - archive_reports now verifies every module listed in TEST_TASKS produced a non-empty report dir before copying, failing loudly when not — previously a missing dir silently produced an empty artifact (commit ffa831a) - deduplicate the gradle task list into TEST_TASKS (portrait/landscape previously repeated it and could drift) - note the API 24 additionalTestOutput warning observed in CI logs --- .github/scripts/connected-android-test.sh | 32 ++++++++++++++++---- .github/workflows/connected-android-test.yml | 1 + 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/.github/scripts/connected-android-test.sh b/.github/scripts/connected-android-test.sh index 29e879a3..5355c7c1 100755 --- a/.github/scripts/connected-android-test.sh +++ b/.github/scripts/connected-android-test.sh @@ -1,8 +1,17 @@ #!/bin/bash +set -euo pipefail OUTPUT_ROOT="build/ci-connected-android-test-reports" echo "reports-path=$OUTPUT_ROOT" >>"$GITHUB_OUTPUT" +# NOTE: on API 24 the app tests log "additionalTestOutput is not supported +# on this device running API level 24" — the OCR additional-test-output +# directory is unavailable on that image. Harmless; tests still pass. + +# Keep in sync with the "Build Application" step in +# .github/workflows/connected-android-test.yml +TEST_TASKS="app:connectedUnstableDebugAndroidTest core:connectedDebugAndroidTest shared:connectedAndroidDeviceTest" + TEST_FAILED=0 set_orientation() { @@ -17,16 +26,27 @@ archive_reports() { local target_dir="$OUTPUT_ROOT/$stage" echo "=== Archiving $stage test reports ===" - mkdir -p "$target_dir/app" "$target_dir/core" "$target_dir/shared" - cp -r app/build/reports/androidTests/connected/* "$target_dir/app/" 2>/dev/null || true - cp -r core/build/reports/androidTests/connected/* "$target_dir/core/" 2>/dev/null || true - cp -r shared/build/reports/androidTests/connected/* "$target_dir/shared/" 2>/dev/null || true + # Every module listed in TEST_TASKS is expected to produce a connected + # test report dir. A missing/empty one means the gradle run claimed + # success without discoverable results — fail loudly instead of + # silently uploading an empty artifact (which actually happened before, + # see commit ffa831a). + local module src + for module in app core shared; do + src="$module/build/reports/androidTests/connected" + if [ ! -d "$src" ] || [ -z "$(ls -A "$src")" ]; then + echo "❌ No connected test reports found for :$module: (expected at $src)" + exit 1 + fi + mkdir -p "$target_dir/$module" + cp -r "$src"/* "$target_dir/$module/" + done } echo "=== Starting Portrait Tests ===" set_orientation 0 -if ! ./gradlew app:connectedUnstableDebugAndroidTest core:connectedDebugAndroidTest shared:connectedAndroidDeviceTest --stacktrace; then +if ! ./gradlew $TEST_TASKS --stacktrace; then echo "❌ Portrait tests failed!" TEST_FAILED=1 fi @@ -34,7 +54,7 @@ archive_reports "portrait" echo "=== Starting Landscape Tests ===" set_orientation 1 -if ! ./gradlew app:connectedUnstableDebugAndroidTest core:connectedDebugAndroidTest shared:connectedAndroidDeviceTest --stacktrace; then +if ! ./gradlew $TEST_TASKS --stacktrace; then echo "❌ Landscape tests failed!" TEST_FAILED=1 fi diff --git a/.github/workflows/connected-android-test.yml b/.github/workflows/connected-android-test.yml index baadfdb2..8764d868 100644 --- a/.github/workflows/connected-android-test.yml +++ b/.github/workflows/connected-android-test.yml @@ -45,6 +45,7 @@ jobs: uses: ./.github/actions/setup-ocr-model # Avoid building the application when AVD is running, so Gradle can use more RAM + # NOTE: keep this task list in sync with TEST_TASKS in .github/scripts/connected-android-test.sh - name: Build Application run: ./gradlew app:assembleUnstableDebugAndroidTest core:assembleDebugAndroidTest shared:assembleAndroidDeviceTest --stacktrace From af1f51ec98ea93677a374a70f5368ae6a42d9892 Mon Sep 17 00:00:00 2001 From: 283375 Date: Sat, 29 Aug 2026 21:01:23 +0800 Subject: [PATCH 4/7] ci: retry once on emulator environment failures with diagnostics API 24 emulators intermittently hang during streamed APK installs (ShellCommandUnresponsiveException -> InstallException: Failed to install-write all apks), randomly hitting whichever module installs next. Seen across CI runs since June regardless of which module is affected. - On failure, check the gradle output against known environment-instability signatures; only then restart adb and retry the stage once. Real test regressions are never retried. - Each attempt's gradle log is kept separately, and logcat + adb devices state are dumped into the artifact on retry for diagnosis. --- .github/scripts/connected-android-test.sh | 61 +++++++++++++++++++++-- 1 file changed, 57 insertions(+), 4 deletions(-) diff --git a/.github/scripts/connected-android-test.sh b/.github/scripts/connected-android-test.sh index 5355c7c1..97791e40 100755 --- a/.github/scripts/connected-android-test.sh +++ b/.github/scripts/connected-android-test.sh @@ -12,6 +12,18 @@ echo "reports-path=$OUTPUT_ROOT" >>"$GITHUB_OUTPUT" # .github/workflows/connected-android-test.yml TEST_TASKS="app:connectedUnstableDebugAndroidTest core:connectedDebugAndroidTest shared:connectedAndroidDeviceTest" +# Environment-instability signatures seen on older API images (e.g. API 24): +# the emulator becomes unresponsive mid-APK-install. When a failure matches +# one of these we retry ONCE after restarting adb. Anything else is treated +# as a real regression and must not be retried. +RETRY_SIGNATURES=( + "ShellCommandUnresponsiveException" + "InstallException" + "install-write" + "device offline" + "device not found" +) + TEST_FAILED=0 set_orientation() { @@ -21,6 +33,49 @@ set_orientation() { adb shell settings put system user_rotation "$rotation" } +is_environment_failure() { + local log=$1 signature + for signature in "${RETRY_SIGNATURES[@]}"; do + if grep -q "$signature" "$log"; then + return 0 + fi + done + return 1 +} + +dump_failure_diagnostics() { + local out="$OUTPUT_ROOT/diagnostics" + mkdir -p "$out" + echo "=== Dumping failure diagnostics to $out ===" + # The device may be half-dead at this point, so tolerate dump failures. + timeout 30 adb logcat -d >"$out/logcat.log" 2>&1 || echo "⚠️ logcat dump failed or timed out" + adb devices -l >"$out/devices.txt" 2>&1 || true +} + +run_stage() { + local stage=$1 orientation=$2 attempt + local log + mkdir -p "$OUTPUT_ROOT" + set_orientation "$orientation" + + for attempt in 1 2; do + log="$OUTPUT_ROOT/gradle-$stage-attempt$attempt.log" + if ./gradlew $TEST_TASKS --stacktrace 2>&1 | tee "$log"; then + return 0 + fi + if [ "$attempt" -eq 1 ] && is_environment_failure "$log"; then + echo "⚠️ Environment failure detected (device unresponsive during install?), retrying once" + dump_failure_diagnostics + adb kill-server || true + adb start-server || true + adb wait-for-device || true + else + return 1 + fi + done + return 1 +} + archive_reports() { local stage=$1 local target_dir="$OUTPUT_ROOT/$stage" @@ -45,16 +100,14 @@ archive_reports() { } echo "=== Starting Portrait Tests ===" -set_orientation 0 -if ! ./gradlew $TEST_TASKS --stacktrace; then +if ! run_stage portrait 0; then echo "❌ Portrait tests failed!" TEST_FAILED=1 fi archive_reports "portrait" echo "=== Starting Landscape Tests ===" -set_orientation 1 -if ! ./gradlew $TEST_TASKS --stacktrace; then +if ! run_stage landscape 1; then echo "❌ Landscape tests failed!" TEST_FAILED=1 fi From 3caf39bb23b1d5db3875074998d020cd4075718d Mon Sep 17 00:00:00 2001 From: 283375 Date: Sat, 29 Aug 2026 21:17:27 +0800 Subject: [PATCH 5/7] ci: also retry on compose hierarchy launch failures "No compose hierarchies found" is the rarer variant of the same environment stall (activity failed to launch on a device that hung mid-install), seen in the July 4 CI run. --- .github/scripts/connected-android-test.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/scripts/connected-android-test.sh b/.github/scripts/connected-android-test.sh index 97791e40..7ab4c6f6 100755 --- a/.github/scripts/connected-android-test.sh +++ b/.github/scripts/connected-android-test.sh @@ -22,6 +22,10 @@ RETRY_SIGNATURES=( "install-write" "device offline" "device not found" + # Rarer variant of the same stall: the activity fails to launch on a + # device that hung mid-install, and Compose tests report it as + # "No compose hierarchies found". + "No compose hierarchies found" ) TEST_FAILED=0 From eaa8cd3532284c76f6418f277680b4e43ab9da5a Mon Sep 17 00:00:00 2001 From: 283375 Date: Sun, 30 Aug 2026 00:44:16 +0800 Subject: [PATCH 6/7] chore: comments --- .github/scripts/connected-android-test.sh | 26 ++++++++--------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/.github/scripts/connected-android-test.sh b/.github/scripts/connected-android-test.sh index 7ab4c6f6..0db7874b 100755 --- a/.github/scripts/connected-android-test.sh +++ b/.github/scripts/connected-android-test.sh @@ -4,27 +4,20 @@ set -euo pipefail OUTPUT_ROOT="build/ci-connected-android-test-reports" echo "reports-path=$OUTPUT_ROOT" >>"$GITHUB_OUTPUT" -# NOTE: on API 24 the app tests log "additionalTestOutput is not supported -# on this device running API level 24" — the OCR additional-test-output -# directory is unavailable on that image. Harmless; tests still pass. - # Keep in sync with the "Build Application" step in # .github/workflows/connected-android-test.yml TEST_TASKS="app:connectedUnstableDebugAndroidTest core:connectedDebugAndroidTest shared:connectedAndroidDeviceTest" -# Environment-instability signatures seen on older API images (e.g. API 24): -# the emulator becomes unresponsive mid-APK-install. When a failure matches -# one of these we retry ONCE after restarting adb. Anything else is treated -# as a real regression and must not be retried. +# Environment-instability signatures seen on older API images (e.g. API 24). +# When a failure matches one of these we retry ONCE after restarting adb. +# Anything else is treated as a real failure and must not be retried. RETRY_SIGNATURES=( "ShellCommandUnresponsiveException" "InstallException" "install-write" "device offline" "device not found" - # Rarer variant of the same stall: the activity fails to launch on a - # device that hung mid-install, and Compose tests report it as - # "No compose hierarchies found". + # This might happen when device hang up or activity launch failed "No compose hierarchies found" ) @@ -64,11 +57,11 @@ run_stage() { for attempt in 1 2; do log="$OUTPUT_ROOT/gradle-$stage-attempt$attempt.log" - if ./gradlew $TEST_TASKS --stacktrace 2>&1 | tee "$log"; then + if ./gradlew "$TEST_TASKS" --stacktrace 2>&1 | tee "$log"; then return 0 fi if [ "$attempt" -eq 1 ] && is_environment_failure "$log"; then - echo "⚠️ Environment failure detected (device unresponsive during install?), retrying once" + echo "⚠️ Environment failure detected, retrying once" dump_failure_diagnostics adb kill-server || true adb start-server || true @@ -88,9 +81,8 @@ archive_reports() { # Every module listed in TEST_TASKS is expected to produce a connected # test report dir. A missing/empty one means the gradle run claimed - # success without discoverable results — fail loudly instead of - # silently uploading an empty artifact (which actually happened before, - # see commit ffa831a). + # success without discoverable results - fail loudly instead of + # silently uploading an empty artifact. local module src for module in app core shared; do src="$module/build/reports/androidTests/connected" @@ -122,4 +114,4 @@ if [ $TEST_FAILED -ne 0 ]; then exit 1 fi -echo "✅ All UI tests passed." +echo "✅ All connected tests passed." From a94844478c70c3f9915dd561b08e620596809798 Mon Sep 17 00:00:00 2001 From: 283375 Date: Sun, 30 Aug 2026 01:22:47 +0800 Subject: [PATCH 7/7] fix: use array for TEST_TASKS --- .github/scripts/connected-android-test.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/scripts/connected-android-test.sh b/.github/scripts/connected-android-test.sh index 0db7874b..ab0433dd 100755 --- a/.github/scripts/connected-android-test.sh +++ b/.github/scripts/connected-android-test.sh @@ -6,7 +6,11 @@ echo "reports-path=$OUTPUT_ROOT" >>"$GITHUB_OUTPUT" # Keep in sync with the "Build Application" step in # .github/workflows/connected-android-test.yml -TEST_TASKS="app:connectedUnstableDebugAndroidTest core:connectedDebugAndroidTest shared:connectedAndroidDeviceTest" +TEST_TASKS=( + "app:connectedUnstableDebugAndroidTest" + "core:connectedDebugAndroidTest" + "shared:connectedAndroidDeviceTest" +) # Environment-instability signatures seen on older API images (e.g. API 24). # When a failure matches one of these we retry ONCE after restarting adb. @@ -57,7 +61,7 @@ run_stage() { for attempt in 1 2; do log="$OUTPUT_ROOT/gradle-$stage-attempt$attempt.log" - if ./gradlew "$TEST_TASKS" --stacktrace 2>&1 | tee "$log"; then + if ./gradlew "${TEST_TASKS[@]}" --stacktrace 2>&1 | tee "$log"; then return 0 fi if [ "$attempt" -eq 1 ] && is_environment_failure "$log"; then