From 28fb6bc8b797b95a91dd4b8802873364fbdb7b47 Mon Sep 17 00:00:00 2001 From: ChrisCanin Date: Wed, 6 May 2026 11:14:47 -0700 Subject: [PATCH 1/3] ci(mobile-e2e): add manual workflow_dispatch for @clerk/expo native components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lands the workflow file ahead of #8334 so that 'workflow_dispatch' shows up in the Actions UI and can be dispatched against the feature branch for end-to-end validation before the larger PR is merged. The workflow itself does nothing on push or PR — it is manual-only. Test infrastructure (Maestro flows, integration-mobile/, packages/expo unit tests) lives in #8334 and will be merged separately. --- .github/workflows/mobile-e2e.yml | 267 +++++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 .github/workflows/mobile-e2e.yml diff --git a/.github/workflows/mobile-e2e.yml b/.github/workflows/mobile-e2e.yml new file mode 100644 index 00000000000..df41e06a70a --- /dev/null +++ b/.github/workflows/mobile-e2e.yml @@ -0,0 +1,267 @@ +# Manual mobile e2e for @clerk/expo native components. +# Clones clerk-expo-quickstart, builds the NativeComponentQuickstart app, +# and runs Maestro flows on iOS simulator and Android emulator. +# +# Secrets: +# INTEGRATION_INSTANCE_KEYS — JSON map of named test instances +# ({ "": { "pk": "pk_test_...", "sk": "sk_test_..." } }). +# Same secret used by /integration (Playwright). We read the entry named +# EXPO_INSTANCE_NAME (set in env: below). +# +# Test users are provisioned per-run via Clerk Backend API and deleted at +# teardown — same pattern as /integration's createBapiUser. +# +# TODO(SDK team): confirm the instance-name slot to add inside +# INTEGRATION_INSTANCE_KEYS for this workflow (placeholder: "expo-native"). +name: "Mobile e2e (@clerk/expo)" + +on: + workflow_dispatch: + inputs: + quickstart_ref: + description: "clerk-expo-quickstart git ref (branch, tag, or SHA)" + required: false + default: "main" + exclude_tags: + description: "Maestro tags to exclude (comma-separated)" + required: false + default: "manual,skip" + +env: + # TODO(SDK team): replace with the canonical mobile-e2e instance name once confirmed. + EXPO_INSTANCE_NAME: expo-native + +concurrency: + group: mobile-e2e-${{ github.ref }} + cancel-in-progress: true + +jobs: + android: + name: Android + runs-on: ubuntu-latest + timeout-minutes: 45 + defaults: + run: + working-directory: . + steps: + - name: Checkout @clerk/javascript + uses: actions/checkout@v4 + + - name: Checkout clerk-expo-quickstart + uses: actions/checkout@v4 + with: + repository: clerk/clerk-expo-quickstart + ref: ${{ inputs.quickstart_ref }} + path: clerk-expo-quickstart + + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Install monorepo deps + run: pnpm install --frozen-lockfile + + - name: Build @clerk/expo + run: pnpm turbo build --filter=@clerk/expo... + + - name: Install quickstart deps + working-directory: clerk-expo-quickstart/NativeComponentQuickstart + run: pnpm install + + - name: Resolve Clerk instance keys + id: keys + env: + INTEGRATION_INSTANCE_KEYS: ${{ secrets.INTEGRATION_INSTANCE_KEYS }} + run: | + if [ -z "$INTEGRATION_INSTANCE_KEYS" ]; then + echo "::error::INTEGRATION_INSTANCE_KEYS secret is not set" + exit 1 + fi + pk=$(echo "$INTEGRATION_INSTANCE_KEYS" | jq -er ".[\"$EXPO_INSTANCE_NAME\"].pk") || { + echo "::error::No entry '$EXPO_INSTANCE_NAME' found in INTEGRATION_INSTANCE_KEYS" + exit 1 + } + sk=$(echo "$INTEGRATION_INSTANCE_KEYS" | jq -er ".[\"$EXPO_INSTANCE_NAME\"].sk") + echo "::add-mask::$sk" + echo "pk=$pk" >> "$GITHUB_OUTPUT" + echo "sk=$sk" >> "$GITHUB_OUTPUT" + + - name: Write quickstart .env + working-directory: clerk-expo-quickstart/NativeComponentQuickstart + run: | + echo "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=${{ steps.keys.outputs.pk }}" > .env + + - name: Provision test user via BAPI + id: user + env: + CLERK_SECRET_KEY: ${{ steps.keys.outputs.sk }} + run: | + email="ci-${GITHUB_RUN_ID}-${RANDOM}+clerk_test@clerkcookie.com" + password="ClerkCI!$(openssl rand -hex 8)Aa1" + response=$(curl -fsS -X POST https://api.clerk.com/v1/users \ + -H "Authorization: Bearer $CLERK_SECRET_KEY" \ + -H "Content-Type: application/json" \ + -d "{\"email_address\":[\"$email\"],\"password\":\"$password\"}") + user_id=$(echo "$response" | jq -er '.id') + echo "::add-mask::$password" + echo "email=$email" >> "$GITHUB_OUTPUT" + echo "password=$password" >> "$GITHUB_OUTPUT" + echo "user_id=$user_id" >> "$GITHUB_OUTPUT" + + - name: Set up JDK 17 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + + - name: Install Maestro + run: | + curl -Ls "https://get.maestro.mobile.dev" | bash + echo "$HOME/.maestro/bin" >> "$GITHUB_PATH" + + - name: Run Android e2e + uses: reactivecircus/android-emulator-runner@v2 + env: + CLERK_TEST_EMAIL: ${{ steps.user.outputs.email }} + CLERK_TEST_PASSWORD: ${{ steps.user.outputs.password }} + with: + api-level: 34 + target: google_apis + arch: x86_64 + script: | + cd clerk-expo-quickstart/NativeComponentQuickstart + npx expo prebuild --clean + npx expo run:android --variant release --no-bundler + cd ../../integration-mobile + # Maestro doesn't auto-recurse into subdirectories; pass each flow explicitly. + find flows -type f -name "*.yaml" ! -path "*/common/*" -print0 | \ + xargs -0 maestro test --exclude-tags "${{ inputs.exclude_tags }}" + + - name: Upload Maestro artifacts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: maestro-android + path: ~/.maestro/tests + + - name: Cleanup test user + if: always() && steps.user.outputs.user_id != '' + env: + CLERK_SECRET_KEY: ${{ steps.keys.outputs.sk }} + USER_ID: ${{ steps.user.outputs.user_id }} + run: | + curl -fsS -X DELETE "https://api.clerk.com/v1/users/$USER_ID" \ + -H "Authorization: Bearer $CLERK_SECRET_KEY" || true + + ios: + name: iOS + runs-on: macos-15 + timeout-minutes: 60 + steps: + - name: Checkout @clerk/javascript + uses: actions/checkout@v4 + + - name: Checkout clerk-expo-quickstart + uses: actions/checkout@v4 + with: + repository: clerk/clerk-expo-quickstart + ref: ${{ inputs.quickstart_ref }} + path: clerk-expo-quickstart + + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: pnpm + + - name: Install monorepo deps + run: pnpm install --frozen-lockfile + + - name: Build @clerk/expo + run: pnpm turbo build --filter=@clerk/expo... + + - name: Install quickstart deps + working-directory: clerk-expo-quickstart/NativeComponentQuickstart + run: pnpm install + + - name: Resolve Clerk instance keys + id: keys + env: + INTEGRATION_INSTANCE_KEYS: ${{ secrets.INTEGRATION_INSTANCE_KEYS }} + run: | + if [ -z "$INTEGRATION_INSTANCE_KEYS" ]; then + echo "::error::INTEGRATION_INSTANCE_KEYS secret is not set" + exit 1 + fi + pk=$(echo "$INTEGRATION_INSTANCE_KEYS" | jq -er ".[\"$EXPO_INSTANCE_NAME\"].pk") || { + echo "::error::No entry '$EXPO_INSTANCE_NAME' found in INTEGRATION_INSTANCE_KEYS" + exit 1 + } + sk=$(echo "$INTEGRATION_INSTANCE_KEYS" | jq -er ".[\"$EXPO_INSTANCE_NAME\"].sk") + echo "::add-mask::$sk" + echo "pk=$pk" >> "$GITHUB_OUTPUT" + echo "sk=$sk" >> "$GITHUB_OUTPUT" + + - name: Write quickstart .env + working-directory: clerk-expo-quickstart/NativeComponentQuickstart + run: | + echo "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=${{ steps.keys.outputs.pk }}" > .env + + - name: Provision test user via BAPI + id: user + env: + CLERK_SECRET_KEY: ${{ steps.keys.outputs.sk }} + run: | + email="ci-${GITHUB_RUN_ID}-${RANDOM}+clerk_test@clerkcookie.com" + password="ClerkCI!$(openssl rand -hex 8)Aa1" + response=$(curl -fsS -X POST https://api.clerk.com/v1/users \ + -H "Authorization: Bearer $CLERK_SECRET_KEY" \ + -H "Content-Type: application/json" \ + -d "{\"email_address\":[\"$email\"],\"password\":\"$password\"}") + user_id=$(echo "$response" | jq -er '.id') + echo "::add-mask::$password" + echo "email=$email" >> "$GITHUB_OUTPUT" + echo "password=$password" >> "$GITHUB_OUTPUT" + echo "user_id=$user_id" >> "$GITHUB_OUTPUT" + + - name: Cache SPM + uses: actions/cache@v4 + with: + path: ~/Library/Developer/Xcode/DerivedData + key: spm-${{ hashFiles('packages/expo/package.json') }} + + - name: Install Maestro + run: | + curl -Ls "https://get.maestro.mobile.dev" | bash + echo "$HOME/.maestro/bin" >> "$GITHUB_PATH" + + - name: Build and run iOS e2e + env: + CLERK_TEST_EMAIL: ${{ steps.user.outputs.email }} + CLERK_TEST_PASSWORD: ${{ steps.user.outputs.password }} + run: | + cd clerk-expo-quickstart/NativeComponentQuickstart + npx expo prebuild --clean + npx expo run:ios --configuration Release --no-bundler + cd ../../integration-mobile + # Maestro doesn't auto-recurse into subdirectories; pass each flow explicitly. + find flows -type f -name "*.yaml" ! -path "*/common/*" -print0 | \ + xargs -0 maestro test --exclude-tags "${{ inputs.exclude_tags }},androidOnly" + + - name: Upload Maestro artifacts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: maestro-ios + path: ~/.maestro/tests + + - name: Cleanup test user + if: always() && steps.user.outputs.user_id != '' + env: + CLERK_SECRET_KEY: ${{ steps.keys.outputs.sk }} + USER_ID: ${{ steps.user.outputs.user_id }} + run: | + curl -fsS -X DELETE "https://api.clerk.com/v1/users/$USER_ID" \ + -H "Authorization: Bearer $CLERK_SECRET_KEY" || true From ed6dc98696853cb04fc60ac7d89d3072fc55ed4f Mon Sep 17 00:00:00 2001 From: ChrisCanin Date: Wed, 6 May 2026 11:17:55 -0700 Subject: [PATCH 2/3] ci(expo): pass exclude_tags via env to avoid shell injection --- .github/workflows/mobile-e2e.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/mobile-e2e.yml b/.github/workflows/mobile-e2e.yml index df41e06a70a..560e8b794ad 100644 --- a/.github/workflows/mobile-e2e.yml +++ b/.github/workflows/mobile-e2e.yml @@ -126,6 +126,7 @@ jobs: env: CLERK_TEST_EMAIL: ${{ steps.user.outputs.email }} CLERK_TEST_PASSWORD: ${{ steps.user.outputs.password }} + EXCLUDE_TAGS: ${{ inputs.exclude_tags }} with: api-level: 34 target: google_apis @@ -137,7 +138,7 @@ jobs: cd ../../integration-mobile # Maestro doesn't auto-recurse into subdirectories; pass each flow explicitly. find flows -type f -name "*.yaml" ! -path "*/common/*" -print0 | \ - xargs -0 maestro test --exclude-tags "${{ inputs.exclude_tags }}" + xargs -0 maestro test --exclude-tags "$EXCLUDE_TAGS" - name: Upload Maestro artifacts on failure if: failure() @@ -241,6 +242,7 @@ jobs: env: CLERK_TEST_EMAIL: ${{ steps.user.outputs.email }} CLERK_TEST_PASSWORD: ${{ steps.user.outputs.password }} + EXCLUDE_TAGS: ${{ inputs.exclude_tags }} run: | cd clerk-expo-quickstart/NativeComponentQuickstart npx expo prebuild --clean @@ -248,7 +250,7 @@ jobs: cd ../../integration-mobile # Maestro doesn't auto-recurse into subdirectories; pass each flow explicitly. find flows -type f -name "*.yaml" ! -path "*/common/*" -print0 | \ - xargs -0 maestro test --exclude-tags "${{ inputs.exclude_tags }},androidOnly" + xargs -0 maestro test --exclude-tags "$EXCLUDE_TAGS,androidOnly" - name: Upload Maestro artifacts on failure if: failure() From 2f3694a7ce7b6ed214c881bbf56fd028d463bd69 Mon Sep 17 00:00:00 2001 From: ChrisCanin Date: Wed, 6 May 2026 15:24:38 -0700 Subject: [PATCH 3/3] ci(mobile-e2e): use Blacksmith runner for Android job --- .github/workflows/mobile-e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/mobile-e2e.yml b/.github/workflows/mobile-e2e.yml index 560e8b794ad..29ae2e74c9c 100644 --- a/.github/workflows/mobile-e2e.yml +++ b/.github/workflows/mobile-e2e.yml @@ -38,7 +38,7 @@ concurrency: jobs: android: name: Android - runs-on: ubuntu-latest + runs-on: 'blacksmith-8vcpu-ubuntu-2204' timeout-minutes: 45 defaults: run: