88 paths :
99 - ' .github/workflows/expo-native-build.yml'
1010 - ' integration/templates/expo-native/**'
11+ - ' integration/tests/expo-native/**'
1112 - ' packages/expo/**'
1213 workflow_dispatch :
1314
@@ -20,15 +21,19 @@ concurrency:
2021
2122env :
2223 FIXTURE_DIR : integration/templates/expo-native
24+ E2E_DIR : integration/tests/expo-native
2325 FIXTURE_PUBLISHABLE_KEY : pk_test_ZHVtbXkuY2xlcmsuYWNjb3VudHMuZGV2JA
2426 SDK_PACK_DIR : /tmp/clerk-expo-pack
27+ E2E_INSTANCE_NAME : clerkstage-with-native-components
28+ BAPI_URL : https://api.clerkstage.dev
29+ MAESTRO_VERSION : ' 2.6.1'
2530
2631jobs :
2732 native-build :
2833 if : ${{ github.head_ref != 'changeset-release/main' }}
2934 name : Expo ${{ matrix.expo-sdk }} / ${{ matrix.platform }}
3035 runs-on : ${{ matrix.runner }}
31- timeout-minutes : 45
36+ timeout-minutes : ${{ matrix.run-e2e == true && 60 || 45 }}
3237 strategy :
3338 fail-fast : false
3439 matrix :
4853 - expo-sdk : 57
4954 platform : android
5055 runner : blacksmith-8vcpu-ubuntu-2204
56+ run-e2e : true
5157 - expo-sdk : 57
5258 platform : ios
5359 runner : blacksmith-6vcpu-macos-26
60+ run-e2e : true
5461
5562 steps :
5663 - name : Checkout repo
@@ -72,27 +79,51 @@ jobs:
7279 pnpm --filter @clerk/expo... build
7380 mkdir -p "$SDK_PACK_DIR"
7481 pnpm --filter @clerk/expo pack --pack-destination "$SDK_PACK_DIR"
75- SDK_TARBALL="$(ls "$SDK_PACK_DIR"/clerk-expo-*.tgz)"
76- if tar -tf "$SDK_TARBALL" | grep -q '^package/e2e/'; then
77- echo "Fixture files must not be packed into @clerk/expo"
78- exit 1
79- fi
8082
8183 - name : Install fixture dependencies
8284 working-directory : ${{ env.FIXTURE_DIR }}
8385 env :
8486 EXPO_SDK : ${{ matrix.expo-sdk }}
87+ RUN_E2E : ${{ matrix.run-e2e == true && 'true' || 'false' }}
8588 run : |
8689 cp "package.sdk-$EXPO_SDK.json" package.json
8790 pnpm install --no-frozen-lockfile
8891 SDK_TARBALL="$(ls "$SDK_PACK_DIR"/clerk-expo-*.tgz)"
8992 pnpm add "$SDK_TARBALL" -w
90- pnpm expo install expo-auth-session expo-constants expo-crypto expo-dev-client expo-secure-store expo-web-browser
93+ # expo-dev-client makes even release builds boot into the dev
94+ # launcher (unreachable Metro in CI), which stalls every Maestro
95+ # flow on a blank screen. Skip it on e2e jobs only.
96+ DEV_CLIENT="expo-dev-client"
97+ if [ "$RUN_E2E" = "true" ]; then DEV_CLIENT=""; fi
98+ pnpm expo install expo-auth-session expo-constants expo-crypto $DEV_CLIENT expo-secure-store expo-web-browser
99+
100+ - name : Check e2e secret availability
101+ id : gate
102+ if : matrix.run-e2e == true
103+ env :
104+ KEYS : ${{ secrets.INTEGRATION_STAGING_INSTANCE_KEYS }}
105+ run : |
106+ if [ -n "$KEYS" ]; then
107+ echo "ok=true" >> "$GITHUB_OUTPUT"
108+ else
109+ echo "ok=false" >> "$GITHUB_OUTPUT"
110+ echo "::notice::INTEGRATION_STAGING_INSTANCE_KEYS unavailable (fork PR?); running build-only."
111+ fi
112+
113+ - name : Resolve Clerk instance keys
114+ id : keys
115+ if : steps.gate.outputs.ok == 'true'
116+ continue-on-error : true
117+ env :
118+ INTEGRATION_STAGING_INSTANCE_KEYS : ${{ secrets.INTEGRATION_STAGING_INSTANCE_KEYS }}
119+ run : node scripts/resolve-instance-keys.mjs INTEGRATION_STAGING_INSTANCE_KEYS "$E2E_INSTANCE_NAME"
91120
92121 - name : Write fixture .env
93122 working-directory : ${{ env.FIXTURE_DIR }}
123+ env :
124+ E2E_PK : ${{ steps.keys.outputs.pk }}
94125 run : |
95- echo "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=$FIXTURE_PUBLISHABLE_KEY" > .env
126+ echo "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=${E2E_PK:-$ FIXTURE_PUBLISHABLE_KEY} " > .env
96127
97128 - name : Set up JDK 17
98129 if : ${{ matrix.platform == 'android' }}
@@ -120,3 +151,201 @@ jobs:
120151 if : ${{ matrix.platform == 'ios' }}
121152 working-directory : ${{ env.FIXTURE_DIR }}
122153 run : pnpm build:ios
154+
155+ - name : Cache Maestro
156+ if : steps.keys.outputs.pk != ''
157+ uses : actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
158+ with :
159+ path : ~/.maestro
160+ key : maestro-${{ runner.os }}-${{ env.MAESTRO_VERSION }}
161+
162+ - name : Install Maestro
163+ if : steps.keys.outputs.pk != ''
164+ # pipefail + binary check so a curl flake doesn't leave a missing
165+ # binary that only surfaces later as exit-code-127.
166+ run : |
167+ set -o pipefail
168+ if [ -x "$HOME/.maestro/bin/maestro" ]; then
169+ echo "Using cached Maestro"
170+ else
171+ installed=0
172+ for i in 1 2 3; do
173+ if curl -fLs --retry 3 --retry-delay 5 "https://get.maestro.mobile.dev" | MAESTRO_VERSION="$MAESTRO_VERSION" bash; then
174+ if [ -x "$HOME/.maestro/bin/maestro" ]; then installed=1; break; fi
175+ fi
176+ echo "Maestro install attempt $i failed (or binary missing); retrying"
177+ sleep 5
178+ done
179+ [ "$installed" = 1 ] || { echo "::error::Maestro install failed after 3 attempts"; exit 1; }
180+ fi
181+ echo "$HOME/.maestro/bin" >> "$GITHUB_PATH"
182+ "$HOME/.maestro/bin/maestro" --version
183+
184+ - name : Boot iOS simulator
185+ id : sim
186+ if : matrix.platform == 'ios' && steps.keys.outputs.pk != ''
187+ uses : futureware-tech/simulator-action@e89aa8f93d3aec35083ff49d2854d07f7186f7f5 # v5
188+ with :
189+ model : ' iPhone 17 Pro'
190+
191+ # Provisioned as late as possible (after the expensive native build and
192+ # simulator boot) so live credentials exist for the shortest window.
193+ - name : Provision test user via BAPI
194+ id : user
195+ if : steps.keys.outputs.pk != ''
196+ continue-on-error : true
197+ env :
198+ CLERK_SECRET_KEY : ${{ steps.keys.outputs.sk }}
199+ run : |
200+ email="ci-${GITHUB_RUN_ID}-${RANDOM}[email protected] " 201+ username="ci_${GITHUB_RUN_ID}_${RANDOM}"
202+ password="ClerkCI!$(openssl rand -hex 8)Aa1"
203+ http_code=$(curl -sS -o /tmp/bapi_response.json -w "%{http_code}" -X POST "$BAPI_URL/v1/users" \
204+ -H "Authorization: Bearer $CLERK_SECRET_KEY" \
205+ -H "Content-Type: application/json" \
206+ -d "{\"email_address\":[\"$email\"],\"username\":\"$username\",\"password\":\"$password\"}")
207+ if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
208+ echo "::error::BAPI user creation failed (HTTP $http_code)"
209+ cat /tmp/bapi_response.json
210+ exit 1
211+ fi
212+ user_id=$(jq -er '.id' /tmp/bapi_response.json)
213+ echo "::add-mask::$password"
214+ {
215+ echo "email=$email"
216+ echo "password=$password"
217+ echo "user_id=$user_id"
218+ } >> "$GITHUB_OUTPUT"
219+
220+ - name : Run iOS e2e (Maestro)
221+ id : run_e2e_ios
222+ if : matrix.platform == 'ios' && steps.user.outputs.user_id != ''
223+ # Burn-in: e2e cannot fail the check yet. Flip to hard-fail in a
224+ # follow-up once the suite has proven quiet.
225+ continue-on-error : true
226+ working-directory : ${{ env.FIXTURE_DIR }}
227+ env :
228+ CLERK_TEST_EMAIL : ${{ steps.user.outputs.email }}
229+ CLERK_TEST_PASSWORD : ${{ steps.user.outputs.password }}
230+ SIM_UDID : ${{ steps.sim.outputs.udid }}
231+ run : |
232+ echo "Using simulator $SIM_UDID"
233+ # Kill animations + predictive keyboard: animations add latency to
234+ # every tap; predictive text hijacks inputText targets.
235+ xcrun simctl spawn "$SIM_UDID" defaults write com.apple.UIKit UIAnimationDragCoefficient -float 0.01 || true
236+ xcrun simctl spawn "$SIM_UDID" defaults write -g ApplePersistenceIgnoreState -bool YES || true
237+ xcrun simctl spawn "$SIM_UDID" defaults write com.apple.keyboard.ContinuousPath -bool NO || true
238+ xcrun simctl spawn "$SIM_UDID" defaults write com.apple.keyboard.AutoCapitalization -bool NO || true
239+ xcrun simctl spawn "$SIM_UDID" defaults write com.apple.keyboard.AutoCorrection -bool NO || true
240+ xcrun simctl spawn "$SIM_UDID" defaults write com.apple.keyboard.Prediction -bool NO || true
241+ xcrun simctl install "$SIM_UDID" ios/build/Build/Products/Release-iphonesimulator/ClerkExpoNativeBuildFixture.app
242+ # Stream the app's console output into the debug artifact so a hang has
243+ # actionable evidence (keychain/network errors) instead of just screenshots.
244+ mkdir -p "$RUNNER_TEMP/maestro-debug"
245+ xcrun simctl spawn "$SIM_UDID" log stream --style compact \
246+ --predicate 'processImagePath CONTAINS "ClerkExpoNativeBuildFixture"' \
247+ > "$RUNNER_TEMP/maestro-debug/sim-console.log" 2>&1 &
248+ LOG_PID=$!
249+ cd "$GITHUB_WORKSPACE/$E2E_DIR"
250+ rc=0
251+ MAESTRO_DEBUG_OUTPUT="$RUNNER_TEMP/maestro-debug" ./run-flows.sh \
252+ xcrun simctl terminate "$SIM_UDID" com.clerk.exponativebuildfixture || rc=$?
253+ kill "$LOG_PID" 2>/dev/null || true
254+ exit "$rc"
255+
256+ - name : Enable KVM
257+ if : matrix.platform == 'android' && steps.user.outputs.user_id != ''
258+ run : |
259+ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
260+ sudo udevadm control --reload-rules
261+ sudo udevadm trigger --name-match=kvm
262+
263+ - name : AVD cache
264+ id : avd-cache
265+ if : matrix.platform == 'android' && steps.user.outputs.user_id != ''
266+ uses : actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
267+ with :
268+ path : |
269+ ~/.android/avd/*
270+ ~/.android/adb*
271+ key : avd-34-google_apis-x86_64-v1
272+
273+ - name : Create AVD snapshot
274+ if : matrix.platform == 'android' && steps.user.outputs.user_id != '' && steps.avd-cache.outputs.cache-hit != 'true'
275+ uses : reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2
276+ with :
277+ api-level : 34
278+ target : google_apis
279+ arch : x86_64
280+ force-avd-creation : false
281+ emulator-options : -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim
282+ disable-animations : false
283+ script : echo "Generated AVD snapshot for caching."
284+
285+ - name : Run Android e2e (Maestro)
286+ id : run_e2e_android
287+ if : matrix.platform == 'android' && steps.user.outputs.user_id != ''
288+ continue-on-error : true
289+ uses : reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2
290+ env :
291+ CLERK_TEST_EMAIL : ${{ steps.user.outputs.email }}
292+ CLERK_TEST_PASSWORD : ${{ steps.user.outputs.password }}
293+ with :
294+ api-level : 34
295+ target : google_apis
296+ arch : x86_64
297+ force-avd-creation : false
298+ emulator-options : -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim
299+ disable-animations : true
300+ # The action runs each script line in a separate sh -c; the folded
301+ # scalar (>-) plus && keeps everything in one shell invocation.
302+ script : >-
303+ adb install -r integration/templates/expo-native/android/app/build/outputs/apk/release/app-release.apk &&
304+ cd integration/tests/expo-native &&
305+ MAESTRO_DEBUG_OUTPUT="$RUNNER_TEMP/maestro-debug" ./run-flows.sh adb shell am force-stop com.clerk.exponativebuildfixture
306+
307+ # Runs before the artifact upload so the credentials are already dead
308+ # by the time the debug files become downloadable.
309+ - name : Cleanup test user
310+ if : always() && steps.user.outputs.user_id != ''
311+ env :
312+ CLERK_SECRET_KEY : ${{ steps.keys.outputs.sk }}
313+ USER_ID : ${{ steps.user.outputs.user_id }}
314+ run : |
315+ curl -fsS -X DELETE "$BAPI_URL/v1/users/$USER_ID" \
316+ -H "Authorization: Bearer $CLERK_SECRET_KEY" || true
317+
318+ # Maestro debug files record flow env (and typed input) in plaintext;
319+ # add-mask only covers step logs, not artifact contents.
320+ - name : Scrub test credentials from Maestro debug output
321+ if : always() && (steps.run_e2e_ios.outcome == 'failure' || steps.run_e2e_android.outcome == 'failure')
322+ env :
323+ CLERK_TEST_PASSWORD : ${{ steps.user.outputs.password }}
324+ run : |
325+ [ -d "$RUNNER_TEMP/maestro-debug" ] || exit 0
326+ [ -n "$CLERK_TEST_PASSWORD" ] || exit 0
327+ # shellcheck disable=SC2016
328+ find "$RUNNER_TEMP/maestro-debug" -type f \
329+ \( -name '*.json' -o -name '*.log' -o -name '*.txt' -o -name '*.yaml' \) \
330+ -exec perl -pi -e 's/\Q$ENV{CLERK_TEST_PASSWORD}\E/[REDACTED]/g' {} +
331+
332+ - name : Upload Maestro artifacts on e2e failure
333+ if : always() && (steps.run_e2e_ios.outcome == 'failure' || steps.run_e2e_android.outcome == 'failure')
334+ uses : actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
335+ with :
336+ name : maestro-${{ matrix.platform }}-sdk${{ matrix.expo-sdk }}
337+ path : ${{ runner.temp }}/maestro-debug
338+ retention-days : 7
339+
340+ - name : Report e2e outcome
341+ if : always() && steps.user.outputs.user_id != ''
342+ env :
343+ IOS_OUTCOME : ${{ steps.run_e2e_ios.outcome }}
344+ ANDROID_OUTCOME : ${{ steps.run_e2e_android.outcome }}
345+ run : |
346+ outcome="$IOS_OUTCOME"
347+ [ "$outcome" = "skipped" ] && outcome="$ANDROID_OUTCOME"
348+ echo "## Maestro e2e (${{ matrix.platform }}, SDK ${{ matrix.expo-sdk }}): $outcome" >> "$GITHUB_STEP_SUMMARY"
349+ if [ "$outcome" = "failure" ]; then
350+ echo "::warning::Maestro e2e failed (burn-in mode: not failing the job). See the maestro-${{ matrix.platform }}-sdk${{ matrix.expo-sdk }} artifact."
351+ fi
0 commit comments