Repository navigation
Verify device specs #2
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |