From 9741a9c13dfe42016d344dfaacc9a1c6ff0ee1e4 Mon Sep 17 00:00:00 2001 From: Sam Maister Date: Fri, 19 Jun 2026 02:41:17 +0100 Subject: [PATCH] ci(mobile): decouple package smokes from e2e so a flaky e2e can't mask them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mobile package smokes share each e2e job's booted device/Metro/WDA to reuse the infra, running right after the e2e suite under `set -e`. So an e2e failure aborted the step before the smoke ran — and the mobile e2e suites are occasionally flaky (iOS CoreSimulator launch wedges #421; transient Android UiAutomator2 / Dart-VM flakes), which hid whether the PACKED tarball actually installs + composes. Decouple the two within the step: capture each exit code with `|| rc=$?` (so the e2e failure no longer aborts before the smoke) and fail only at the end if either failed. The smoke still runs second, on a device the suite has warmed, so it doesn't inherit the cold-start risk. Kept in one step (not split) because Metro/WDA/sim state lives there. On the iOS legs the step is one bash shell, so the rc vars span lines normally. On Android the whole e2e+smoke+check pipeline stays on ONE line: android-emulator-runner runs the script line-by-line, each in its own `sh -c`, so a var set on one line is gone on the next — split across lines the final check sees empty rc vars and `[ "" != 0 ]` would always exit 1. Applies to all four legs: RN iOS, RN Android, Flutter iOS, Flutter Android. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../_ci-e2e-flutter-ios.reusable.yml | 18 ++++++++++----- .../workflows/_ci-e2e-flutter.reusable.yml | 18 +++++++++------ .../_ci-e2e-react-native-ios.reusable.yml | 23 +++++++++++-------- .../_ci-e2e-react-native.reusable.yml | 23 ++++++++++--------- 4 files changed, 49 insertions(+), 33 deletions(-) diff --git a/.github/workflows/_ci-e2e-flutter-ios.reusable.yml b/.github/workflows/_ci-e2e-flutter-ios.reusable.yml index 9446b9459..a6d52f76b 100644 --- a/.github/workflows/_ci-e2e-flutter-ios.reusable.yml +++ b/.github/workflows/_ci-e2e-flutter-ios.reusable.yml @@ -191,13 +191,19 @@ jobs: FLUTTER_IOS_DEVICE: ${{ inputs.ios-device }} run: | set -euo pipefail - pnpm --filter @repo/e2e run test:e2e:flutter:ios - # Package-install smoke (#387): packed @wdio/flutter-service against THIS booted sim, - # reusing the prebuilt WDA (FLUTTER_WDA_DD, read by the fixture config). test-package.ts - # overwrites the isolated fixture's appium-flutter-driver with the getVMServiceUrl fork - # (AFD_FORK_BUILD_DIR, built above) — the published driver can't reach the Dart VM. + # Run the e2e suite and the package smoke DECOUPLED: capture each exit code with `|| rc=$?` + # (so a failed e2e suite doesn't abort the step before the smoke runs) and fail at the end + # if either failed. The smoke validates the PACKED tarball — its signal must survive an + # (occasionally flaky) e2e failure. It stays second so it runs on a sim the suite warmed. + e2e_rc=0; pnpm --filter @repo/e2e run test:e2e:flutter:ios || e2e_rc=$? + # Packed @wdio/flutter-service against THIS booted sim, reusing the prebuilt WDA + # (FLUTTER_WDA_DD, read by the fixture config). test-package.ts overwrites the isolated + # fixture's appium-flutter-driver with the getVMServiceUrl fork (AFD_FORK_BUILD_DIR, built + # above) — the published driver can't reach the Dart VM. + smoke_rc=0 FLUTTER_PLATFORM=ios FLUTTER_DEVICE_NAME="$FLUTTER_IOS_DEVICE" \ - AFD_FORK_BUILD_DIR="$RUNNER_TEMP/afd/driver/build" pnpm test:package:flutter + AFD_FORK_BUILD_DIR="$RUNNER_TEMP/afd/driver/build" pnpm test:package:flutter || smoke_rc=$? + if [ "$e2e_rc" != 0 ] || [ "$smoke_rc" != 0 ]; then exit 1; fi - name: 📦 Upload Test Logs if: always() diff --git a/.github/workflows/_ci-e2e-flutter.reusable.yml b/.github/workflows/_ci-e2e-flutter.reusable.yml index f009c5730..3fd8fe459 100644 --- a/.github/workflows/_ci-e2e-flutter.reusable.yml +++ b/.github/workflows/_ci-e2e-flutter.reusable.yml @@ -179,13 +179,17 @@ jobs: # not `set -euo pipefail` (no pipefail in dash → aborts the script). script: | set -eu - pnpm --filter @repo/e2e run test:e2e:flutter - # Package-install smoke (#387): validate the PACKED @wdio/flutter-service tarball against - # THIS already-booted emulator. AFD_FORK_BUILD_DIR lets test-package.ts overwrite the - # isolated fixture's appium-flutter-driver with the getVMServiceUrl fork built above (the - # published driver can't reach the Dart VM). No arch split → runs once. Single line: - # android-emulator-runner runs the script line-by-line (each its own `sh -c`). - FLUTTER_PLATFORM=android AFD_FORK_BUILD_DIR="$RUNNER_TEMP/afd/driver/build" pnpm test:package:flutter + # Run the e2e suite and the package smoke DECOUPLED so a flaky e2e can't mask the smoke's + # signal (does the PACKED tarball install + compose?): `|| rc=$?` captures each exit code, + # then we fail at the end only if either failed. The smoke validates the PACKED + # @wdio/flutter-service against THIS already-booted emulator; AFD_FORK_BUILD_DIR lets + # test-package.ts overwrite the isolated fixture's appium-flutter-driver with the + # getVMServiceUrl fork built above (the published driver can't reach the Dart VM); no arch + # split → runs once. + # The WHOLE pipeline MUST be one line: android-emulator-runner runs the script line-by-line, + # each in its own `sh -c`, so a var set on one line is gone on the next — split across lines + # the final check sees empty e2e_rc/smoke_rc and `[ "" != 0 ]` always exits 1. + e2e_rc=0; pnpm --filter @repo/e2e run test:e2e:flutter || e2e_rc=$?; smoke_rc=0; FLUTTER_PLATFORM=android AFD_FORK_BUILD_DIR="$RUNNER_TEMP/afd/driver/build" pnpm test:package:flutter || smoke_rc=$?; if [ "$e2e_rc" != 0 ] || [ "$smoke_rc" != 0 ]; then exit 1; fi - name: 📦 Upload Test Logs if: always() diff --git a/.github/workflows/_ci-e2e-react-native-ios.reusable.yml b/.github/workflows/_ci-e2e-react-native-ios.reusable.yml index ba6b5b1c0..9e8b8321e 100644 --- a/.github/workflows/_ci-e2e-react-native-ios.reusable.yml +++ b/.github/workflows/_ci-e2e-react-native-ios.reusable.yml @@ -232,17 +232,22 @@ jobs: # simulator reaches it over the shared host loopback (no adb-reverse needed on iOS). This # leg is the direct e2e verification of that lifecycle (replaces the manual # start + wait-on + curl-prebundle previously here). - pnpm --filter @repo/e2e run test:e2e:react-native:ios - # Package-install smoke (#387): validate the PACKED @wdio/react-native-service tarball - # against THIS booted simulator + Metro, reusing the prebuilt WDA. test-package.ts installs - # it into the isolated fixture, whose own devDeps provide appium + the xcuitest driver - # (Appium detects drivers from node_modules). The fixture config reads RN_IOS_UDID + - # RN_WDA_DD (already exported) to pin the sim and reuse the WDA. Gated to the new-arch leg - # so it runs once per service (packaging is arch-independent). A normal bash step, so a - # multi-line if/fi is fine here (unlike the android-emulator-runner line-by-line script). + # Run the e2e suite and the package smoke DECOUPLED: capture each exit code with `|| rc=$?` + # (so a failed e2e suite doesn't abort the step before the smoke runs) and fail at the end + # if either failed. The smoke validates the PACKED tarball — its signal must survive an + # (occasionally flaky) e2e failure. It stays second so it runs on a sim+Metro the suite warmed. + e2e_rc=0; pnpm --filter @repo/e2e run test:e2e:react-native:ios || e2e_rc=$? + # Validate the PACKED @wdio/react-native-service tarball against THIS booted simulator + + # Metro, reusing the prebuilt WDA. test-package.ts installs it into the isolated fixture, + # whose own devDeps provide appium + the xcuitest driver (Appium detects drivers from + # node_modules). The fixture config reads RN_IOS_UDID + RN_WDA_DD (already exported) to pin + # the sim and reuse the WDA. Gated to the new-arch leg so it runs once per service + # (packaging is arch-independent). + smoke_rc=0 if [ "${{ inputs.new-arch }}" = "true" ]; then - RN_PLATFORM=ios pnpm test:package:react-native + RN_PLATFORM=ios pnpm test:package:react-native || smoke_rc=$? fi + if [ "$e2e_rc" != 0 ] || [ "$smoke_rc" != 0 ]; then exit 1; fi # The failing session-create (#359) leaves no app/page-source, so the appium debug log is the # primary evidence. Surface its tail inline (the session error is near the end) for a quick diff --git a/.github/workflows/_ci-e2e-react-native.reusable.yml b/.github/workflows/_ci-e2e-react-native.reusable.yml index e380cb6d7..6145323bb 100644 --- a/.github/workflows/_ci-e2e-react-native.reusable.yml +++ b/.github/workflows/_ci-e2e-react-native.reusable.yml @@ -182,17 +182,18 @@ jobs: # onWorkerStart — pre-launch, so the app's first bundle load reaches Metro. This leg is # the direct e2e verification of that lifecycle (replaces the manual adb-reverse + # start + wait-on previously here). - pnpm --filter @repo/e2e run test:e2e:react-native - # Package-install smoke (#387): validate the PACKED @wdio/react-native-service tarball - # installs + composes against THIS already-booted emulator + Metro + APK — the cheap - # "reuse the e2e device" approach (no second emulator). test-package.ts packs the service - # (+ native-mobile-core/cdp-bridge/types) and installs it into the isolated fixture, whose - # own devDeps provide appium + the uiautomator2 driver (Appium detects drivers from - # node_modules, not APPIUM_HOME); the running emulator + Metro + RN_APP_PATH are reused. - # Gated to the new-arch leg so it runs once per service (packaging is arch-independent). - # Single line: android-emulator-runner runs the script line-by-line (each its own - # `sh -c`), so a multi-line if/then/fi splits and the `if` executes without its `fi`. - if [ "${{ inputs.new-arch }}" = "true" ]; then RN_PLATFORM=android pnpm test:package:react-native; fi + # Run the e2e suite and the package smoke DECOUPLED so a flaky e2e can't mask the smoke's + # signal (does the PACKED tarball install + compose?): `|| rc=$?` captures each exit code, + # then we fail at the end only if either failed. The smoke validates the PACKED + # @wdio/react-native-service against THIS already-booted emulator + Metro + APK — the cheap + # "reuse the e2e device" approach (no second emulator); test-package.ts packs the service + # (+ native-mobile-core/cdp-bridge/types) into the isolated fixture, whose own devDeps + # provide appium + the uiautomator2 driver (detected from node_modules), gated to the + # new-arch leg so it runs once per service (packaging is arch-independent). + # The WHOLE pipeline MUST be one line: android-emulator-runner runs the script line-by-line, + # each in its own `sh -c`, so a var set on one line is gone on the next — split across lines + # the final check sees empty e2e_rc/smoke_rc and `[ "" != 0 ]` always exits 1. + e2e_rc=0; pnpm --filter @repo/e2e run test:e2e:react-native || e2e_rc=$?; smoke_rc=0; if [ "${{ inputs.new-arch }}" = "true" ]; then RN_PLATFORM=android pnpm test:package:react-native || smoke_rc=$?; fi; if [ "$e2e_rc" != 0 ] || [ "$smoke_rc" != 0 ]; then exit 1; fi - name: 📦 Upload Test Logs if: always()