Skip to content

refactor: move platform provider composition out of daemon #7073

refactor: move platform provider composition out of daemon

refactor: move platform provider composition out of daemon #7073

Workflow file for this run

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/**