diff --git a/.agents/skills/verify-clerk-expo b/.agents/skills/verify-clerk-expo new file mode 120000 index 00000000000..bb470267b68 --- /dev/null +++ b/.agents/skills/verify-clerk-expo @@ -0,0 +1 @@ +../../.claude/skills/verify-clerk-expo \ No newline at end of file diff --git a/.changeset/expo-verify-borrowed-device.md b/.changeset/expo-verify-borrowed-device.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/expo-verify-borrowed-device.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.changeset/expo-verify-ci.md b/.changeset/expo-verify-ci.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/expo-verify-ci.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.changeset/expo-verify-evidence.md b/.changeset/expo-verify-evidence.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/expo-verify-evidence.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.changeset/expo-verify-parity-skill.md b/.changeset/expo-verify-parity-skill.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/expo-verify-parity-skill.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.changeset/expo-verify-port-sessions.md b/.changeset/expo-verify-port-sessions.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/expo-verify-port-sessions.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/.claude/skills/README.md b/.claude/skills/README.md index 0031ac9bc9c..5b90de9df83 100644 --- a/.claude/skills/README.md +++ b/.claude/skills/README.md @@ -23,9 +23,13 @@ Edits to a `SKILL.md` take effect immediately, including in already-running sess ## Scope -Skills are Claude Code specific. Cursor does not read this directory; it uses `.cursor/rules/` and -`AGENTS.md`. When a repo rule changes, update `AGENTS.md` first, then mirror the change here and in -`.cursor/rules/` where relevant. +Skills here are Claude Code specific, except `verify-clerk-expo`, the agent-neutral skill that drives +`@clerk/expo` on a simulator or emulator. Its `SKILL.md` and references live here, and the tests and +the CLI it drives are the package `integration/expo-native/`. `.cursor/skills/verify-clerk-expo` is a +symlink to this directory so Cursor reads the same skill. Edit it here, not through the +symlink. For the other skills, Cursor uses `.cursor/rules/` and `AGENTS.md`. When a repo rule +changes, update `AGENTS.md` first, then mirror the change here and in `.cursor/rules/` where +relevant. ## Maintaining a skill @@ -41,7 +45,8 @@ Skills are Claude Code specific. Cursor does not read this directory; it uses `. ## Skills in this repo -| Skill | Use it for | -| ---------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| `clerk-monorepo` | Day-to-day work in the monorepo: setup, build/test loops, the package map, changesets, commits, PRs, breaking-change checks. | -| `mosaic` | Mosaic flow UI: authoring machines, controllers, and views, and migrating a legacy component into the split (with parity verification). | +| Skill | Use it for | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| `clerk-monorepo` | Day-to-day work in the monorepo: setup, build/test loops, the package map, changesets, commits, PRs, breaking-change checks. | +| `mosaic` | Mosaic flow UI: authoring machines, controllers, and views, and migrating a legacy component into the split (with parity verification). | +| `verify-clerk-expo` | Proving a `@clerk/expo` change on an iOS simulator or Android emulator, local or borrowed on a CI runner, with video and screenshots as evidence. | diff --git a/.claude/skills/verify-clerk-expo/SKILL.md b/.claude/skills/verify-clerk-expo/SKILL.md new file mode 100644 index 00000000000..b892f3a9f91 --- /dev/null +++ b/.claude/skills/verify-clerk-expo/SKILL.md @@ -0,0 +1,326 @@ +--- +name: verify-clerk-expo +description: Drive @clerk/expo and @clerk/expo-native-components in the expo-native test app (native AuthView, UserButton, UserProfileView, custom useSignIn and useSignUp flows, token cache, the Google and biometrics native modules) on an iOS simulator or Android emulator against a real Clerk development instance that the session creates and deletes, and capture video, screenshots, and the app log as evidence. The device runs on this Mac, or on a CI runner when the machine cannot run it. Use it to prove any change to packages/expo or the test app works before calling it done, to reproduce a UI bug, or to run the e2e tests. +--- + +# verify-clerk-expo + +`integration/expo-native/bin/control-clerk-expo` is a control CLI over [e2e](https://github.com/tester-army/e2e) 0.18.0 and `@e2e-dev/mobile` 0.10.0. It builds the `expo-native` test app in `integration/templates/expo-native`, leases a simulator or emulator, creates one Clerk application for the worktree, seeds `+clerk_test` users, runs specs, and keeps the evidence. On a Mac the device is local, the test app is a Debug dev client, and Metro serves your working tree to it. On a machine that cannot run the device, the CLI leases one on a GitHub Actions runner, and the runner builds your pushed commit as a Release app with the JS embedded. The verbs, specs, and evidence are the same. + +The tests are an ordinary e2e project. `e2e.config.ts` and `specs/` run under `npx e2e run` when the environment names a device, a build of the test app, and a development instance's keys. The CLI sits on top: it makes those three for a run and keeps the evidence. [The package README](../../../integration/expo-native/README.md) has the commands for a run by hand and what such a run leaves out. + +No change to `@clerk/expo` or `@clerk/expo-native-components` UI or auth behavior is done until a simulator or emulator shows the changed behavior, on each platform the change touches. A `run` on the test app is one way to show it. An app of your own that exercises the change is just as good. + +Run every command from the repo root. In the prose below, `doctor`, `up`, `run`, `screen`, `attach`, and `down` are verbs of that CLI. The tests and the CLI are the Node package `integration/expo-native/`, and paths that begin `specs/`, `src/`, `test/`, or `.verify/` are inside it. `features/` and `references/` are beside this file. Every verb but `attach` takes `--platform ios|android`, and iOS is the default. Every error prints a `fix` line. Every verb takes `--json`, and [CLI output](references/output.md) has its shape, the exit codes, and the error codes. + +The test app links `@clerk/expo`, `@clerk/expo-native-components`, `@clerk/expo-biometrics`, and `@clerk/expo-google-signin` from the workspace. It does not install `@clerk/expo-passkeys`, so the skill cannot verify passkeys. + +## Launch + +Set up each machine once. + +1. Install Node 24, at 24.8.0 or newer. For a local iOS device, install Xcode with an iOS simulator runtime. For a local Android device, install Android Studio with the SDK, the emulator, and Java 21. The CLI looks for the SDK in `ANDROID_HOME`, `ANDROID_SDK_ROOT`, and `~/Library/Android/sdk`. +2. Create the iOS template simulator, which the CLI clones to make each simulator it drives, with `xcrun simctl create "Clerk Verify Template iOS" "iPhone Air"`. That makes a new simulator that holds only the system's apps. Do not clone a simulator you already use: a clone holds every app of the original, and those apps show on the home screen at the start of a video. Boot the template once and shut it down: a clone of a template that never booted takes about twenty seconds longer to boot. `doctor` says when a system HTTPS proxy needs its CA trusted there. Android needs no template: the first `up --platform android` writes the `Clerk_Verify_Pixel` AVD. +3. Give the machine the team's Clerk Platform API key. Set `CLERK_PLATFORM_API_KEY` to the key. Or set `CLERK_PLATFORM_API_KEY_FILE` to a file that holds it and that only you can read (mode 0600). Or set `VERIFY_PLATFORM_KEY_REFERENCE` to the key's 1Password secret reference, which the 1Password CLI reads once a person approves the request in the 1Password app, within 60 seconds. Never put the key or the reference in a file inside a repository. +4. Install ffmpeg (`brew install ffmpeg`), which `attach --video` uses to cut the video to the part that `host.evidence` marks. Without it, `attach` posts the whole video and says that ffmpeg is not installed. + +Then, in each worktree: + +```console +$ pnpm install # once per worktree +$ npm ci --prefix integration/expo-native # once per worktree +$ integration/expo-native/bin/control-clerk-expo doctor --platform ios +$ integration/expo-native/bin/control-clerk-expo up --platform ios +backend local this Mac runs the simulator itself +instance creating verify-throwaway-until-- in org_3KHungJxbvIscuSvy8oos5MHAli +build local building... +build turbo build @clerk/expo, @clerk/expo-native-components, @clerk/expo-biometrics, @clerk/expo-google-signin +instance up in 0.7s on standard, 62 settings match src/core/instances/base.json +build expo prebuild --clean --platform ios +build xcodebuild Debug (dev client) +build local built in 110s +device verify-ios-1 cloning Clerk Verify Template iOS +install on verify-ios-1 +watch packages/expo tsdown --watch (pid ) +metro :8082 expo start (pid ) +metro :8082 bundling ios once so the first launch does not wait on Metro +device verify-ios-1 local leased by this worktree installed +``` + +The package is outside the pnpm workspace, so `npm ci` installs its pinned `e2e` and `agent-device` from the package's own lockfile. The sample is the first `up` in a worktree, without its `instances` and `clerk` lines. The device is ready when `up` prints the `device` line that ends in `installed `, which is its last line. `run` does the same steps itself, so `up` only starts the slow part early. Teardown is `down` (see [Cleanup](#cleanup)). + +`up` does four things: + +- It builds the dev client when no build matches the native inputs: `turbo build` for the four packages, `expo prebuild --clean`, then `xcodebuild` or `gradlew assembleDebug`. The build overwrites the test app's generated `package.json`, `ios/`, and `android/`. A change to anything else reuses the build and prints `build local reused`. +- It creates this worktree's Clerk application through Clerk's Platform API, in the team's verification workspace, and puts its development instance on the standard settings in `src/core/instances/base.json`. The application holds only the users that this worktree's runs create, and `down` deletes it. [Test instances](references/instances.md) has the credential lookup, the application's lifetime, and its limits. +- It leases a lane, which is a simulator or emulator that the CLI creates, and installs the build. +- It starts `tsdown --watch` in `packages/expo` (the watch build) and `expo start` on the lane's Metro port. + +`up` is idempotent. It keeps a lease that this worktree already holds. A failed `up` stops the Metro and the watch build that it started. + +On a local device a JS change in `packages/expo/src` or in the test app reaches the app with no build. A JS change in `packages/expo-native-components/src`, where AuthView, UserButton, and UserProfileView are, makes the next `up` or `run` rebuild that package first. Before the specs start, `run` waits until the watch build has caught up and Metro serves the current code, and it fails with `NOT_READY` and the path of the Metro log when Metro never does. [How a change reaches the app](references/freshness.md) has the native inputs, the checks, the ports, and the logs. It also says what to do after a change to another workspace package, such as `@clerk/clerk-js` or `@clerk/shared`. While Metro runs, never run `pnpm --filter @clerk/expo build` or a build of a package that `@clerk/expo` depends on, because the build deletes the `dist` that Metro serves. + +A worktree can hold one lane of each platform. The two lanes share the watch build and the application, and each has its own Metro. Run them one after the other. The application serves one run at a time, because a run can change its settings. A `run` that starts while a run on the other platform is driving leases its device and then fails with `DEVICE_BUSY`, and its fix is to let that run finish and rerun. + +A Mac has four iOS lanes and two Android lanes, shared by every worktree on it. When all are taken, `up` and `run` fail with `POOL_FULL`, and `--wait ` on either verb waits for a lane. The CLI drives only the simulators and emulators that it creates. [Local devices](references/devices.md) says how to find a lane's UDID or serial. + +### Borrow a device on a CI runner + +A machine that is not a Mac cannot run the simulator, and a machine with no hardware virtualization cannot run the emulator. There the CLI leases a device on a GitHub Actions runner and drives it through a tunnel. That machine needs Node 24.8.0 or newer on 24, the Platform API key, and access to GitHub, and `doctor` checks each. In the worktree it needs `npm ci --prefix integration/expo-native` and no `pnpm install`, because the runner builds the app. The session builds a pushed commit. When the app sources differ from it, `up` and `run` send the difference with it, and the session builds the commit with your changes on top. + +```console +$ git push +$ integration/expo-native/bin/control-clerk-expo up --platform ios --backend remote +backend remote forced by --backend remote +build github-actions commit the session builds it +device remote ios starting session on macos-26 (idle stop 15 min, cap 60 min) +device remote ios tunnel up, iPhone 17 Pro on macos-26 +build github-actions built in 1263s on macos-26 +device iPhone 17 Pro on macos-26 remote leased by this worktree installed +$ integration/expo-native/bin/control-clerk-expo down # ends the runner job +``` + +The `backend` line says which backend the CLI chose and why. This transcript is from a Mac, where `--backend remote` forced the remote backend, and it leaves out the `instance`, `clerk`, `install`, and `wait` lines and the line with the run's URL. On a machine that cannot run the device, `up` needs no flag, and the `backend` line says why the local backend is out. `--backend local` or `--backend remote` on `doctor`, `up`, or `run` forces a backend, and a worktree that holds a lease keeps its backend until `down`. `--runner