Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
269 changes: 269 additions & 0 deletions .github/workflows/mobile-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
# 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
# ({ "<name>": { "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: 'blacksmith-8vcpu-ubuntu-2204'
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
Comment on lines +91 to +94
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Pass pk via env: instead of ${{ … }} interpolation in the run: script.

pk originates from a parsed secret, so the practical injection risk is low, but the same anti-pattern was already addressed in this PR for exclude_tags (now using the EXCLUDE_TAGS env var). For consistency with that fix and the Semgrep guidance previously applied here, route pk through env: as well — this also avoids quoting issues if the publishable key ever contains a " or backslash. Apply the same change in both Android (line 91–94) and iOS (line 208–211) jobs.

🔒 Proposed fix
       - name: Write quickstart .env
         working-directory: clerk-expo-quickstart/NativeComponentQuickstart
+        env:
+          PUBLISHABLE_KEY: ${{ steps.keys.outputs.pk }}
         run: |
-          echo "EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=${{ steps.keys.outputs.pk }}" > .env
+          printf 'EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=%s\n' "$PUBLISHABLE_KEY" > .env

Also applies to: 208-211

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/mobile-e2e.yml around lines 91 - 94, Update the Android
and iOS job steps that currently interpolate steps.keys.outputs.pk inside the
run block; instead expose the value via env: (e.g., set
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ steps.keys.outputs.pk }}) and change the
run script to reference the env var (use $EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY)
when writing the .env file; locate the steps where steps.keys.outputs.pk is used
and replace the inline interpolation with the env binding in both job blocks.


- 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"
Comment on lines +100 to +111
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Register ::add-mask:: immediately after generating the password.

The mask directive on line 108 (and 225 in the iOS job) runs after curl has already used $password. With -fsS, curl is normally silent, but on a non-2xx response it can dump the request body to stderr — which would surface the unmasked password in the workflow log before the mask is registered. Move the mask call to right after the password is generated, so it's always in effect before the value can appear anywhere.

🔒 Proposed fix (apply to both jobs)
           email="ci-${GITHUB_RUN_ID}-${RANDOM}+clerk_test@clerkcookie.com"
           password="ClerkCI!$(openssl rand -hex 8)Aa1"
+          echo "::add-mask::$password"
           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"

Optionally, also build the JSON body via jq -n --arg email "$email" --arg password "$password" '{email_address:[$email],password:$password}' to avoid shell-quoting fragility if either value ever contains " or \.

Also applies to: 217-228

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/mobile-e2e.yml around lines 100 - 111, The password
masking is registered too late—move the echo "::add-mask::$password" call to
immediately after the password is generated (right after the line that sets the
password variable) in both the Android and iOS job blocks (refer to the password
variable and the existing mask echo and curl POST request) so the mask is active
before curl or any other command can emit the secret; optionally, build the JSON
body with jq (e.g., using jq -n --arg ...) instead of shell string interpolation
to avoid quoting issues when sending the POST.


- 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 }}
EXCLUDE_TAGS: ${{ inputs.exclude_tags }}
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 "$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:

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
Comment on lines +40 to +159
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 }}
EXCLUDE_TAGS: ${{ inputs.exclude_tags }}
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 "$EXCLUDE_TAGS,androidOnly"

Comment on lines +246 to +254
Copy link
Copy Markdown

@semgrep-code-clerk semgrep-code-clerk Bot May 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using variable interpolation ${{...}} with github context data in a run: step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. github context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with env: to store the data and use the environment variable in the run: script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".

🥳 Fixed in commit ed6dc98 🥳

- 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

Check warning

Code scanning / CodeQL

Workflow does not contain permissions Medium

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {contents: read}
Comment on lines +160 to +269
Loading