You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Rails 8.1 made local CI first-class: bin/ci runs a declared step list on the developer's machine with timed steps, ✅/❌ result lines, a failure summary, --fail-fast, parallel groups whose output is captured and replayed whole, and an optional gh signoff. The generated GitHub workflow is the optional half (rails new --skip-ci drops it), and the guide tells every other provider to point the pipeline at bin/ci.
WebJs is the inverse today. The scaffold ships a four-job GitHub workflow (packages/cli/templates/.github/workflows/ci.yml) and no local aggregate. A developer or an agent runs check, doctor, typecheck, test:server, test:browser, and the e2e variant as six separate commands, nothing times them or produces one verdict, and the workflow restates the same list by hand so the two drift. The research record with the full Rails study and the decisions below is #1470.
Config is JSON in the webjs block, not a ci.ts DSL."webjs": { "ci": { "steps": [...] } }, the Unify webjs dev/start/db with npm-script behavior via a declarative tasks config #550 seam next to webjs.dev / webjs.start. A step is a string (shorthand for { title: s, run: s }), a { title, run, env? } command, or a { title, steps, parallel? } group (parallel is a slot count, default 1). A group inside a parallel group runs sequentially in one slot; a nested parallel is a config problem. env exists so the e2e opt-in does not depend on a POSIX VAR=1 cmd prefix.
webjs ci [--fail-fast|-f] [--only <title>]... [--json] [--signoff]. Every child gets CI=true plus the npm-style PATH. Output mirrors Rails (heading, ✅ <title> passed in 2.11s, ❌ ... failed in ..., total line, ↳ <title> failed summary), exit 1 on any failure. Runs wherever the nearest package.json declares webjs.ci (a workspace root included, so the monorepo itself runs local CI); a missing block is the error, with a hint naming workspace members that declare one. Under GitHub Actions it wraps each step in ::group:: / ::endgroup:: and emits ::error:: annotations for failed steps, so a failure still names its layer in the Actions log. --json prints one document { ok, seconds, steps: [{ title, run, group, ok, code, seconds, output? }] } (output only for failed steps) with the human output on stderr. --signoff runs gh signoff after a green run; off by default. With GITHUB_STEP_SUMMARY set, a markdown table of step results is appended.
Every new app ships a default webjs.ci block and a "ci": "webjs ci" script. Setup (webjs db migrate), then a Checks group parallel: 2 of webjs check, webjs doctor, webjs typecheck, a dependency audit (npm audit --audit-level=high, bun audit on a Bun scaffold), and a SEQUENTIAL Tests sub-group of webjs test --server, webjs test --browser, and webjs test --server with env: { WEBJS_E2E: '1' }. Tests stay sequential because the server and e2e layers share one SQLite file. Every default step is a bare webjs ... command, never npm run ..., the invariant test/scaffolds/scaffold-runtime.test.js:60-62 pins for before.
The scaffold workflow collapses to ONE job that runs npm run ci (setup-node, npm ci, Playwright Chromium, puppeteer-core, the CHROMIUM_PATH resolve, db:generate && db:migrate with DATABASE_URL: file:./ci.db, then npm run ci). The step list has one source. A team wanting per-layer required checks back uses a matrix over webjs ci --only "<group>"; the workflow comment says so.
webjs create --skip-ci omits only .github/workflows/ci.yml. The PR template stays (test/scaffolds/scaffold-agent-skills.test.js:72 expects it). The default keeps generating the workflow, because Move the test gate from pre-commit to CI (framework + scaffolded apps) #188's rationale (a gate a local --no-verify cannot skip) still holds.
The monorepo root gets a webjs.ci block mirroring its GitHub jobs (conventions, unit, browser, e2e, Bun matrix, dist, apps) and a ci script, documented in framework-dev.md, so the framework runs its own local CI. The three in-repo apps (gallery, examples/blog, website) get a ci script and a webjs.ci block using their npm scripts (the website's pretest hooks fire only through npm run). The framework's own .github/workflows/ci.yml jobs are NOT converted (they are the required checks).
Not in scope: a doctor advisory for a missing webjs.ci, gh signoff fail posting, MCP exposure.
Implementation notes (for the implementing agent)
Where to edit
New files
packages/cli/lib/ci-config.js: readCiConfig(appDir, readFile?) returning { steps, problems }, plus normalizeSteps(raw, path, inParallel) and selectSteps(steps, only) (an unknown --only title is a problem, not silence). Pure, injectable reader, same pattern as packages/cli/lib/app-tasks.js (copy its private readWebjsBlock helper). Problems carry the JSON path, e.g. webjs.ci.steps[1].steps[0].
packages/cli/lib/ci-runner.js: runCi(steps, cwd, opts) with opts = { spawn, write, isTTY, now, timers, failFast, captureAll, env } returning { ok, seconds, steps, interrupted } plus an interrupt() handle. Internals: a pure worker pool runPool(tasks, slots, runOne, { failFast, isInterrupted }) and runOne(step, ctx). Exported pure formatters: formatElapsed (Rails' 2.11s / 1m2.11s), formatHeading, formatResult, formatProgress, formatSummary, stepSummaryMarkdown. Pure of process.exit and console, same contract as packages/cli/lib/run-tasks.js L30-33.
Tests: packages/cli/test/ci-config/ci-config.test.mjs, packages/cli/test/ci-runner/ci-runner.test.mjs (fake spawn like packages/cli/test/run-tasks/run-tasks.test.mjs L14-29, extended with stdout / stderr emitters; fake now counter), test/cli/ci.test.mjs (real bin against a temp app, pattern test/cli/check-target.test.mjs L33-42), test/bun/ci-runner.mjs + test/bun/ci-runner.test.mjs (pattern test/bun/run-tasks.mjs; auto-discovered by scripts/run-bun-tests.js), test/repo-health/in-repo-ci-blocks.test.mjs.
Framework
packages/cli/lib/run-tasks.js L15: export envWithLocalBin (and killChildTree, which already has the ESRCH fallback).
packages/cli/lib/check-target.js L72-97: export workspaceApps(cwd) so the missing-config hint can name workspace members; no app-dir refusal for ci (the config, not app/, is the predicate).
packages/cli/bin/webjs.js: USAGE banner (L87-125), the HELP map (L137-281, entries are { usage, summary, options?, examples }), a case 'ci' beside case 'check' (L753). flag() is at L360. Load .env via loadAppEnv before the steps, mirroring dev / start (dogfood: webjs dev/start ignore PORT in .env (port read before loadEnvFile) #447), so a local webjs db migrate sees DATABASE_URL. Set process.exitCode instead of calling process.exit after the run. case 'create' (L1299-1359): add --skip-ci next to --no-install (L1346) and pass skipCi to scaffoldApp.
packages/core/src/webjs-config.d.ts: WebjsCiStep, WebjsCiGroup, WebjsCiConfig, and the member ci?: WebjsCiConfig inside interface WebjsConfig at TWO-space indent (the drift test's regex is ^ {2}(\w+)\??:).
packages/server/webjs-config.schema.json (draft-07, so definitions, not $defs): a ci property with description, additionalProperties: false, steps array whose items $ref#/definitions/ciStep, a oneOf of string (minLength 1) | command object (title, run, env with string values) | group object (title, parallel integer ≥ 1, steps of #/definitions/ciNestedStep, the same shape minus parallel). Every property needs a description (the drift test requires it).
packages/server/test/config/webjs-config-schema.test.js: add ci to KNOWN_KEYS (L50-71); add a nested guard for ci.steps modeled on the regenerate guard (L193-212).
packages/server/src/webjs-config-validate.js docblock L44-50 and website/app/docs/configuration/page.ts L167: the "17 keys" / "9 of the 18" counts move by one.
test/types/webjs-config.test-d.ts: a valid ci block in full, plus @ts-expect-error counterfactuals.
.claude/hooks/block-prose-punctuation.sh L309 webjs_cli= alternation: add ci. test/hooks/block-prose-punctuation.test.mjs L307 hard-fails otherwise. Check packages/cli/templates/.claude/hooks/block-prose-punctuation.sh for the same list.
Scaffold (invoke the scaffold-sync skill first)
packages/cli/lib/create.js: ci: 'webjs ci' after doctor in scripts (L423, the plain-tooling group, no bun --bun); the default webjs.ci block after doctor: { gate } (L570); opts.skipCi filters .github/workflows/ci.yml out of templateFiles (L667).
packages/cli/templates/.github/workflows/ci.yml: one job. Keep the exact 4-line setup-node block including cache: npm, because bunifyCi (packages/cli/lib/runtime-rewrite.js L176-190) matches it verbatim to inject setup-bun; npm run ci becomes bun run ci through its generic npm run rewrite.
Prose: packages/cli/templates/.hooks/pre-commit comment L10-13, templates/.agents/rules/workflow.md item 4 (L53-61), templates/partials/agents-playbook-fullstack.md L100-125 and agents-playbook-api.md L49-70 (the checklist and the Commands fence), templates/.github/pull_request_template.md test plan.
gallery/package.json, examples/blog/package.json, website/package.json: ci script + webjs.ci block.
Docs (invoke the doc-sync skill first; rows: new CLI command, new webjs.* key, scaffold convention)
AGENTS.md: the CLI reference fence (L568-584: a webjs ci line, --skip-ci on the create line), the "webjs" block bullet (L594), Code-workflow item 4.
.agents/skills/webjs/references/built-ins.md after the dev/start orchestration section (L207-218); references/testing.md next to ## App runners (L56); SKILL.md Testing Defaults (L265).
website/app/docs/configuration/page.ts: <h3>webjs ci</h3> in CLI Options (after L76); website/app/docs/testing/page.ts: <h2>webjs ci command</h2> after the webjs test section (L186-199); website/app/docs/deployment/page.ts checklist (L410-421).
README.md L128 and the Status paragraph; packages/cli/README.md Commands fence (L38-57) and templates list (L71); packages/cli/AGENTS.md command table (L179-184); packages/server/AGENTS.md reader inventory (L147-192).
Landmines
Four tests pin the four-job workflow by regex: test/scaffolds/scaffold-integration.test.js L284-290, test/scaffolds/scaffold-template-validation.test.js L141 (/^\s+- run: npm run doctor$/m), test/scaffolds/scaffold-runtime.test.js L112 (- run: bun run check), and packages/cli/test/runtime-rewrite/runtime-rewrite.test.mjs L125-152. Rewrite them to assert npm run ci / bun run ci in the workflow and the layers in pkg.webjs.ci.steps.
bunifyCi's .replaceAll('- run: npm ci', '- run: bun install') does not touch - run: npm run ci, but keep the two spellings apart in the file with a comment; scaffold-runtime.test.js L114 asserts no npm ci survives in the Bun flavour.
Captured children: spawn detached: true with stdio: ['ignore', 'pipe', 'pipe'] (a TTY-reading child stops on SIGTTIN otherwise), resolve on close not exit (data can arrive after exit), with a bounded grace after exit for a leaked grandchild holding the pipe. Sequential steps stay non-detached with inherited stdio so Ctrl-C reaches them natively, the same split webjs dev makes (run-tasks.js L74, bin/webjs.js L470-471). A child that exits by signal is interrupted, not failed.
Progress line: only on a TTY, only while a pool is active, unref() the interval, clear before every replay and before the group summary, and make every replayed buffer end in a newline. Never render it while an inherit-stdio step owns the terminal.
FORCE_COLOR=1 only on captured children and only when stdout is a TTY. Node has no PTY without a native dependency, which is disqualified. node --test under a pipe reports as TAP; document, do not work around.
GITHUB_STEP_SUMMARY: escape | in titles and commands.
The boot validator never follows $ref (webjs-config-validate.js L60-80 checks top-level membership only), so readCiConfig must validate the step shapes itself and the bin must refuse with exit 1 on any problem.
webjs check refuses at a workspace root; webjs ci must NOT, because the monorepo root is a legitimate ci target. The predicate is the webjs.ci block.
The audit step: keep it in the scaffold default only if a freshly generated app passes npm audit --audit-level=high (and bun audit on Bun 1.3.14). If a default install has a high finding, drop the step and say so in the PR.
Website steps must go through npm (pretest / pretest:browser run scripts/copy-registry.mjs); a webjs test step there reds the suite.
Invariant 11: no em-dashes, no pause hyphens, WebJs capitalised in prose, webjs ci lowercase only as a command.
Invariants
No new dependency, no build step (AGENTS.md "Deliberately deferred"). packages/ stays plain .js + JSDoc.
The schema, the WebjsConfig type, the reader, and KNOWN_KEYS move in lockstep (packages/server/AGENTS.md L190-196).
The task's worktree, commit per logical unit, push after each, draft PR first, conventional feat: title, Closes #<this issue> in the body.
Test layers and doc surfaces
Unit (packages/cli/test/**, packages/server/test/config/**), spawn-based CLI (test/cli/**), type fixture (test/types/**), Bun parity (test/bun/ci-runner.*), scaffold (test/scaffolds/**, plus generating both templates on both runtimes with install: false, and one full-stack app WITH install running npm run ci end to end), repo-health (test/repo-health/**), hook drift (test/hooks/block-prose-punctuation.test.mjs). Docs as listed above; the changelog is generated from the feat: PR title.
Acceptance criteria
webjs ci in a scaffolded app runs every declared step, prints Rails-shaped output with per-step timing, a total line and a failure summary, and exits 1 on any failure; --fail-fast stops after the first failure; --only selects by title and reports an unknown title; --json emits exactly one JSON document on stdout
A parallel group never runs more than parallel steps at once, a nested group takes one slot sequentially, and captured output is replayed whole and never interleaved (counterfactual: two children emitting alternately)
webjs ci with no webjs.ci block exits 1 naming the workspace members that declare one (JSON: error.code = 'NO_CI_CONFIG'); npm run ci at the monorepo root runs the framework's own local CI
Under GITHUB_ACTIONS, each step's output is wrapped in ::group:: / ::endgroup:: and a failed step emits an ::error:: annotation
webjs.ci is in the schema, the WebjsConfig type, KNOWN_KEYS, and the type fixture, with the nested-parallel rule expressed in the schema and enforced by the reader
A fresh full-stack and api app (Node and Bun flavours) carries the default webjs.ci block, a ci script, and a single-job workflow running npm run ci / bun run ci; webjs create --skip-ci emits no workflow and still emits the PR template
A fresh full-stack app with dependencies installed passes npm run ci end to end
npm run ci passes in gallery, examples/blog, and website
A counterfactual proves each new test fires (fail-fast, pool cap, replay, exit-then-data, signal-as-interrupted, root refusal)
Tests cover the new behaviour at every layer it touches, including test/bun/ci-runner.* on both runtimes
Problem
Rails 8.1 made local CI first-class:
bin/ciruns a declared step list on the developer's machine with timed steps, ✅/❌ result lines, a failure summary,--fail-fast, parallel groups whose output is captured and replayed whole, and an optionalgh signoff. The generated GitHub workflow is the optional half (rails new --skip-cidrops it), and the guide tells every other provider to point the pipeline atbin/ci.WebJs is the inverse today. The scaffold ships a four-job GitHub workflow (
packages/cli/templates/.github/workflows/ci.yml) and no local aggregate. A developer or an agent runscheck,doctor,typecheck,test:server,test:browser, and the e2e variant as six separate commands, nothing times them or produces one verdict, and the workflow restates the same list by hand so the two drift. The research record with the full Rails study and the decisions below is #1470.Design / approach
Settled in #1470, not reopened here:
webjsblock, not aci.tsDSL."webjs": { "ci": { "steps": [...] } }, the Unify webjs dev/start/db with npm-script behavior via a declarative tasks config #550 seam next towebjs.dev/webjs.start. A step is astring(shorthand for{ title: s, run: s }), a{ title, run, env? }command, or a{ title, steps, parallel? }group (parallelis a slot count, default 1). A group inside a parallel group runs sequentially in one slot; a nestedparallelis a config problem.envexists so the e2e opt-in does not depend on a POSIXVAR=1 cmdprefix.webjs ci [--fail-fast|-f] [--only <title>]... [--json] [--signoff]. Every child getsCI=trueplus the npm-style PATH. Output mirrors Rails (heading,✅ <title> passed in 2.11s,❌ ... failed in ..., total line,↳ <title> failedsummary), exit 1 on any failure. Runs wherever the nearestpackage.jsondeclareswebjs.ci(a workspace root included, so the monorepo itself runs local CI); a missing block is the error, with a hint naming workspace members that declare one. Under GitHub Actions it wraps each step in::group::/::endgroup::and emits::error::annotations for failed steps, so a failure still names its layer in the Actions log.--jsonprints one document{ ok, seconds, steps: [{ title, run, group, ok, code, seconds, output? }] }(output only for failed steps) with the human output on stderr.--signoffrunsgh signoffafter a green run; off by default. WithGITHUB_STEP_SUMMARYset, a markdown table of step results is appended.webjs.ciblock and a"ci": "webjs ci"script. Setup (webjs db migrate), then aChecksgroupparallel: 2ofwebjs check,webjs doctor,webjs typecheck, a dependency audit (npm audit --audit-level=high,bun auditon a Bun scaffold), and a SEQUENTIALTestssub-group ofwebjs test --server,webjs test --browser, andwebjs test --serverwithenv: { WEBJS_E2E: '1' }. Tests stay sequential because the server and e2e layers share one SQLite file. Every default step is a barewebjs ...command, nevernpm run ..., the invarianttest/scaffolds/scaffold-runtime.test.js:60-62pins forbefore.npm run ci(setup-node,npm ci, Playwright Chromium,puppeteer-core, theCHROMIUM_PATHresolve,db:generate && db:migratewithDATABASE_URL: file:./ci.db, thennpm run ci). The step list has one source. A team wanting per-layer required checks back uses a matrix overwebjs ci --only "<group>"; the workflow comment says so.webjs create --skip-ciomits only.github/workflows/ci.yml. The PR template stays (test/scaffolds/scaffold-agent-skills.test.js:72expects it). The default keeps generating the workflow, because Move the test gate from pre-commit to CI (framework + scaffolded apps) #188's rationale (a gate a local--no-verifycannot skip) still holds.webjs ciis a pre-push habit, not a commit hook.webjs.ciblock mirroring its GitHub jobs (conventions, unit, browser, e2e, Bun matrix, dist, apps) and aciscript, documented inframework-dev.md, so the framework runs its own local CI. The three in-repo apps (gallery,examples/blog,website) get aciscript and awebjs.ciblock using theirnpmscripts (the website'spretesthooks fire only throughnpm run). The framework's own.github/workflows/ci.ymljobs are NOT converted (they are the required checks).webjs.ci,gh signoff failposting, MCP exposure.Implementation notes (for the implementing agent)
Where to edit
New files
packages/cli/lib/ci-config.js:readCiConfig(appDir, readFile?)returning{ steps, problems }, plusnormalizeSteps(raw, path, inParallel)andselectSteps(steps, only)(an unknown--onlytitle is a problem, not silence). Pure, injectable reader, same pattern aspackages/cli/lib/app-tasks.js(copy its privatereadWebjsBlockhelper). Problems carry the JSON path, e.g.webjs.ci.steps[1].steps[0].packages/cli/lib/ci-runner.js:runCi(steps, cwd, opts)withopts = { spawn, write, isTTY, now, timers, failFast, captureAll, env }returning{ ok, seconds, steps, interrupted }plus aninterrupt()handle. Internals: a pure worker poolrunPool(tasks, slots, runOne, { failFast, isInterrupted })andrunOne(step, ctx). Exported pure formatters:formatElapsed(Rails'2.11s/1m2.11s),formatHeading,formatResult,formatProgress,formatSummary,stepSummaryMarkdown. Pure ofprocess.exitandconsole, same contract aspackages/cli/lib/run-tasks.jsL30-33.packages/cli/test/ci-config/ci-config.test.mjs,packages/cli/test/ci-runner/ci-runner.test.mjs(fake spawn likepackages/cli/test/run-tasks/run-tasks.test.mjsL14-29, extended withstdout/stderremitters; fakenowcounter),test/cli/ci.test.mjs(real bin against a temp app, patterntest/cli/check-target.test.mjsL33-42),test/bun/ci-runner.mjs+test/bun/ci-runner.test.mjs(patterntest/bun/run-tasks.mjs; auto-discovered byscripts/run-bun-tests.js),test/repo-health/in-repo-ci-blocks.test.mjs.Framework
packages/cli/lib/run-tasks.jsL15: exportenvWithLocalBin(andkillChildTree, which already has the ESRCH fallback).packages/cli/lib/check-target.jsL72-97: exportworkspaceApps(cwd)so the missing-config hint can name workspace members; no app-dir refusal forci(the config, notapp/, is the predicate).packages/cli/bin/webjs.js:USAGEbanner (L87-125), theHELPmap (L137-281, entries are{ usage, summary, options?, examples }), acase 'ci'besidecase 'check'(L753).flag()is at L360. Load.envvialoadAppEnvbefore the steps, mirroringdev/start(dogfood: webjs dev/start ignore PORT in .env (port read before loadEnvFile) #447), so a localwebjs db migrateseesDATABASE_URL. Setprocess.exitCodeinstead of callingprocess.exitafter the run.case 'create'(L1299-1359): add--skip-cinext to--no-install(L1346) and passskipCitoscaffoldApp.packages/core/src/webjs-config.d.ts:WebjsCiStep,WebjsCiGroup,WebjsCiConfig, and the memberci?: WebjsCiConfiginsideinterface WebjsConfigat TWO-space indent (the drift test's regex is^ {2}(\w+)\??:).packages/server/webjs-config.schema.json(draft-07, sodefinitions, not$defs): aciproperty withdescription,additionalProperties: false,stepsarray whose items$ref#/definitions/ciStep, aoneOfof string (minLength 1) | command object (title,run,envwith string values) | group object (title,parallelinteger ≥ 1,stepsof#/definitions/ciNestedStep, the same shape minusparallel). Every property needs adescription(the drift test requires it).packages/server/test/config/webjs-config-schema.test.js: addcitoKNOWN_KEYS(L50-71); add a nested guard forci.stepsmodeled on theregenerateguard (L193-212).packages/server/src/webjs-config-validate.jsdocblock L44-50 andwebsite/app/docs/configuration/page.tsL167: the "17 keys" / "9 of the 18" counts move by one.test/types/webjs-config.test-d.ts: a validciblock infull, plus@ts-expect-errorcounterfactuals..claude/hooks/block-prose-punctuation.shL309webjs_cli=alternation: addci.test/hooks/block-prose-punctuation.test.mjsL307 hard-fails otherwise. Checkpackages/cli/templates/.claude/hooks/block-prose-punctuation.shfor the same list.Scaffold (invoke the scaffold-sync skill first)
packages/cli/lib/create.js:ci: 'webjs ci'afterdoctorinscripts(L423, the plain-tooling group, nobun --bun); the defaultwebjs.ciblock afterdoctor: { gate }(L570);opts.skipCifilters.github/workflows/ci.ymlout oftemplateFiles(L667).packages/cli/templates/.github/workflows/ci.yml: one job. Keep the exact 4-line setup-node block includingcache: npm, becausebunifyCi(packages/cli/lib/runtime-rewrite.jsL176-190) matches it verbatim to inject setup-bun;npm run cibecomesbun run cithrough its genericnpm runrewrite.packages/cli/templates/.hooks/pre-commitcomment L10-13,templates/.agents/rules/workflow.mditem 4 (L53-61),templates/partials/agents-playbook-fullstack.mdL100-125 andagents-playbook-api.mdL49-70 (the checklist and the Commands fence),templates/.github/pull_request_template.mdtest plan.gallery/package.json,examples/blog/package.json,website/package.json:ciscript +webjs.ciblock.Docs (invoke the doc-sync skill first; rows: new CLI command, new
webjs.*key, scaffold convention)AGENTS.md: the CLI reference fence (L568-584: awebjs ciline,--skip-cion thecreateline), the"webjs"block bullet (L594), Code-workflow item 4..agents/skills/webjs/references/built-ins.mdafter the dev/start orchestration section (L207-218);references/testing.mdnext to## App runners(L56);SKILL.mdTesting Defaults (L265).website/app/docs/configuration/page.ts:<h3>webjs ci</h3>in CLI Options (after L76);website/app/docs/testing/page.ts:<h2>webjs ci command</h2>after thewebjs testsection (L186-199);website/app/docs/deployment/page.tschecklist (L410-421).README.mdL128 and the Status paragraph;packages/cli/README.mdCommands fence (L38-57) and templates list (L71);packages/cli/AGENTS.mdcommand table (L179-184);packages/server/AGENTS.mdreader inventory (L147-192).Landmines
test/scaffolds/scaffold-integration.test.jsL284-290,test/scaffolds/scaffold-template-validation.test.jsL141 (/^\s+- run: npm run doctor$/m),test/scaffolds/scaffold-runtime.test.jsL112 (- run: bun run check), andpackages/cli/test/runtime-rewrite/runtime-rewrite.test.mjsL125-152. Rewrite them to assertnpm run ci/bun run ciin the workflow and the layers inpkg.webjs.ci.steps.bunifyCi's.replaceAll('- run: npm ci', '- run: bun install')does not touch- run: npm run ci, but keep the two spellings apart in the file with a comment;scaffold-runtime.test.jsL114 asserts nonpm cisurvives in the Bun flavour.detached: truewithstdio: ['ignore', 'pipe', 'pipe'](a TTY-reading child stops on SIGTTIN otherwise), resolve onclosenotexit(data can arrive afterexit), with a bounded grace afterexitfor a leaked grandchild holding the pipe. Sequential steps stay non-detached with inherited stdio so Ctrl-C reaches them natively, the same splitwebjs devmakes (run-tasks.jsL74,bin/webjs.jsL470-471). A child that exits by signal isinterrupted, not failed.unref()the interval, clear before every replay and before the group summary, and make every replayed buffer end in a newline. Never render it while an inherit-stdio step owns the terminal.FORCE_COLOR=1only on captured children and only when stdout is a TTY. Node has no PTY without a native dependency, which is disqualified.node --testunder a pipe reports as TAP; document, do not work around.GITHUB_STEP_SUMMARY: escape|in titles and commands.$ref(webjs-config-validate.jsL60-80 checks top-level membership only), soreadCiConfigmust validate the step shapes itself and the bin must refuse with exit 1 on any problem.webjs checkrefuses at a workspace root;webjs cimust NOT, because the monorepo root is a legitimatecitarget. The predicate is thewebjs.ciblock.npm audit --audit-level=high(andbun auditon Bun 1.3.14). If a default install has a high finding, drop the step and say so in the PR.npm(pretest/pretest:browserrunscripts/copy-registry.mjs); awebjs teststep there reds the suite.npm run worktree:linkafter cutting the worktree; delete everynode_modulessymlink before any real install.WebJscapitalised in prose,webjs cilowercase only as a command.Invariants
packages/stays plain.js+ JSDoc.WebjsConfigtype, the reader, andKNOWN_KEYSmove in lockstep (packages/server/AGENTS.mdL190-196).feat:title,Closes #<this issue>in the body.Test layers and doc surfaces
Unit (
packages/cli/test/**,packages/server/test/config/**), spawn-based CLI (test/cli/**), type fixture (test/types/**), Bun parity (test/bun/ci-runner.*), scaffold (test/scaffolds/**, plus generating both templates on both runtimes withinstall: false, and one full-stack app WITH install runningnpm run ciend to end), repo-health (test/repo-health/**), hook drift (test/hooks/block-prose-punctuation.test.mjs). Docs as listed above; the changelog is generated from thefeat:PR title.Acceptance criteria
webjs ciin a scaffolded app runs every declared step, prints Rails-shaped output with per-step timing, a total line and a failure summary, and exits 1 on any failure;--fail-faststops after the first failure;--onlyselects by title and reports an unknown title;--jsonemits exactly one JSON document on stdoutparallelsteps at once, a nested group takes one slot sequentially, and captured output is replayed whole and never interleaved (counterfactual: two children emitting alternately)webjs ciwith nowebjs.ciblock exits 1 naming the workspace members that declare one (JSON:error.code = 'NO_CI_CONFIG');npm run ciat the monorepo root runs the framework's own local CIGITHUB_ACTIONS, each step's output is wrapped in::group::/::endgroup::and a failed step emits an::error::annotationwebjs.ciis in the schema, theWebjsConfigtype,KNOWN_KEYS, and the type fixture, with the nested-parallel rule expressed in the schema and enforced by the readerwebjs.ciblock, aciscript, and a single-job workflow runningnpm run ci/bun run ci;webjs create --skip-ciemits no workflow and still emits the PR templatenpm run ciend to endnpm run cipasses ingallery,examples/blog, andwebsitetest/bun/ci-runner.*on both runtimesResearch record: #1470.