-
Notifications
You must be signed in to change notification settings - Fork 0
484 lines (466 loc) · 22.8 KB
/
Copy pathci.yml
File metadata and controls
484 lines (466 loc) · 22.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
name: CI
on:
pull_request:
push:
branches: [main]
# Merge queue: every job that is a required check on main must also run on
# merge_group, or a queued entry waits until the queue times it out. The
# event never fires while this repository is user-owned (GitHub offers the
# queue only to organization-owned repositories: creating a `merge_queue`
# ruleset here returns 422 "Invalid rule 'merge_queue'"), so this trigger
# is dormant until the repository moves to an organization.
merge_group:
branches: [main]
# A manual dispatch runs the full main-push matrix on any ref. The nightly
# lanes (release-boundary packed matrix, evidence capture, MCP conformance)
# live in nightly.yml so a red nightly never hides behind a PR job and a
# PR job never waits on a nightly.
workflow_dispatch:
permissions:
contents: read
# Only the latest commit of a ref matters: a PR push cancels the superseded
# run of the same PR, and a main push cancels the superseded main run, so a
# burst of merges leaves one run for the tip instead of a queue. Merge-queue
# entries and dispatches key on their own ref. Same pattern as
# package-preview.yml; docs.yml and release.yml never cancel a main run
# because a deploy or publish must not die mid-flight.
concurrency:
group: ci-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
# Workbench browser suites launch Playwright's bundled Chromium — pinned by
# the Playwright version in pnpm-lock.yaml and installed by the
# setup-workspace action — instead of the Google Chrome the ubuntu-latest
# image happens to ship that week, so a browser change is always a commit
# (#576). The switch is read by packages/workbench/tests/support/
# workbench-e2e.ts (`browserLaunchOptions`); unset, tests keep launching
# branded Chrome, which is what developers have locally. The shipped
# `agentBundleBrowserRstest` helper still targets branded Chrome, so the
# examples-check job is the one place that keeps the image's Chrome.
AGENT_BUNDLE_PLAYWRIGHT_CHANNEL: chromium
jobs:
# PR-only docs allowlist: docs/**, agent-patterns/**, website/** (the
# Rspress site, validated by docs.yml), .changeset/*.md, and top-level *.md.
# Nested markdown elsewhere is code because examples and packages contain
# compiled SKILL.md artifacts, and package markdown affects npm pack audits.
# Classification fails open so uncertain PRs run every heavy job; pushes to
# main and merge-queue entries never skip any job based on changed paths
# (this job does not run for them, and the heavy jobs treat the missing
# output as "run").
# Path rules and fail-open listing checks live in
# scripts/classify-docs-only.mjs (unit-tested). This job sparse-checkouts
# only that script so the vendored Effect subtree never lands on the
# classify critical path.
changes:
if: github.event_name == 'pull_request'
name: Detect changed paths
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
pull-requests: read
outputs:
docs_only: ${{ steps.classify.outputs.docs_only }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 1
filter: blob:none
persist-credentials: false
sparse-checkout: |
scripts/classify-docs-only.mjs
sparse-checkout-cone-mode: false
- name: Classify changed files
id: classify
env:
GH_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
files_file="$(mktemp)"
trap 'rm -f "$files_file"' EXIT
if ! changed_files="$(
gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \
--jq '.changed_files'
)"; then
echo "Could not read the PR changed-files count; failing open so heavy jobs run."
node scripts/classify-docs-only.mjs --listing-error
exit 0
fi
if ! gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files" \
--paginate \
--jq '.[] | [.filename, (.previous_filename // "")] | @tsv' \
> "$files_file"; then
echo "Could not list changed files; failing open so heavy jobs run."
node scripts/classify-docs-only.mjs --listing-error
exit 0
fi
node scripts/classify-docs-only.mjs \
--changed-files-count "$changed_files" \
--listing "$files_file"
# Builds and checks every public example through its own toolchain, then
# runs examples/mcp-app's browser-app suite — the repository's only
# `@rstest/browser` consumer, which `pnpm examples:check` (each example's
# `check` script) does not include (#576).
examples-check:
needs: changes
if: >-
${{ !cancelled() &&
(github.event_name != 'pull_request' || needs.changes.outputs.docs_only != 'true') }}
name: Examples check (Node 22.19)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup-workspace
with:
node-version: 22.19.0
# The shipped `agentBundleBrowserRstest` helper launches branded Chrome
# (`channel: 'chrome'`), which Playwright cannot pin to a version, so
# this suite runs on the Chrome preinstalled on the runner image and the
# exact build is recorded in the step summary for bisecting a red run
# after an image refresh. Falls back to the full install (browser + OS
# deps) if the image ever drops Chrome.
- name: Ensure branded Chrome for the browser-app suite
run: |
if ! command -v google-chrome >/dev/null 2>&1; then
pnpm exec playwright install --with-deps chrome
fi
version="$(google-chrome --version)"
echo "Using $version"
echo "Browser-app suite ran on $version" >> "$GITHUB_STEP_SUMMARY"
- run: pnpm examples:check
- name: examples/mcp-app browser-app suite
run: pnpm --filter @agent-bundle-example/mcp-app test:browser-app
# The Verify matrix has two dimensions. `leg` splits the former serial
# `pnpm test` (unit → route-unit → projection → integration, 13.8 min on a
# PR, 66% of it the integration pool on 2 workers) into a `fast` leg
# (build, typecheck, lint, unit, route-unit, projection, ≈4 min) and two
# integration shards (`rstest --shard N/2` over rstest.integration.config.ts,
# sorted paths in contiguous slices, ≈6 and ≈4.5 min), so the PR critical
# path is the slower shard, not the sum (#576). `node-version` follows the
# event: PRs and merge-queue entries run Node 24 for every leg plus the
# `fast` leg on Node 26 — 20 of the 24 main-push failures in #576's
# 200-run window were Node-26-only breaks in the unit/projection pools that
# no PR leg had exercised — while main pushes and manual dispatches run
# every leg on the full supported matrix (22.19.0 = engines floor, 24, 26).
# Branch protection requires the static "Verify gate" context below, never
# a leg name (#549, #557), so this shape can change without touching it.
verify:
needs: changes
if: >-
${{ !cancelled() &&
(github.event_name != 'pull_request' || needs.changes.outputs.docs_only != 'true') }}
name: Verify (${{ matrix.leg }}, Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
# Slowest leg measured ≈6.5 min; ≥2.5× headroom for cold caches, but a
# hung Chrome or dev server no longer burns 45 minutes per leg.
timeout-minutes: 20
env:
# Read by the shard steps and by the partition guard in the fast leg;
# changing the count changes both (and the matrix `leg` list).
INTEGRATION_SHARD_COUNT: '2'
strategy:
fail-fast: false
matrix:
leg: [fast, integration-1, integration-2]
node-version: ${{ (github.event_name == 'pull_request' || github.event_name == 'merge_group') && fromJSON('["24"]') || fromJSON('["22.19.0","24","26"]') }}
include: ${{ (github.event_name == 'pull_request' || github.event_name == 'merge_group') && fromJSON('[{"leg":"fast","node-version":"26"}]') || fromJSON('[]') }}
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup-workspace
with:
node-version: ${{ matrix.node-version }}
# Only the integration pool drives a browser; the unit, route-unit,
# and projection pools never launch one.
playwright-browser: ${{ startsWith(matrix.leg, 'integration-') && 'chromium' || '' }}
# Build first: checked-in suites and API tests import the package's built
# type declarations, so typecheck requires dist (same order as
# `pnpm check`), and the integration pool reads the prebuilt
# packages/{agent-bundle,workbench}/dist (`test:integration:run`).
# publint runs inside each publishable package's `rslib build`
# (rsbuild-plugin-publint, throwOn: 'warning'), so a manifest warning
# already fails this step; there is no separate publint step.
- run: pnpm build
- if: matrix.leg == 'fast'
run: pnpm typecheck
- if: matrix.leg == 'fast'
run: pnpm lint
# Rstest's default reporters under GITHUB_ACTIONS=true are `default` plus
# `github-actions`, so failures already annotate the PR; no --reporter
# flag is needed on these steps (verified on @rstest/core 0.11.10).
- if: matrix.leg == 'fast'
run: pnpm test:unit
- if: matrix.leg == 'fast'
run: pnpm test:route-unit
- if: matrix.leg == 'fast'
run: pnpm test:projection
# Proves the shards partition the integration pool: `rstest list` for
# the whole pool and for each `--shard i/N` must be disjoint and add up
# to the full file list, so a shard-count or manifest change can never
# silently drop a test file from CI. Runs in the fast leg (it finishes
# long before the shards) and the gate requires the fast leg.
- if: matrix.leg == 'fast'
name: Integration shards cover every integration test file
run: node scripts/verify-rstest-shards.mjs --config rstest.integration.config.ts --count "$INTEGRATION_SHARD_COUNT"
# No `--` before --shard: pnpm forwards the flag to the script either
# way, but rstest's CLI parser discards everything after a `--`
# separator, so `pnpm test:integration:run -- --shard 1/2` ran the whole
# pool on both shards and reported green (the same shape as the
# native-host-smoke filter bug, #576). The banner check turns that
# regression into a failure: rstest prints "Running shard i of N (...)"
# only when the flag took effect. `pipefail` is set explicitly because
# the default `run:` shell is `bash -e` without it, which would let a
# failed rstest hide behind tee's exit status. The banner check is one
# grep, not `sed | grep -q`: under pipefail, grep -q exiting on the
# match sends the producer SIGPIPE (status 141) and the pipeline reads
# as failed; the pattern instead allows the colour codes rstest emits
# under GITHUB_ACTIONS before the text.
- if: startsWith(matrix.leg, 'integration-')
name: Integration pool shard (${{ matrix.leg }})
env:
LEG: ${{ matrix.leg }}
run: |
set -euo pipefail
index="${LEG#integration-}"
log="$RUNNER_TEMP/integration-shard.log"
pnpm test:integration:run --shard "${index}/${INTEGRATION_SHARD_COUNT}" 2>&1 | tee "$log"
if ! grep -qP "^(?:\x1b\[[0-9;]*m)*Running shard ${index} of ${INTEGRATION_SHARD_COUNT} \(" "$log"; then
echo "::error::rstest did not run as shard ${index}/${INTEGRATION_SHARD_COUNT} (no 'Running shard' banner) — the --shard flag was dropped and the whole pool ran."
exit 1
fi
# The shared e2e fixture records a Playwright trace per test and keeps
# it only when the test fails (`trace: 'retain-on-failure'` under CI in
# packages/workbench/tests/support/workbench-e2e.ts); this preserves
# those traces so a red browser test can be replayed with
# `playwright show-trace` instead of diagnosed from reporter text.
- if: failure() && startsWith(matrix.leg, 'integration-')
name: Preserve Playwright traces of failed browser tests
uses: actions/upload-artifact@v7
with:
name: playwright-traces-${{ matrix.leg }}-node-${{ matrix.node-version }}
path: .rstest/playwright-traces
if-no-files-found: ignore
retention-days: 7
# Required-check anchor for the Verify matrix. When a matrix job is skipped
# (docs-only PRs), GitHub reports it under its unexpanded name
# "Verify (${{ matrix.leg }}, Node ${{ matrix.node-version }})", so a branch
# protection that requires a leg name never sees that context and blocks the
# PR for good. This job has a static name and always runs after Verify. It
# passes only when every leg and shard passed (`needs.verify.result` is
# `success` only if the whole matrix succeeded) or when the matrix was
# skipped for the one legitimate reason — a docs-only pull request; a
# failed, cancelled, or otherwise skipped matrix fails the gate, so a shard
# that never ran can never be mistaken for a green one. Require
# "Verify gate" on main, not a leg name.
verify-gate:
needs: [changes, verify]
if: always()
name: Verify gate
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Require every Verify leg and shard to have passed
env:
VERIFY_RESULT: ${{ needs.verify.result }}
DOCS_ONLY: ${{ needs.changes.outputs.docs_only }}
EVENT_NAME: ${{ github.event_name }}
run: |
echo "Verify matrix result: $VERIFY_RESULT (event: $EVENT_NAME, docs_only: ${DOCS_ONLY:-unset})"
case "$VERIFY_RESULT" in
success) exit 0 ;;
skipped)
if [ "$EVENT_NAME" = pull_request ] && [ "$DOCS_ONLY" = true ]; then
echo "Docs-only pull request: the Verify matrix is skipped by design."
exit 0
fi
echo "::error::The Verify matrix was skipped, but this is not a docs-only pull request; no leg or shard ran."
exit 1
;;
*)
echo "::error::Verify matrix result is '$VERIFY_RESULT'; see the Verify (…) jobs."
exit 1
;;
esac
release-gates:
needs: changes
if: >-
${{ !cancelled() &&
(github.event_name != 'pull_request' || needs.changes.outputs.docs_only != 'true') }}
name: Release gates (Node 22.19)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
# The packed pool includes packed-release.e2e, a Workbench browser suite.
- uses: ./.github/actions/setup-workspace
with:
node-version: 22.19.0
playwright-browser: chromium
# Per-PR packed pool: single pack+install proofs plus the
# minimal-template scaffolder smoke. The full template matrix runs in
# nightly.yml's packed-matrix job, in release-candidate.yml for
# Version Packages / on-demand qualification, and in pre-publish
# `pnpm check:release`.
- run: pnpm check:release:ci
- if: failure()
name: Preserve Playwright traces of failed browser tests
uses: actions/upload-artifact@v7
with:
name: playwright-traces-release-gates
path: .rstest/playwright-traces
if-no-files-found: ignore
retention-days: 7
# Binary-gated real-host install proofs. The proof suites skip their Claude
# and Codex legs when the CLIs are absent, which is how #364 changed the
# Codex `interface.logo` emission and broke both proofs on main without CI
# noticing (#367/#368 repaired them from local runs). This job installs the
# exact CLI versions pinned in each adapter's schema PROVENANCE.json
# (`hostCli`, kept equal to `observedCliVersion` by scripts/host-cli-pins.mjs),
# fails closed if `claude`/`codex --version` differs from the pin, and runs
# the source-built proofs, the packed-tarball proofs, and the packed Claude
# plugin validation. Nothing here needs a login: every proof runs against an
# isolated HOME / CLAUDE_CONFIG_DIR / CODEX_HOME and no secrets are passed.
# Signed-in smokes (`claude -p`, `codex exec`, the Eval harnesses) stay in
# the opt-in native-host-smoke workflow.
host-install-proofs:
needs: changes
if: >-
${{ !cancelled() &&
(github.event_name != 'pull_request' || needs.changes.outputs.docs_only != 'true') }}
name: Host install proofs (Node 22.19)
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup-workspace
with:
node-version: 22.19.0
- name: Read pinned host CLI versions
id: pins
run: node scripts/host-cli-pins.mjs print
# The global npm prefix holding both CLIs (Claude Code and Codex ship
# platform-native binaries), keyed by OS, arch, and the exact pins
# (package name and version per host) so a deliberate re-pin of either
# misses the cache and installs fresh.
- name: Restore pinned host CLIs
id: host-cli-cache
uses: actions/cache@v6
with:
path: ${{ runner.temp }}/host-cli
key: host-cli-${{ runner.os }}-${{ runner.arch }}-${{ steps.pins.outputs.pins }}
- name: Install pinned host CLIs
if: steps.host-cli-cache.outputs.cache-hit != 'true'
run: node scripts/host-cli-pins.mjs install --prefix "$RUNNER_TEMP/host-cli"
# ubuntu runner: npm's global executables live in <prefix>/bin (the
# script's globalBinDirectory rule; Windows would be the prefix itself).
- name: Expose host CLIs on PATH
run: echo "$RUNNER_TEMP/host-cli/bin" >> "$GITHUB_PATH"
- name: Verify host CLI versions match the schema pins
run: node scripts/host-cli-pins.mjs verify
- run: pnpm build
- name: Host install proofs (source-built bundle)
run: pnpm test:host-install
- name: Host install proofs (packed tarball)
run: pnpm test:host-install:packed
- name: Packed Claude plugin validation proof
env:
AGENT_BUNDLE_PACKAGE_PREBUILT: '1'
run: pnpm test:packed:native
# Narrow OS matrix for the host-install / filesystem slice. The rest of CI
# stays on ubuntu-latest (Verify, examples, release-gates, the pinned-CLI
# host-install-proofs job). Cloning those onto three OSes would multiply the
# PR critical path; this job is the smallest proof that the platform-generic
# npm package's win32/darwin branches are not Ubuntu-only qualifications:
# `dev --install-host` rollback and stale ownership, install/uninstall
# receipts, durable-fs publish/fsync, packaged installer-bin resolution,
# and the #769 internal-child cwd policy. It does not install `claude` or
# `codex` — those pins stay in host-install-proofs — so Claude/Codex legs
# in `dev-host-install.test.ts` skip here the same way they skip locally
# when the binaries are absent. The slice uses
# `rstest.host-filesystem.config.ts` (not `rstest.config.ts`) so it never
# builds the Workbench e2e example payload. One Node line (22.19, engines
# floor) keeps the matrix at three cells, not nine.
host-filesystem:
needs: changes
if: >-
${{ !cancelled() &&
(github.event_name != 'pull_request' || needs.changes.outputs.docs_only != 'true') }}
name: Host filesystem (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup-workspace
with:
node-version: 22.19.0
- run: pnpm build
# Dist is already built; packed-install-bin's ad-hoc pack must not
# rebuild it. Workbench is unused by this slice.
- name: Host-install filesystem slice
env:
AGENT_BUNDLE_PACKAGE_PREBUILT: '1'
AGENT_BUNDLE_WORKBENCH_PREBUILT: '1'
run: pnpm test:host-filesystem
rsc-runtime-micro-eval:
# Deterministic end-to-end spot-check of the built RSC runtime artifacts
# (hook -> RSC worker -> shared kernel state -> MCP tool lowering) without
# any real Claude/Codex host. Login-free real-host install proofs run in
# the host-install-proofs job; signed-in native smokes stay skip-gated in
# the manually dispatched native-host-smoke workflow on purpose.
needs: changes
if: >-
${{ !cancelled() &&
(github.event_name != 'pull_request' || needs.changes.outputs.docs_only != 'true') }}
name: RSC runtime micro-eval (Node 22.19)
runs-on: ubuntu-latest
timeout-minutes: 8
steps:
- uses: actions/checkout@v7
- uses: ./.github/actions/setup-workspace
with:
node-version: 22.19.0
- run: pnpm eval:spot
# release.yml qualifies each Version Packages candidate by dispatching this
# workflow on changeset-release/main (GITHUB_TOKEN pushes start no
# pull_request run). A dispatch run is not a PR check, so without this the
# PR page and `gh pr checks` show nothing and the merge gate degrades to
# "trust the base". Post the aggregate result as a commit status on the
# candidate head; `needs` names every job that runs on dispatch, so a
# skipped or failed job is a failed status.
candidate-status:
if: >-
always() && github.event_name == 'workflow_dispatch' &&
github.ref == 'refs/heads/changeset-release/main'
needs: [verify-gate, examples-check, release-gates, host-install-proofs, host-filesystem, rsc-runtime-micro-eval]
name: Candidate status
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
statuses: write
steps:
- name: Report the dispatch result on the candidate commit
env:
NEEDS: ${{ toJSON(needs) }}
CONTEXT: CI (Version Packages candidate)
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
failed=$(jq -r 'to_entries | map(select(.value.result != "success") | "\(.key)=\(.value.result)") | join(", ")' <<<"$NEEDS")
state=success; description="every dispatched job passed"
if [ -n "$failed" ]; then state=failure; description="$failed"; fi
gh api "repos/$GITHUB_REPOSITORY/statuses/$GITHUB_SHA" \
-f state="$state" -f context="$CONTEXT" -f description="${description:0:140}" \
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" >/dev/null
echo "$CONTEXT: $state ($description)"
dependency-review:
if: github.event_name == 'pull_request'
name: Dependency review
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v7
- uses: actions/dependency-review-action@v5