name: Mobile Checks on: pull_request: types: - opened - synchronize - reopened paths: - 'mobile/**' # Why all of src/shared: mobile imports hundreds of its modules, directly and through the RPC # recordings and the host params check, so any shared edit can move a golden or break mobile's # typecheck. Why the root lockfile: those modules resolve their packages from the root install. - 'src/shared/**' - 'pnpm-lock.yaml' # Mobile launch contracts exercise the real host dispatcher and durable receipt store. - 'src/main/agent-launch/**' - 'src/main/runtime/rpc/**' - 'src/main/runtime/runtime-rpc/**' - 'src/main/runtime/runtime-rpc.ts' - 'src/main/runtime/device-registry.ts' - 'src/main/runtime/orca-runtime.ts' - 'src/main/runtime/agent-session-*.ts' - 'src/main/native-chat/agent-session-wire/**' # Why: this job holds the only checks that load the Fastfile, so edits to # it or to the release workflow it guards must re-run them. - '.github/workflows/mobile.yml' - '.github/actions/install-node-dependencies/**' - '.github/workflows/mobile-ios-release.yml' - 'config/scripts/mobile-release-check-scope*' - 'config/scripts/mobile-test-change-scope*' - 'config/scripts/pr-code-change-scope.mjs' # Why main too: two pull requests can each pass against their own base and disagree once both # land, and `verify` never runs on main. The same source paths as above, less the CI inputs only # `verify` reads. push: branches: - main paths: - 'mobile/**' - 'src/shared/**' - 'pnpm-lock.yaml' - 'src/main/agent-launch/**' - 'src/main/runtime/rpc/**' - 'src/main/runtime/runtime-rpc/**' - 'src/main/runtime/runtime-rpc.ts' - 'src/main/runtime/device-registry.ts' - 'src/main/runtime/orca-runtime.ts' - 'src/main/runtime/agent-session-*.ts' - 'src/main/native-chat/agent-session-wire/**' - '.github/workflows/mobile.yml' concurrency: # Per commit on main, not per branch. GitHub cancels any PENDING run in a group when a new one # queues, whatever `cancel-in-progress` says, so one shared main group drops the middle merge of # three -- and a merge that breaks main there is exactly what `main-tests` checks for. group: mobile-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true jobs: verify: if: github.event_name == 'pull_request' # Why ARM: 209s of this job is Vitest and nothing here needs x86: no Android SDK, emulator, gradle, # Hermes or Watchman, no docker, and no artifacts. The Gemfile.lock lists the generic `ruby` # platform, so frozen bundler installs without an aarch64-linux entry. runs-on: ubuntu-24.04-arm env: # Why: an unfrozen bundler silently re-resolves when Gemfile.lock drifts # from the Gemfile, which is how the release jobs could land on different # fastlane versions in the first place. Fail here instead. BUNDLE_FROZEN: 'true' defaults: run: working-directory: mobile steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 2 - uses: ./.github/actions/install-node-dependencies with: cache-dependency-path: | pnpm-lock.yaml mobile/pnpm-lock.yaml - name: Detect Ruby release inputs id: ruby-scope shell: bash working-directory: . run: | # Keep deletions when release files move into an application directory. if ! git diff --name-only --no-renames -z HEAD^1 HEAD > "$RUNNER_TEMP/mobile-release-changes"; then echo 'should_run=true' >> "$GITHUB_OUTPUT" elif ! node config/scripts/mobile-release-check-scope.mjs "$RUNNER_TEMP/mobile-release-changes"; then echo 'should_run=true' >> "$GITHUB_OUTPUT" fi # bundler-cache installs mobile/Gemfile.lock, so this job is also what # proves the pinned fastlane the release workflow depends on still # resolves — before a release run finds out. - name: Setup Ruby and fastlane if: steps.ruby-scope.outputs.should_run != 'false' uses: ruby/setup-ruby@v1 with: ruby-version: '3.3' bundler-cache: true working-directory: mobile - name: Install dependencies run: pnpm install --frozen-lockfile # Both compilers are read-only; finish them before starting the test workers. - name: Typecheck id: production-types background: true run: pnpm typecheck # Why a ratchet and not the raw typecheck: mobile/tsconfig.json excludes test files, so until # tsconfig.test.json existed nothing checked them, and at introduction 127 of the 632 had # drifted. This fails when a test file that checks today stops checking, when a test leaves # the program, and on @ts-nocheck; the baseline may only shrink. - name: Typecheck tests (ratchet) run: pnpm run check:tests-typecheck - wait: production-types # This includes the bridged replay of the whole recording corpus, which used to be a second # step of its own behind RPC_FOUNDATION_BRIDGE=1. A gate nobody can forget to set is the point: # it fails when a divergence class grows, when a divergence lands in no class at all, or when # one of the 103 goldens inside the C1 page closure changes the verdict it is pinned to. It is # ~3 min of test time on its own, and Vitest runs it on a worker beside the rest of the suite, # so folding it in costs a fraction of that in wall time and one step less to skip. - name: Detect mobile test inputs id: test-scope shell: bash working-directory: . run: | if ! git diff --name-only --no-renames -z HEAD^1 HEAD > "$RUNNER_TEMP/mobile-test-changes"; then echo 'should_run=true' >> "$GITHUB_OUTPUT" elif ! node config/scripts/mobile-test-change-scope.mjs "$RUNNER_TEMP/mobile-test-changes"; then echo 'should_run=true' >> "$GITHUB_OUTPUT" fi - name: Test if: steps.test-scope.outputs.should_run != 'false' env: ORCA_BACKGROUND_LAUNCH: '1' run: pnpm test # Reports, never gates: the goldens are content-addressed JSON, so their raw diff is hashes. # This decodes which recorded behaviour the pull request moves into the job summary, and runs # after a red Test too, when a reviewer most wants it. - name: Summarize RPC recording changes if: ${{ !cancelled() }} continue-on-error: true run: pnpm run rpc:diff HEAD^1 --summary "$GITHUB_STEP_SUMMARY" - name: Test iOS release version resolution if: steps.ruby-scope.outputs.should_run != 'false' run: ruby fastlane/ios_release_version_test.rb - name: Test TestFlight lane arguments if: steps.ruby-scope.outputs.should_run != 'false' run: ruby fastlane/fastfile_testflight_arguments_test.rb # Why: nothing else in CI loads the Fastfile, so a syntax error, a broken # require, or an undefined constant only surfaces mid-release — the # ios-distribute job failed every run for six days that way. `lanes` just # loads and lists, so it needs no App Store Connect credentials and makes # no network calls to Apple. - name: Smoke-check the Fastfile if: steps.ruby-scope.outputs.should_run != 'false' env: FASTLANE_SKIP_UPDATE_CHECK: '1' FASTLANE_OPT_OUT_USAGE: '1' run: bundle exec fastlane lanes - name: Lint run: pnpm lint - name: Check formatting run: pnpm format:check main-tests: name: Mobile tests on main if: github.event_name == 'push' # Why ARM: the same pure-Node Vitest run `verify` makes on a pull request. runs-on: ubuntu-24.04-arm defaults: run: working-directory: mobile steps: - name: Checkout uses: actions/checkout@v6 with: persist-credentials: false - uses: ./.github/actions/install-node-dependencies with: cache-dependency-path: | pnpm-lock.yaml mobile/pnpm-lock.yaml - name: Install dependencies run: pnpm install --frozen-lockfile # The whole suite, not only the recordings: the mutant and page-bridge suites read the goldens # too. A red run here names each golden and the `rpc:record` command; the author of the merge # that turned it red re-records in a follow-up. - name: Test env: ORCA_BACKGROUND_LAUNCH: '1' run: pnpm test