Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Run these from the repository root. `pnpm examples:check` validates and builds e

## Development

`pnpm check` runs the local delivery gate (build, unit and integration tests, lint, typecheck); `pnpm check:release` adds the packaging gates. Versioning goes through Changesets. Native Claude/Codex host smokes are opt-in and intentionally skipped in CI. The Workbench architecture and the optional RSC runtime are documented in [docs/architecture/rsc-runtime-workbench.md](docs/architecture/rsc-runtime-workbench.md).
`pnpm check` runs the local delivery gate (build, unit and integration tests, lint, typecheck); `pnpm check:release` adds the packaging gates. `pnpm check:local-ci` mirrors the full hosted CI gate — the three-Node verify matrix plus the examples, release, and micro-eval jobs — in parallel local worktrees, and is the merge gate for the local-merge workflow described in [docs/local-ci.md](docs/local-ci.md). Versioning goes through Changesets. Native Claude/Codex host smokes are opt-in and intentionally skipped in CI. The Workbench architecture and the optional RSC runtime are documented in [docs/architecture/rsc-runtime-workbench.md](docs/architecture/rsc-runtime-workbench.md).

## Status

Expand Down
96 changes: 96 additions & 0 deletions docs/local-ci.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Local CI gate

`pnpm check:local-ci` proves what the hosted CI gate proves, on the
development machine, in one command — including the full Node matrix. It
exists because a hosted Verify leg takes ~13–16 minutes while a many-core
development machine can run all three legs plus the release gates
concurrently in less wall time. The local-merge workflow it enables:

1. Run `pnpm check:local-ci` on the branch's HEAD commit.
2. If the gate is green, the branch is mergeable — merge it.
3. Hosted CI still runs on the merged commit (push to `main`) and stays the
asynchronous post-merge safety net; if it disagrees with the local run,
the hosted result wins and the merge gets a follow-up fix.

For quick iteration, `pnpm check:local-ci --current-node-only` runs a single
Verify-equivalent leg on whatever Node is currently active, with the repo's
normal local worker derivation. It skips the Node matrix and the
examples/release/micro-eval gates, so it is a fast signal, not a merge gate.

## What it runs

Every leg is an isolated git worktree pinned to the HEAD commit (uncommitted
changes are not covered — the runner warns), with its own `node_modules`.
Legs live under `.worktrees/local-ci/` (gitignored), are reused across runs
for warm caches, and can be recreated with `--fresh`.

| Local leg | Node | Steps | Mirrors hosted job |
| --- | --- | --- | --- |
| `verify-node22` | 22.19.x | `install`, `playwright install chrome`, `build`, `lint:package`, `typecheck`, `lint`, `test:unit`, `test:integration` | `verify (22.19.0)` |
| `verify-node24` | 24.x | same | `verify (24)` |
| `verify-node26` | 26.x | same | `verify (26)` |
| `gates-node22` | 22.19.x | `install`, `examples:check`, `check:release`, `eval:spot` | `examples-check`, `release-gates`, `rsc-runtime-micro-eval` |

The three hosted Node-22.19 jobs fold into one `gates-node22` worktree
because each of their entry scripts starts from `pnpm build` in a fresh
install, which one worktree provides just as well as three.

All four legs run concurrently. The summary table (leg × step × status ×
duration × test census) is printed and written to
`.worktrees/local-ci/summary.md` (plus `summary.json`); per-step logs land in
`.worktrees/local-ci/logs/`. The command exits non-zero if any step fails.

## Node provisioning

The runner introduces no new tooling. For each hosted runtime line
(22.19.x, 24.x, 26.x) it resolves a Node binary from, in order:

1. `AGENT_BUNDLE_LOCAL_CI_NODE_22` / `_24` / `_26` — a Node binary or bin
directory, for machines with bespoke layouts;
2. `mise where node@<line>`;
3. `~/.nvm/versions/node/*`;
4. the current process's Node, if it matches the line.

Every resolved binary is version-checked against the hosted line before use.
If a line is missing, the runner fails with the exact install command (e.g.
`mise install node@22.19`). pnpm itself is pinned by reusing the entrypoint
that launched the runner, executed on each leg's own Node, so `pnpm`, its
lifecycle children, and `pnpm exec node` all agree on the leg's runtime —
`node_modules` trees (native modules such as the rspack bindings) are never
shared across Node ABIs, while the content-addressed pnpm store is shared
safely.

## Parallelism and time budgets

The integration pool derives workers from cores
(`rstest.integration.config.ts`), tuned for a leg that owns the machine. The
runner instead slices the machine: with N concurrent Verify legs each leg
gets `min(4, cores / (2 N))` integration workers
(`AGENT_BUNDLE_INTEGRATION_MAX_WORKERS`) and `cores / N` unit workers
(`--pool.maxWorkers`), and full runs pin `AGENT_BUNDLE_TEST_TIME_SCALE=4` —
the same polling-budget scale hosted CI uses — because four legs sharing a
machine is exactly the contention that scale exists for. Exporting
`AGENT_BUNDLE_TEST_TIME_SCALE` yourself (e.g. when the machine is also
running other heavy work) overrides the default; the integration config
never lets it drop below what its own pool shape requires.

## What is deliberately not covered

- **dependency-review** runs as a GitHub-side action against the GitHub
advisory database on the PR diff; it has no local equivalent and stays a
hosted-only, PR-time check.
- **package-preview** (pkg.pr.new) and the **release publish** workflow are
publish-side effects, not checks; nothing about them gates a merge.
- **native-host-smoke** needs signed-in Claude/Codex CLIs and is opt-in even
on hosted CI.
- **Environment skew**: hosted runners are `ubuntu-latest` with the exact
glibc/OS package set `--with-deps` installs, and hosted Verify installs
branded Chrome fresh. A green local run on a different distro, glibc, or
Chrome build is strong but not identical evidence — this is the main
reason hosted CI remains the post-merge safety net. The local `browsers`
step only validates/installs the browser itself (`playwright install
chrome`); the OS dependencies (`--with-deps`) are one-time machine setup
and may need root.
- **Job isolation**: hosted gives every job a fresh VM; local legs reuse
worktrees for speed. `--fresh` restores cold-start fidelity when staleness
is suspected.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"lint": "rslint .",
"typecheck": "tsc --noEmit && tsc --project packages/workbench/tsconfig.json && tsc --project packages/create-agent-bundle/tsconfig.json",
"check": "pnpm build && pnpm test:unit && pnpm test:integration:run && pnpm lint && pnpm typecheck",
"check:local-ci": "node scripts/local-ci.mjs",
"docs:runtime-topology": "node scripts/rsc-runtime-topology.mjs --root . --output docs/architecture/rsc-runtime-workbench.md",
"eval:spot": "pnpm build && pnpm --filter @agent-bundle/rsc-agent-runtime-demo build && pnpm --filter @agent-bundle/rsc-agent-runtime-demo exec rstest run tests/micro-eval.spot.test.ts --config rstest.config.ts",
"check:runtime-topology": "node scripts/rsc-runtime-topology.mjs --root . --output docs/architecture/rsc-runtime-workbench.md --check",
Expand Down
16 changes: 15 additions & 1 deletion rstest.integration.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ const maxWorkers = Number.isSafeInteger(overrideWorkers) && overrideWorkers >= 1
? 1
: Math.max(1, Math.min(4, Math.floor(availableParallelism() / 2)));

/**
* Polling budgets scale with contention. A multi-worker pool needs at least
* 2 (see the env comment below); an externally set
* AGENT_BUNDLE_TEST_TIME_SCALE raises it further when the machine is shared —
* scripts/local-ci.mjs passes 4 (hosted CI's own scale) because it runs
* three Node legs plus the release gates concurrently. The external value
* never lowers the scale below what the pool shape requires.
*/
const externalTimeScale = Number(process.env['AGENT_BUNDLE_TEST_TIME_SCALE'] ?? '');
const poolTimeScale = maxWorkers > 1 ? 2 : 1;
const timeScale = Number.isSafeInteger(externalTimeScale) && externalTimeScale >= 1
? Math.max(externalTimeScale, poolTimeScale)
: poolTimeScale;

/**
* Build- and process-running tests that only read workspace-shared artifacts;
* files that WRITE shared locations (root builds, `npm pack`) run through the
Expand All @@ -36,7 +50,7 @@ export default defineConfig({
// parallel runs double the polling budgets (see tests/support/time-scale.ts)
// and raise the 5s default test timeout, which real in-process builds can
// exceed when workers share the machine. Explicit per-test timeouts win.
env: { AGENT_BUNDLE_TEST_TIME_SCALE: maxWorkers > 1 ? '2' : '1' },
env: { AGENT_BUNDLE_TEST_TIME_SCALE: String(timeScale) },
testTimeout: 30_000,
// isolate: false would cut Playwright startup cost, but the log pipeline
// suites rely on per-file module isolation (verified: logs-real.e2e fails
Expand Down
Loading
Loading