Skip to content

Commit 004cab7

Browse files
authored
ci(e2e): Add Expo native E2E testing (#9186)
1 parent 89916b0 commit 004cab7

18 files changed

Lines changed: 580 additions & 257 deletions

‎.changeset/new-queens-brake.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

‎.github/workflows/expo-native-build.yml‎

Lines changed: 237 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ on:
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

2122
env:
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

2631
jobs:
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:
@@ -48,9 +53,11 @@ jobs:
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

Comments
 (0)