Skip to content

Verify device specs

Verify device specs #2

name: Verify device specs
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
branches:
- main
paths:
- '.claude/skills/verify-clerk-expo/**'
- '!.claude/skills/verify-clerk-expo/**/*.md'
- '.github/workflows/verify-device-specs.yml'
- 'integration/templates/expo-native/**'
- 'integration/tests/expo-native/boot-ios-simulators.sh'
- 'packages/expo/**'
- 'packages/expo-biometrics/**'
- 'packages/expo-google-signin/**'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: verify-device-specs-${{ github.head_ref || github.ref }}
cancel-in-progress: true
jobs:
plan:
if: ${{ github.head_ref != 'changeset-release/main' }}
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
run: ${{ steps.plan.outputs.run }}
steps:
- name: Decide whether the device specs can run
id: plan
env:
HEAD_REPOSITORY: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
HAS_PLATFORM_API_KEY: ${{ secrets.MOBILE_VERIFICATION_PLATFORM_API_KEY != '' }}
run: |
set -euo pipefail
skip_reason=""
if [ "$HEAD_REPOSITORY" != "$GITHUB_REPOSITORY" ]; then
skip_reason="The device specs did not run. They need a repository secret, and this pull request comes from a fork ($HEAD_REPOSITORY)."
elif [ "$HAS_PLATFORM_API_KEY" != "true" ]; then
skip_reason="The device specs did not run. The MOBILE_VERIFICATION_PLATFORM_API_KEY secret is not set for this repository."
fi
if [ -n "$skip_reason" ]; then
echo "::notice title=Device specs skipped::$skip_reason"
echo "run=false" >> "$GITHUB_OUTPUT"
else
echo "run=true" >> "$GITHUB_OUTPUT"
fi
device-specs:
needs: plan
if: needs.plan.outputs.run == 'true'
runs-on: ${{ matrix.runner }}
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
include:
- platform: ios
runner: ${{ vars.VERIFY_CI_RUNNER_IOS || 'blacksmith-6vcpu-macos-26' }}
- platform: android
runner: ${{ vars.VERIFY_CI_RUNNER_ANDROID || 'blacksmith-8vcpu-ubuntu-2204' }}
env:
SKILL: .claude/skills/verify-clerk-expo
CLI: .claude/skills/verify-clerk-expo/bin/control-clerk-expo
PLATFORM: ${{ matrix.platform }}
VERIFY_LOCAL_BUILD: standalone
VERIFY_THROWAWAY_HOURS: '2'
steps:
- name: Checkout repo
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false
- uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 24.15.0
cache: pnpm
- name: Install monorepo dependencies
run: pnpm install --frozen-lockfile
- name: Set up JDK 21
if: matrix.platform == 'android'
uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 # v4
with:
distribution: temurin
java-version: 21
- name: Set up Gradle cache
if: matrix.platform == 'android'
uses: gradle/actions/setup-gradle@4733eaac7c1b0da527e4206b7671e0061de1ce37 # v6.3.0
with:
cache-read-only: false
add-job-summary: never
- name: Enable KVM
if: matrix.platform == 'android'
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: Install the emulator and its system image
if: matrix.platform == 'android'
run: |
set -euo pipefail
IMAGE="system-images;android-36;google_apis;x86_64"
"$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --install "$IMAGE" emulator platform-tools < /dev/null > "$RUNNER_TEMP/sdkmanager.log" 2>&1 \
|| { echo "::error::sdkmanager could not install $IMAGE"; tail -20 "$RUNNER_TEMP/sdkmanager.log"; exit 1; }
echo "$ANDROID_HOME/platform-tools" >> "$GITHUB_PATH"
- name: Create the template simulator that the skill clones
if: matrix.platform == 'ios'
run: |
set -euo pipefail
created="$(GITHUB_ENV=/dev/null integration/tests/expo-native/boot-ios-simulators.sh boot 1)"
echo "$created"
udid="${created##*: }"
CLERK_TEST_DEVICES="$udid" integration/tests/expo-native/boot-ios-simulators.sh wait
xcrun simctl shutdown "$udid"
xcrun simctl rename "$udid" "Clerk Verify Template iOS"
- name: Install the verify skill
id: install
run: npm ci --prefix "$SKILL" --no-audit --no-fund
- name: Build the fixture and boot a device
timeout-minutes: 15
env:
CLERK_PLATFORM_API_KEY: ${{ secrets.MOBILE_VERIFICATION_PLATFORM_API_KEY }}
run: |
"$CLI" up --platform "$PLATFORM" --backend local
- name: Cache the XCTest runner
if: matrix.platform == 'ios'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ~/.agent-device/apple-runner
key: verify-agent-device-${{ runner.os }}-${{ hashFiles('.claude/skills/verify-clerk-expo/package-lock.json') }}
- name: Compile the XCTest runner before the first spec launches
if: matrix.platform == 'ios'
timeout-minutes: 20
run: |
set -euo pipefail
udid="$(node -p 'JSON.parse(require("fs").readFileSync(process.argv[1], "utf8")).deviceId' "$SKILL/.verify/leases/ios.json")"
AGENT_DEVICE_STATE_DIR="$GITHUB_WORKSPACE/$SKILL/.verify/agent-device" \
"$SKILL/node_modules/.bin/agent-device" prepare ios-runner --platform ios --udid "$udid" --timeout 900000 \
|| echo "::warning::prepare ios-runner failed; the first spec pays for it"
- name: Run the golden specs
timeout-minutes: 20
env:
CLERK_PLATFORM_API_KEY: ${{ secrets.MOBILE_VERIFICATION_PLATFORM_API_KEY }}
run: |
"$CLI" run --all --platform "$PLATFORM" --backend local
- name: Release the device and delete the Clerk application
if: always() && steps.install.outcome == 'success'
env:
CLERK_PLATFORM_API_KEY: ${{ secrets.MOBILE_VERIFICATION_PLATFORM_API_KEY }}
run: |
"$CLI" down
- name: Check that every run is sealed
id: evidence
if: ${{ !cancelled() && steps.install.outcome == 'success' }}
run: |
node -e '
const fs = require("fs");
const path = require("path");
const runs = path.join(process.env.SKILL, ".verify", "runs");
const ids = fs.existsSync(runs) ? fs.readdirSync(runs) : [];
let upload = ids.length > 0;
for (const id of ids) {
let record = null;
try { record = JSON.parse(fs.readFileSync(path.join(runs, id, "run.json"), "utf8")); } catch {}
if (record === null || record.sealed !== true || record.tainted.length > 0) {
upload = false;
console.log("::warning::Run " + id + " was not sealed, or a file in it holds a secret value. No evidence is uploaded.");
}
}
fs.appendFileSync(process.env.GITHUB_OUTPUT, "upload=" + upload + "\n");
'
- name: Summarize the run
if: ${{ !cancelled() && steps.evidence.outputs.upload == 'true' }}
run: |
for summary in "$SKILL"/.verify/runs/*/e2e/summary.md "$SKILL"/.verify/runs/*/e2e-*/summary.md; do
if [ -f "$summary" ]; then
cat "$summary" >> "$GITHUB_STEP_SUMMARY"
fi
done
- name: Upload the evidence
if: ${{ !cancelled() && steps.evidence.outputs.upload == 'true' }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: verify-device-specs-${{ matrix.platform }}
overwrite: true
include-hidden-files: true
path: |
${{ env.SKILL }}/.verify/runs/*/run.json
${{ env.SKILL }}/.verify/runs/*/video.mp4
${{ env.SKILL }}/.verify/runs/*/screenshots
${{ env.SKILL }}/.verify/runs/*/e2e*/summary.md
${{ env.SKILL }}/.verify/runs/*/e2e*/failures
${{ env.SKILL }}/.verify/runs/*/e2e*/artifacts
retention-days: 3
if-no-files-found: warn