77 - main
88 paths :
99 - ' .github/workflows/expo-native-build.yml'
10+ - ' integration/playwright.expo-native.config.ts'
1011 - ' integration/templates/expo-native/**'
1112 - ' integration/tests/expo-native/**'
1213 - ' packages/expo/**'
2728 SDK_PACK_DIR : /tmp/clerk-expo-pack
2829 E2E_INSTANCE_NAME : clerkstage-with-native-components
2930 BAPI_URL : https://api.clerkstage.dev
30- MAESTRO_VERSION : ' 2.8.0'
31+ # Only keys the runner cache. Keep it equal to the agent-device version
32+ # pinned in the root package.json, or the cache silently stops hitting.
33+ AGENT_DEVICE_VERSION : ' 0.20.10'
3134
3235jobs :
3336 native-build :
@@ -103,16 +106,18 @@ jobs:
103106 # Bump the version when native build commands change.
104107 key : expo-native-build-v1-${{ runner.os }}-${{ matrix.expo-sdk }}-${{ matrix.platform }}-${{ matrix.run-e2e == true && 'e2e' || 'build' }}-${{ env.E2E_INSTANCE_NAME }}-${{ steps.native-build-key.outputs.hash }}
105108
106- - if : steps.native-build-cache.outputs.cache-hit != 'true'
109+ - if : steps.native-build-cache.outputs.cache-hit != 'true' || matrix.run-e2e == true
107110 uses : pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
108- - if : steps.native-build-cache.outputs.cache-hit != 'true'
111+ - if : steps.native-build-cache.outputs.cache-hit != 'true' || matrix.run-e2e == true
109112 uses : actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
110113 with :
111114 node-version : 24.15.0
112115 cache : pnpm
113116
117+ # An e2e job installs even on a native-build cache hit: the suite runs on
118+ # Playwright and touchpress out of the workspace's own node_modules.
114119 - name : Install monorepo dependencies
115- if : steps.native-build-cache.outputs.cache-hit != 'true'
120+ if : steps.native-build-cache.outputs.cache-hit != 'true' || matrix.run-e2e == true
116121 run : pnpm install --frozen-lockfile
117122
118123 - name : Build and pack Clerk packages
@@ -137,8 +142,8 @@ jobs:
137142 GOOGLE_SIGNIN_TARBALL="$(ls "$SDK_PACK_DIR"/clerk-expo-google-signin-*.tgz)"
138143 pnpm add "$SDK_TARBALL" "$GOOGLE_SIGNIN_TARBALL" -w
139144 # expo-dev-client makes even release builds boot into the dev
140- # launcher (unreachable Metro in CI), which stalls every Maestro
141- # flow on a blank screen. Skip it on e2e jobs only.
145+ # launcher (unreachable Metro in CI), which stalls every e2e spec on
146+ # a blank screen. Skip it on e2e jobs only.
142147 DEV_CLIENT="expo-dev-client"
143148 if [ "$RUN_E2E" = "true" ]; then DEV_CLIENT=""; fi
144149 pnpm expo install expo-auth-session expo-constants expo-crypto $DEV_CLIENT expo-secure-store expo-web-browser
@@ -223,32 +228,21 @@ jobs:
223228 path : ${{ steps.native-build-key.outputs.artifact }}
224229 key : ${{ steps.native-build-cache.outputs.cache-primary-key }}
225230
226- - name : Cache maestro CLI
227- if : steps.keys.outputs.pk != ''
231+ - name : Resolve Xcode version
232+ id : xcode
233+ if : matrix.platform == 'ios' && steps.keys.outputs.pk != ''
234+ run : echo "version=$(xcodebuild -version | head -1 | tr ' ' '-')" >> "$GITHUB_OUTPUT"
235+
236+ # agent-device builds an XCTest runner the first time it drives a
237+ # simulator, which costs minutes. Exact key on purpose: agent-device
238+ # advises against restore-key fallbacks here, and prepare recovers a
239+ # stale one anyway.
240+ - name : Cache agent-device Apple runner
241+ if : matrix.platform == 'ios' && steps.keys.outputs.pk != ''
228242 uses : actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
229243 with :
230- path : ~/.maestro
231- key : maestro-${{ runner.os }}-${{ env.MAESTRO_VERSION }}
232-
233- - name : Install maestro CLI
234- if : steps.keys.outputs.pk != ''
235- run : |
236- set -o pipefail
237- if [ -x "$HOME/.maestro/bin/maestro" ]; then
238- echo "Using cached Maestro"
239- else
240- installed=0
241- for i in 1 2 3; do
242- if curl -fLs --retry 3 --retry-delay 5 "https://get.maestro.mobile.dev" | MAESTRO_VERSION="$MAESTRO_VERSION" bash; then
243- if [ -x "$HOME/.maestro/bin/maestro" ]; then installed=1; break; fi
244- fi
245- echo "Maestro install attempt $i failed (or binary missing); retrying"
246- sleep 5
247- done
248- [ "$installed" = 1 ] || { echo "::error::Maestro install failed after 3 attempts"; exit 1; }
249- fi
250- echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
251- "$HOME/.maestro/bin/maestro" --version
244+ path : ~/.agent-device/apple-runner/derived
245+ key : agent-device-runner-${{ runner.os }}-${{ env.AGENT_DEVICE_VERSION }}-${{ steps.xcode.outputs.version }}
252246
253247 - name : Boot iOS simulator
254248 id : sim
@@ -286,7 +280,7 @@ jobs:
286280 echo "user_id=$user_id"
287281 } >> "$GITHUB_OUTPUT"
288282
289- - name : Run iOS e2e (Maestro)
283+ - name : Run iOS e2e
290284 id : run_e2e_ios
291285 if : matrix.platform == 'ios' && steps.user.outputs.user_id != ''
292286 working-directory : ${{ env.FIXTURE_DIR }}
@@ -305,22 +299,28 @@ jobs:
305299 xcrun simctl spawn "$SIM_UDID" defaults write com.apple.keyboard.AutoCorrection -bool NO || true
306300 xcrun simctl spawn "$SIM_UDID" defaults write com.apple.keyboard.Prediction -bool NO || true
307301 # The one-time keyboard tutorial sheets carry their own Continue
308- # button, which can hijack taps on the AuthView's Continue.
302+ # button, which makes the AuthView's Continue ambiguous .
309303 for key in DidShowContinuousPathIntroduction DidShowGestureKeyboardIntroduction KeyboardDidShowProductivityTutorial UIKeyboardDidShowInternationalInfoIntroduction; do
310304 xcrun simctl spawn "$SIM_UDID" defaults write com.apple.keyboard.preferences "$key" -bool YES || true
311305 done
312306 xcrun simctl install "$SIM_UDID" ios/build/Build/Products/Release-iphonesimulator/ClerkExpoNativeBuildFixture.app
313307 # Stream the app's console output into the debug artifact so a hang has
314308 # actionable evidence (keychain/network errors) instead of just screenshots.
315- mkdir -p "$RUNNER_TEMP/maestro -debug"
309+ mkdir -p "$RUNNER_TEMP/e2e -debug"
316310 xcrun simctl spawn "$SIM_UDID" log stream --style compact \
317311 --predicate 'processImagePath CONTAINS "ClerkExpoNativeBuildFixture"' \
318- > "$RUNNER_TEMP/maestro -debug/sim-console.log" 2>&1 &
312+ > "$RUNNER_TEMP/e2e -debug/sim-console.log" 2>&1 &
319313 LOG_PID=$!
320- cd "$GITHUB_WORKSPACE/$E2E_DIR"
314+ cd "$GITHUB_WORKSPACE"
315+ # On a native-build cache hit no xcodebuild has run, so without this
316+ # the first snapshot pays for the cold XCTest runner build and the
317+ # launch times out. This is the only thing the repo still needs
318+ # agent-device itself for; the specs reach the device through
319+ # touchpress. Pinned to touchpress's own version, because a CLI at a
320+ # different version replaces the daemon and drops its sessions.
321+ ./node_modules/.bin/agent-device prepare ios-runner --platform ios --udid "$SIM_UDID" --timeout 600000
321322 rc=0
322- MAESTRO_DEBUG_OUTPUT="$RUNNER_TEMP/maestro-debug" ./run-flows.sh \
323- xcrun simctl terminate "$SIM_UDID" com.clerk.exponativebuildfixture || rc=$?
323+ pnpm test:integration:expo-native --project=ios || rc=$?
324324 kill "$LOG_PID" 2>/dev/null || true
325325 exit "$rc"
326326
@@ -353,7 +353,7 @@ jobs:
353353 disable-animations : false
354354 script : echo "Generated AVD snapshot for caching."
355355
356- - name : Run Android e2e (Maestro)
356+ - name : Run Android e2e
357357 id : run_e2e_android
358358 if : matrix.platform == 'android' && steps.user.outputs.user_id != ''
359359 uses : reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2
@@ -370,9 +370,11 @@ jobs:
370370 # The action runs each script line in a separate sh -c; the folded
371371 # scalar (>-) plus && keeps everything in one shell invocation.
372372 script : >-
373- cd integration/tests/expo-native &&
374- MAESTRO_DEBUG_OUTPUT="$RUNNER_TEMP/maestro-debug" ./run-android-flows.sh
375- "$GITHUB_WORKSPACE/$FIXTURE_DIR/android/app/build/outputs/apk/release/app-release.apk"
373+ mkdir -p "$RUNNER_TEMP/e2e-debug" &&
374+ adb install -r "$GITHUB_WORKSPACE/$FIXTURE_DIR/android/app/build/outputs/apk/release/app-release.apk" &&
375+ adb logcat -c &&
376+ (adb logcat -v threadtime > "$RUNNER_TEMP/e2e-debug/android-logcat.log" 2>&1 &) &&
377+ pnpm test:integration:expo-native --project=android
376378
377379 # Runs before the artifact upload so the credentials are already dead
378380 # by the time the debug files become downloadable.
@@ -385,26 +387,30 @@ jobs:
385387 curl -fsS -X DELETE "$BAPI_URL/v1/users/$USER_ID" \
386388 -H "Authorization: Bearer $CLERK_SECRET_KEY" || true
387389
388- # Test reports record flow env (and typed input) in plaintext;
389- # add-mask only covers step logs, not artifact contents.
390- - name : Scrub test credentials from Maestro debug output
390+ # A driver error can quote the text it was handed, and add-mask only
391+ # covers step logs, not artifact contents.
392+ - name : Scrub test credentials from the e2e report
391393 if : always() && (steps.run_e2e_ios.outcome == 'failure' || steps.run_e2e_android.outcome == 'failure')
392394 env :
393395 CLERK_TEST_PASSWORD : ${{ steps.user.outputs.password }}
394396 run : |
395- [ -d "$RUNNER_TEMP/maestro-debug" ] || exit 0
396397 [ -n "$CLERK_TEST_PASSWORD" ] || exit 0
398+ mkdir -p "$RUNNER_TEMP/e2e-debug"
399+ for dir in "$E2E_DIR/playwright-report" "$E2E_DIR/test-results"; do
400+ [ -d "$dir" ] || continue
401+ cp -R "$dir" "$RUNNER_TEMP/e2e-debug/"
402+ done
397403 # shellcheck disable=SC2016
398- find "$RUNNER_TEMP/maestro -debug" -type f \
399- \( -name '*.html' -o -name '*.json' -o -name '*.log' -o -name '*.txt' -o -name '*.xml' -o -name '*.yaml' \) \
404+ find "$RUNNER_TEMP/e2e -debug" -type f \
405+ \( -name '*.html' -o -name '*.json' -o -name '*.jsonl' -o -name '*.md' -o -name '*. log' -o -name '*.txt' -o -name '*.xml' -o -name '*.yaml' -o -name '*.zip ' \) \
400406 -exec perl -pi -e 's/\Q$ENV{CLERK_TEST_PASSWORD}\E/[REDACTED]/g' {} +
401407
402- - name : Upload Maestro artifacts on e2e failure
408+ - name : Upload e2e artifacts on failure
403409 if : always() && (steps.run_e2e_ios.outcome == 'failure' || steps.run_e2e_android.outcome == 'failure')
404410 uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
405411 with :
406- name : maestro -${{ matrix.platform }}
407- path : ${{ runner.temp }}/maestro -debug
412+ name : expo-native-e2e -${{ matrix.platform }}
413+ path : ${{ runner.temp }}/e2e -debug
408414 retention-days : 7
409415
410416 - name : Report e2e outcome
@@ -415,7 +421,7 @@ jobs:
415421 run : |
416422 outcome="$IOS_OUTCOME"
417423 [ "$outcome" = "skipped" ] && outcome="$ANDROID_OUTCOME"
418- echo "## Maestro e2e (${{ matrix.platform }}): $outcome" >> "$GITHUB_STEP_SUMMARY"
424+ echo "## Expo native e2e (${{ matrix.platform }}): $outcome" >> "$GITHUB_STEP_SUMMARY"
419425 if [ "$outcome" = "failure" ]; then
420- echo "::error::Maestro e2e failed. See the maestro -${{ matrix.platform }} artifact."
426+ echo "::error::Expo native e2e failed. See the expo-native-e2e -${{ matrix.platform }} artifact."
421427 fi
0 commit comments