refactor: close the daemon platform boundary #7075
Workflow file for this run
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: CI | |
| on: | |
| pull_request: | |
| paths-ignore: | |
| - 'docs/**' | |
| - 'website/**' | |
| - 'README.md' | |
| - 'AGENTS.md' | |
| - 'CHANGELOG.md' | |
| - 'CONTEXT.md' | |
| - 'CONTRIBUTING.md' | |
| - 'LICENSE' | |
| - 'SECURITY.md' | |
| - '.github/actions/build-docs/action.yml' | |
| - '.github/workflows/deploy.yml' | |
| - '.github/workflows/pr-preview.yml' | |
| - '.github/workflows/pr-preview-cleanup.yml' | |
| push: | |
| branches: | |
| - main | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| # The Swift trailing-comma assertion is text-only and runs before the toolchain setup, so a | |
| # grep failure does not wait on an install. The DI-seams check below needs a real TypeScript | |
| # runtime (#1976 / PR #2006), so it runs after Setup toolchain instead. | |
| lint: | |
| name: Lint & Format | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| # #1976: ripgrep is never installed on ubuntu-latest, so `rg` failed with "command not | |
| # found" (exit 127) on every run, and `if rg ...; then ... fi` cannot distinguish that | |
| # from "no matches" (exit 1) — both read as false, so the step passed without the | |
| # assertion ever executing. Rewritten against `grep`, which every runner ships, with the | |
| # match/no-match/error exit codes handled explicitly so a broken scan fails loudly instead | |
| # of silently passing. | |
| - name: Disallow trailing commas before closing parenthesis in Swift | |
| run: | | |
| mapfile -d '' -t swift_files < <(git ls-files -z -- 'apple/runner' | grep -z '\.swift$') | |
| if [ "${#swift_files[@]}" -eq 0 ]; then | |
| echo "No apple/runner/*.swift files are tracked; the trailing-comma check has nothing to scan." >&2 | |
| exit 1 | |
| fi | |
| set +e | |
| grep -PzoH ',\s*\n\s*\)' "${swift_files[@]}" | |
| status=$? | |
| set -e | |
| if [ "$status" -eq 0 ]; then | |
| echo "Found trailing commas before ')' in Swift files. This syntax requires Swift 6.1+ and breaks older Xcode toolchains." | |
| exit 1 | |
| elif [ "$status" -ne 1 ]; then | |
| echo "grep exited $status while scanning apple/runner for trailing commas; treating an unreadable scan as a failure instead of a silent pass." | |
| exit 1 | |
| fi | |
| - name: Setup toolchain | |
| uses: ./.github/actions/setup-node-pnpm | |
| # Same false-green shape as the Swift check above (#1976). An earlier revision of this | |
| # gate (PR #2006, first review pass) fixed the exit-code handling but kept the ban/allow | |
| # decision as a regex that exempted matches by the *spelling* of the typeof target | |
| # (`typeof fetch` always passed, SCREAMING_SNAKE_CASE targets always passed) — a name-based | |
| # semantic allowlist that would silently pass a new, genuinely test-only `typeof fetch` seam | |
| # anywhere in the tree while banning an equally legitimate seam under any other name. | |
| # scripts/di-seams instead checks each match against an explicit, typed, per-site allowlist | |
| # (scripts/di-seams/approved.ts) keyed by (file, field, typeof-target): a triple is exempt | |
| # only because it was individually reviewed and named, never because of how it is spelled. | |
| # The gate fails just as hard on a stale approval (one whose triple no longer matches | |
| # anything) as on an unapproved seam, so the allowlist can't drift out of sync with the code | |
| # it describes. See scripts/di-seams/model.ts and its tests. | |
| - name: Fail if test-only DI seams reappear in production code | |
| uses: ./.github/actions/run-gate | |
| with: { gate: di-seams } | |
| - name: Run oxlint | |
| uses: ./.github/actions/run-gate | |
| with: { gate: lint } | |
| - name: Check formatting | |
| uses: ./.github/actions/run-gate | |
| with: { gate: format } | |
| # Structural guards share one checkout and install. Every gate stays an | |
| # independently named step with its own failure message; the gate manifest | |
| # derives lane ownership from these declarations, so merging jobs changes | |
| # nothing it asserts. | |
| repo-guards: | |
| name: Repo Guards | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| # The layering gate parses production sources with `oxc-parser`, so | |
| # dependencies are required; keep install-deps enabled. | |
| - name: Setup toolchain | |
| uses: ./.github/actions/setup-node-pnpm | |
| - name: Check import-direction DAG | |
| # Structured import-direction lint over the resolved graph. See | |
| # scripts/layering/check.ts and CONTEXT.md (Architecture: folder DAG + | |
| # layering lint). | |
| uses: ./.github/actions/run-gate | |
| with: { gate: layering } | |
| - name: Check the depgraph report agrees with the gate | |
| # scripts/depgraph reads the same model as the gate, so its inversion count must | |
| # reproduce TYPE_INVERSION_BASELINE. Free two-sources check: if the tree changes | |
| # and only one side is updated, this fails and names the difference. Runs beside | |
| # the layering gate so the two can never be green independently. | |
| uses: ./.github/actions/run-gate | |
| with: { gate: depgraph } | |
| # Tests for the TMPDIR redirection itself, hidden the same way as the check above. | |
| # | |
| # Deliberately NOT in Coverage next to `check:tmpdir-leaks`, where the subject matter | |
| # would put it: vitest-tmpdir-global-setup.test.ts proves the lifecycle by spawning a | |
| # real nested `vitest run`, and the Coverage lane already loses runs to | |
| # `[vitest-pool]: Worker forks emitted error` when a fork is slow to terminate. | |
| # Starting a nested Vitest seconds before the full instrumented suite is a contention | |
| # risk with nothing to gain. | |
| - name: Check the tmpdir redirection model | |
| uses: ./.github/actions/run-gate | |
| with: { gate: tmpdir-leaks-model } | |
| # The selector is fail-open and advisory (GitHub CI stays authoritative), | |
| # so the gate only guards the derivation model. | |
| - name: Check affected-selector model | |
| uses: ./.github/actions/run-gate | |
| with: { gate: affected-selector } | |
| # The gate-of-gates (#1429). It shares this job because it validates the | |
| # same artifact the selector is built on — CHECK_CATALOG's `ciJobs` — and | |
| # because a gate that proves the other gates are wired must not be the one | |
| # gate sitting in its own job, green on its own. Deterministic and | |
| # network-free: every input is a file in the checkout. | |
| - name: Check the gate manifest model | |
| uses: ./.github/actions/run-gate | |
| with: { gate: gate-manifest-model } | |
| - name: Check every gate is owned, wired, and reachable | |
| uses: ./.github/actions/run-gate | |
| with: { gate: gate-manifest } | |
| # Same family as the manifest above — a CI selection that has stopped selecting what | |
| # it claims. ios.yml runs a hand-written subset of the runner XCTest methods through an | |
| # `-only-testing:` list, and xcodebuild treats an identifier that matches nothing as | |
| # an empty selection rather than an error, so a rename drops a test with no signal — | |
| # in both directions, since a typo in xctest-nightly.yml's `-skip-testing:` entry | |
| # re-arms a 24-hour hang. Parse-only, no Xcode (#1781 A7). | |
| - name: Check the PR XCTest selection still names real tests | |
| uses: ./.github/actions/run-gate | |
| with: { gate: xctest-selection } | |
| # Layers 1-2 of the conformance oracle: replay the JVM-generated fixtures | |
| # against the live engine. Deterministic and Java-free — the generated | |
| # fixtures are checked in and only regenerated on an upstream-pin bump. The | |
| # device-backed layer 3 runs on the scheduled conformance-differential | |
| # workflow. See scripts/maestro-conformance/README.md. Unlike the guards | |
| # above, this parses corpus flows with the live engine and imports the | |
| # `yaml` package — covered by the shared install above. | |
| - name: Verify Maestro conformance fixtures | |
| uses: ./.github/actions/run-gate | |
| with: { gate: maestro-conformance } | |
| # server.json/smithery.yaml drift otherwise surfaces at publish time (where | |
| # publish-mcp-registry.yml duplicates the same command). Parse-only. | |
| - name: Check MCP registry metadata is in sync | |
| uses: ./.github/actions/run-gate | |
| with: { gate: mcp-metadata } | |
| # Slowest guard (~3 min), so it runs last and structural failures surface | |
| # before it. Runs on plain Node via the shared install above. | |
| - name: Check numeric ranges | |
| uses: ./.github/actions/run-gate | |
| with: { gate: freerange } | |
| # History-backed compatibility gates share one deep checkout: fallow, | |
| # replay-compat, and daemon-wire-compat all need either full history or tags, | |
| # which no shallow unit lane can read. | |
| compat-provenance: | |
| name: Compatibility & Provenance | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| - name: Setup toolchain | |
| uses: ./.github/actions/setup-node-pnpm | |
| - name: Run Fallow audit | |
| env: | |
| FALLOW_BASE: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} | |
| uses: ./.github/actions/run-gate | |
| with: | |
| gate: fallow | |
| args: | | |
| --base | |
| ${{ env.FALLOW_BASE }} | |
| - name: Check for production-unused exports | |
| uses: ./.github/actions/run-gate | |
| with: { gate: production-exports } | |
| # The frozen replay-compat corpus (#1417) claims each entry was published by | |
| # a released tag. Only a full-history checkout can re-derive that claim. | |
| - name: Verify corpus entries against their released blobs | |
| uses: ./.github/actions/run-gate | |
| with: { gate: replay-compat } | |
| - name: Verify the wire-compat rules | |
| uses: ./.github/actions/run-gate | |
| with: { gate: wire-compat-model } | |
| # The daemon RPC wire ledger (#1432) is compared against the ledger as it | |
| # stood at the last RELEASED tag, which only a tagged checkout can read. | |
| # The shallow Repo Guards lane holds the ledger to its source; this step | |
| # holds it to the last release. | |
| - name: Compare the daemon RPC wire surface against the last released tag | |
| uses: ./.github/actions/run-gate | |
| with: { gate: daemon-wire-compat } | |
| # Typecheck and package verification share one checkout and install; both | |
| # need the default toolchain only, and the package step re-pins Node itself. | |
| typecheck-package: | |
| name: Typecheck & Package | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup toolchain | |
| uses: ./.github/actions/setup-node-pnpm | |
| - name: Run typecheck | |
| uses: ./.github/actions/run-gate | |
| with: { gate: typecheck } | |
| - name: Build CLI | |
| uses: ./.github/actions/run-gate | |
| with: { gate: build } | |
| - name: Verify emitted chunk ownership | |
| uses: ./.github/actions/run-gate | |
| with: { gate: bundle-owner-files } | |
| # The build runs on the default toolchain Node and the package is verified on the minimum | |
| # supported Node, so this job covers what a user on `engines.node` floor actually installs. | |
| - name: Setup Node.js 22.12 | |
| uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 | |
| with: | |
| node-version: '22.12' | |
| # Packs, lints the tarball with publint/attw, installs it outside the workspace, and imports | |
| # every published entry point before running the CLI. See scripts/check-package.ts. | |
| # | |
| # Runs the script directly rather than through `pnpm check:package`: the repo's pinned pnpm | |
| # requires Node >= 22.13 and refuses to start on the 22.12 floor this job exists to cover. The | |
| # gate itself only needs `node` and `npm`, so it is the package.json script minus the launcher. | |
| - name: Verify the published package on Node.js 22.12 | |
| run: node --experimental-strip-types scripts/check-package.ts | |
| # Runs the full unit + provider-integration suites under coverage with | |
| # thresholds, so a separate unit-tests job would rerun the same tests. The | |
| # suite is sharded across runners; each shard writes a blob report and the | |
| # Coverage Report job merges them, evaluates thresholds once over the full | |
| # suite, and produces every coverage artifact (vitest.config.ts carries the | |
| # shard/merge switches). | |
| coverage: | |
| name: Coverage | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| shard: [1, 2] | |
| env: | |
| AGENT_DEVICE_COVERAGE_SHARD: ${{ matrix.shard }}/2 | |
| OUTPUT_ECONOMY_BASE: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup toolchain | |
| uses: ./.github/actions/setup-node-pnpm | |
| - name: Test changed-line coverage gate | |
| if: matrix.shard == 1 | |
| uses: ./.github/actions/run-gate | |
| with: { gate: coverage-model } | |
| - name: Run coverage shard | |
| uses: ./.github/actions/run-gate | |
| with: { gate: unit-ci } | |
| # The TMPDIR redirection both test lanes depend on (#1593/#1595). The check is a real | |
| # package script that no workflow ran: it is reachable only through `check:unit`, an | |
| # aggregate CI never invokes, so a leak regression could not fail a PR. Runs per shard, | |
| # because a leak lands on whichever runner executed the leaking file. | |
| - name: Check for leaked temp directories | |
| uses: ./.github/actions/run-gate | |
| with: { gate: tmpdir-leaks } | |
| - name: Upload coverage blob | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: coverage-blob-${{ matrix.shard }} | |
| path: .vitest-reports/ | |
| # The directory is dot-prefixed, which v4 excludes by default. | |
| include-hidden-files: true | |
| if-no-files-found: error | |
| coverage-report: | |
| name: Coverage Report | |
| needs: coverage | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| env: | |
| AGENT_DEVICE_COVERAGE_MERGE: '1' | |
| OUTPUT_ECONOMY_BASE: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup toolchain | |
| uses: ./.github/actions/setup-node-pnpm | |
| - name: Download coverage blobs | |
| uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 | |
| with: | |
| pattern: coverage-blob-* | |
| path: .tmp/coverage-blobs | |
| # download-artifact nests each artifact in its own subdirectory; the blob | |
| # merge reads one flat directory. | |
| - name: Collect coverage blobs | |
| run: | | |
| set -euo pipefail | |
| mkdir -p .vitest-reports | |
| find .tmp/coverage-blobs -name '*.json' -exec mv {} .vitest-reports/ \; | |
| ls .vitest-reports | |
| # Reuses the blobs the shards wrote (never reruns tests) and fails when | |
| # merged changed-line coverage < the threshold in | |
| # scripts/coverage-changed/model.ts. The `coverage-waiver` PR label maps to | |
| # the waiver env, which skips the failure but still prints the numbers. | |
| # Gated on the merge step's own outcome (#1781 A5): when it fails, | |
| # lcov.info is never written, so this step would just re-report that | |
| # failure as its own red ("no lcov report") instead of a coverage verdict. | |
| - name: Merge coverage shards | |
| id: run-coverage | |
| uses: ./.github/actions/run-gate | |
| with: { gate: unit-ci } | |
| - name: Enforce changed-line coverage gate | |
| if: steps.run-coverage.outcome == 'success' && github.event_name == 'pull_request' | |
| env: | |
| AGENT_DEVICE_COVERAGE_WAIVER: ${{ contains(github.event.pull_request.labels.*.name, 'coverage-waiver') }} | |
| uses: ./.github/actions/run-gate | |
| with: | |
| gate: coverage | |
| args: | | |
| --base | |
| ${{ github.event.pull_request.base.sha }} | |
| integration: | |
| name: Integration Tests | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| - name: Setup toolchain | |
| uses: ./.github/actions/setup-node-pnpm | |
| - name: Run integration tests | |
| run: pnpm clean:daemon | |
| - name: Execute integration tests | |
| uses: ./.github/actions/run-gate | |
| with: { gate: integration-node } | |
| - name: Run seeded concurrency torture lane (fast PR sweep) | |
| # #1416's nightly torture lane lives under test/integration/nightly/, out | |
| # of the test:integration:node glob, so this is a *deliberate* fast PR | |
| # sweep (TORTURE_RUNS default 128 seeds, ~sub-second) — not an accidental | |
| # glob inclusion. The Concurrency Torture Nightly workflow sweeps a much | |
| # larger seed range on schedule. | |
| uses: ./.github/actions/run-gate | |
| with: { gate: concurrency-torture } | |
| - name: Run provider-backed integration tests | |
| uses: ./.github/actions/run-gate | |
| with: { gate: provider-integration } | |
| - name: Check Provider-backed integration architecture progress | |
| uses: ./.github/actions/run-gate | |
| with: { gate: integration-progress } | |
| # A build-cache lookup outage must degrade setup-fixture-app to an inline | |
| # build, not fail the caller. This drives that step's real shell against a | |
| # failing `gh`. | |
| - name: Setup-fixture-app cache-failure fallback | |
| uses: ./.github/actions/run-gate | |
| with: { gate: fixture-fallback } | |
| # The trusted-artifact contract the device lanes rely on to decide a cached fixture | |
| # app is the one this commit expects. A real test file that no workflow ran. | |
| - name: Check the trusted fixture-artifact contract | |
| uses: ./.github/actions/run-gate | |
| with: { gate: fixture-cache } | |
| # Shares this job's ubuntu toolchain. AGENT_DEVICE_WEB_E2E is step-scoped | |
| # so it cannot leak into the node/provider suites above. | |
| - name: Run live web smoke | |
| env: | |
| AGENT_DEVICE_WEB_E2E: '1' | |
| run: pnpm clean:daemon | |
| - name: Execute live web smoke | |
| env: | |
| AGENT_DEVICE_WEB_E2E: '1' | |
| uses: ./.github/actions/run-gate | |
| with: { gate: web-smoke } | |
| - name: Upload web smoke artifacts | |
| if: always() | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: web-smoke-artifacts | |
| if-no-files-found: ignore | |
| path: | | |
| test/artifacts/web/** |