mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
d97a628e38
* fix(ci): make the two rg-based static checks actually run
ripgrep is never installed on ubuntu-latest, so both `rg` assertions in
the Lint & Format job failed with "command not found" (exit 127) on
every run. `if rg ...; then ... fi` cannot distinguish that from "no
matches" (exit 1) — both read as false, so each step silently passed
without its assertion ever executing. The DI-seams check had 7 live
violations it never reported.
Rewrite both against `grep`, which every runner ships, with match/
no-match/error exit codes handled explicitly so a broken scan fails
the lane instead of reading as a pass, plus a zero-tracked-files guard
so a renamed directory can't quietly go uncovered.
The DI-seam pattern also gets narrower to drop two classes of false
positive surfaced by actually running it: `typeof fetch` (fetchImpl?/
fetch? seams inject the one global with no module boundary vi.mock can
intercept; auth-session.ts/cloud-profile.ts/daemon-proxy.ts exercise
the seam directly in their unit tests, while CLI-level tests use
vi.stubGlobal('fetch', ...) where the seam isn't reachable — a
deliberate, exercised seam) and `typeof SOME_CONSTANT` in
SCREAMING_SNAKE_CASE (derives a literal union type from a constant,
e.g. interaction-touch-response.ts's dispatchPath field — not an
injectable seam at all).
Fixes #1976
* fix(ci): replace the DI-seam name-based allowlist with an explicit per-site one
Review on PR #2006 (#1976): the previous revision fixed the exit-code
handling but decided which `?: typeof X` matches to ban with a regex
that exempted matches by the *spelling* of the typeof target
(`typeof fetch` always passed, SCREAMING_SNAKE_CASE targets always
passed). That's a name-based semantic allowlist, not ownership: a new,
genuinely test-only `typeof fetch` seam anywhere in the tree would
have silently passed, while an equally legitimate seam under any
other name would still fail.
Add scripts/di-seams: a small, tested TypeScript checker that judges
each match against an explicit, typed, per-site allowlist
(scripts/di-seams/approved.ts) keyed by (file, field name, typeof
target) rather than by name. A triple is exempt only because it was
individually reviewed and named — never because of how it's spelled —
and the gate fails just as hard on a stale approval (one whose triple
no longer matches anything, e.g. after a rename) as on an unapproved
seam, so the list can't silently drift out of sync with the code it
describes.
Moves the DI-seams step in ci.yml to run after Setup toolchain (it's
no longer a toolchain-free text scan); the Swift trailing-comma check
stays where it was.
* fix(ci): register di-seams as a real gate and route it through the tmpdir wrapper
CI caught two things the local (dependency-free) run couldn't:
- oxfmt formatting on the two new files.
- scripts/node-test-tmpdir.test.ts's repo-wide audit: every package.json
script that invokes `node --test` directly must route through
scripts/node-test-tmpdir.ts, or a crash/timeout mid-run leaks its
scratch TMPDIR. check:di-seams now does.
- check:gate-manifest: a package.json script that runs `node --test`
must be covered by a registered CHECK_CATALOG gate, or the audit
reports the test suite as run by no lane. Registered 'di-seams' in
scripts/check-affected/{model,checks}.ts and wired the CI step
through run-gate like every other structural guard in this job,
instead of invoking pnpm directly.
Verified locally with node_modules installed: check:di-seams,
check:gate-manifest, check:gate-manifest:test, check:affected:test,
check:layering, check:fallow (scoped to the changed files), format,
lint, and typecheck all pass.
* fix(ci): close the multiline and duplicate-site gaps in the DI-seam scanner
Review round 2 on PR #2006 (#1976):
- findSeamMatches scanned line by line, so a declaration split across
lines (`field?:` on one line, `typeof X` on the next) was invisible.
Matching now runs against each file's whole source in one pass —
`\s` matches a real newline in JavaScript regexes with no extra flag
needed — with the line number derived from the match's character
offset.
- checkSeams keyed approval by (file, field, target) alone, so once
one occurrence of a triple was approved, any further occurrence of
that same triple anywhere in the file passed too. The key now
includes the line the match starts on, so an approval names one
specific declaration, not a recurring pattern. approved.ts expands
from 5 collapsed entries to the 7 exact sites this closes down to.
Added regression tests planting both gaps directly (a cross-line
declaration, and a second unreviewed fetchImpl?: typeof fetch at a
different line in an already-approved file) and verified both against
the real tree with injected violations, restored cleanly afterward.
Re-ran the full local gate suite (di-seams, gate-manifest, layering,
fallow, format, lint, typecheck) — all green.
* fix(ci): resync approved DI-seam line after merging main
Merging main (#2002) removed an unused import above the approved
dispatchPath?: typeof MAESTRO_COORDINATE_FALLBACK_PATH declaration in
interaction-touch-response.ts, shifting it from line 61 to line 60 —
exactly the location-specific-approval staleness the gate is designed
to catch, just triggered by an unrelated upstream edit rather than a
change in this PR. Updated the approved line to match.
* fix(ci): replace the DI-seam positional table with a code-local approval marker
Review round 3 on PR #2006 (#1976): CI proved the round-2 fix's core
assumption wrong within one push. Keying approval by (file, line,
field, target) made a line number the identity — an unrelated edit
anywhere earlier in a file shifts every approval below it, and that's
exactly what happened: merging main removed an unused import above
the approved dispatchPath declaration, and the gate rejected an
unchanged, already-reviewed line.
Detection is now AST-based (oxc-parser, the same tool
scripts/layering/*.ts already uses) instead of a source-text regex:
any `{ optional: true, typeAnnotation: TSTypeQuery }` node — a
property signature or a bare parameter — is a candidate, which finds
a multiline `field?:\n typeof X` declaration for free instead of
needing a special case for it.
Approval is a `// di-seam-approved: <reason>` comment immediately
above the declaration, matching this repo's own `//
fallow-ignore-next-line complexity` convention: the marker precedes
what it exempts. approved.ts (the external table) is deleted — there
is nothing left to keep in sync, since the approval travels with the
code it approves. A second, unmarked seam under the same field/target
elsewhere still fails; reordering unrelated code around an approved
declaration no longer touches it.
Added the marker to the 7 real approved sites (fetch-global
injection seams in auth-session.ts/cloud-profile.ts/daemon-proxy.ts;
the literal-type-derivation false positive in
interaction-touch-response.ts) and regression tests proving: a
cross-line declaration is still found, a second unmarked occurrence
of an approved field/target pair still fails, and an unrelated
insertion above an approved declaration no longer breaks it. Verified
against the real tree with an injected multi-line unrelated insertion
before an approved site — still green. Re-ran the full local gate
suite (di-seams, gate-manifest, layering, fallow, format, lint,
typecheck, auth-session unit tests) — all green.
* fix(ci): reject a di-seam-approved marker with no reason text
Review round 4 on PR #2006 (#1976): approvalReason() returned '' (not
null) for a bare `// di-seam-approved:` comment with nothing after
it, and checkSeams() only filtered out null, so an empty marker
silently approved a seam with zero justification — exactly the kind
of unreviewed bypass this gate exists to prevent.
approvalReason() now returns null when the joined reason text is
empty after trimming, so a bare or whitespace-only marker is treated
the same as no marker at all. Added tests for both the model-level
behavior and the end-to-end checkSeams() result, plus verified
against the real tree by injecting a bare-marker declaration and
confirming it's flagged, then restored cleanly.
---------
Co-authored-by: Claude <noreply@anthropic.com>
456 lines
19 KiB
YAML
456 lines
19 KiB
YAML
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/**
|