feat: add webjs ci, a local CI runner the scaffold workflow reuses - #1472
feat: add webjs ci, a local CI runner the scaffold workflow reuses#1472vivek7405 wants to merge 6 commits into
webjs ci, a local CI runner the scaffold workflow reuses#1472Conversation
Local CI (#1471) needs a step list an app declares once and every tool can read without importing app code, so it lives in the package.json `webjs` block next to the #550 dev/start orchestration. This lands the key in the three-surface lockstep (the JSON Schema with recursive definitions for a step, the WebjsConfig type, KNOWN_KEYS plus a nested guard for the recursive shape) and the pure CLI reader that normalizes the shorthand and reports every malformed entry with its JSON path. The reader validates the shapes itself because the boot validator only checks top-level key membership and never follows a $ref, and a step that is silently dropped is a check that never ran, the exact false green local CI exists to prevent. A group nested inside a parallel group takes one slot and runs sequentially, so a `parallel` on it is reported rather than honoured, the same rule Rails' runner applies. The runner and the `ci` command follow in the next commits.
The runner behind the ci command (#1471), modeled on Rails 8.1's ActiveSupport::ContinuousIntegration: a heading and a timed result line per step, a failure list plus one total line, fail-fast, and parallel groups whose steps run on N slots with output captured and replayed whole so nothing interleaves, a nested group taking one slot. Two spawn shapes on purpose. A sequential step inherits stdio and is not detached, so it owns the terminal and Ctrl-C reaches it natively, the split webjs dev already makes for before-steps versus watchers. A captured step is detached (its own process group, reaped on interrupt), has stdin ignored so a TTY-reading tool cannot stop on SIGTTIN and hang the pool, and resolves on close rather than exit, with a bounded grace so a leaked grandchild holding the pipe cannot hang the run. FORCE_COLOR reaches captured children only when the parent's stdout is a TTY; Node has no PTY without a native dependency. Under GitHub Actions each step is folded into a log group and a failure is annotated, so one cloud job running the whole list still names the layer that broke, which is what the per-layer jobs used to buy. Pure of process.exit, console, and the clock, so the slot cap, the fail-fast cutoff, replay atomicity, exit-then-data ordering, and the grace path are all proven with a scripted fake child.
The local CI command (#1471): run the step list package.json declares under webjs.ci, with -f/--fail-fast, --only <title>, --json, and an opt-in --signoff that posts a green commit status through gh signoff after a green run, the Rails 8.1 bin/ci posture. The predicate is the config, not an app/ directory, unlike webjs check, because a workspace root is a legitimate target and this monorepo declares its own list. Nothing declared is exit 1 rather than 0, since a run of zero steps would read as green; the refusal names the workspace members that do declare one. A malformed block refuses with every problem's JSON path and runs nothing. Under --json stdout carries exactly one document and the human report moves to stderr; the exit code is set through exitCode so a non-TTY stdout is never truncated. .env is loaded before the steps, like dev and start, so a local db migrate step sees DATABASE_URL while a CI runner's explicit env still wins. The prose hook's CLI subcommand list gains ci so its drift test keeps passing, and the end-to-end CLI test and the Node + Bun proof script cover the exit codes, the output shape, the env every child sees, fail-fast, --only, the refusals, the GitHub Actions log groups and step summary, and a real interrupt reaping a detached sleep.
Every new app now declares its CI once, in package.json under webjs.ci (#1471): a Setup step, then a Checks group running two at a time (webjs check, webjs doctor, webjs typecheck, a dependency audit) with a sequential Tests sub-group (the server, browser, and e2e layers), plus a `ci` script. The Tests group stays sequential because the server and e2e layers share one SQLite file. Every step is a bare `webjs ...` command, the same bar the before-steps meet, and a Bun app audits with bun audit. The generated GitHub workflow collapses from four hand-restated jobs to one job that prepares the runner and runs `npm run ci`, so the cloud and local lists cannot drift, which is the Rails guide's rule for every provider. The cloud practices stay: a read-only token, a bounded job, a concurrency cancel, and, through the runner's Actions mode, a log group per step, an annotation per failure, and a step table in the job summary, so a failure still names its layer. A team wanting per-layer required checks runs `webjs ci --only` in a matrix, as the workflow comment says. `webjs create --skip-ci` omits the workflow and nothing else (rails new parity); the local list always ships. The pre-commit hook is unchanged per #174: `npm run ci` is the pre-push gate, and the scaffold's agent rule files, playbooks, and PR template now say so.
The framework dogfoods its own local CI (#1471). The root package.json declares a webjs.ci list mirroring the GitHub jobs (setup, the per-app check and doctor plus the two source invariants three at a time, the root test suite, the in-repo app typechecks and suites, the browser suite, the blog e2e, the Bun matrix) behind `npm run ci`, and gallery, examples/blog, and website each declare a shorter list behind their own `ci` script. The root script and every root step invoke this checkout's CLI by path rather than a hoisted bin, because in a linked worktree node_modules/.bin resolves into the primary checkout. The app lists go through their npm scripts because the website's pretest hook copies the ui registry, which a bare test invocation would skip. A repo-health test parses all four blocks through the same reader the command uses and checks that every npm script a step names exists in the package it targets, so a stale step fails here rather than at run time with a message that never mentions the block. The GitHub workflow itself is unchanged: its jobs are the required merge checks.
The ci command and the webjs.ci config key (#1471) on each surface the doc-sync map names: the AGENTS.md CLI reference, config-block bullet, and code-workflow items; the agent skill (a Local CI section in built-ins.md, a one-command section in testing.md, a Testing Defaults bullet in SKILL.md); the docs site (a webjs ci entry under CLI Options on the configuration page with the step shapes, the flags, the Actions surfaces, and the signoff merge gate, a webjs ci command section on the testing page, a deployment-checklist item, and the known-key count); the root and CLI READMEs; and framework-dev.md for the monorepo's own list.
|
Decisions carried from the plan, and two that moved during the build Settled in #1470 and kept: JSON in the Two calls moved while building.
The per-layer status checks the four-job workflow used to provide are replaced, not dropped. The runner detects Two environmental findings worth knowing when reading a local run. A shell exporting |
Closes #1471
webjs ciruns an app's declared step list locally with the Rails 8.1bin/ciexperience: a timed result line per step, a failure list and one total line,--fail-fast,--only <title>,--jsonfor an agent loop, and an opt-in--signoffthat posts a green commit status throughgh signoff. The list lives inpackage.jsonunderwebjs.ci(a string, a{ title, run, env }command, or a{ title, steps, parallel }group whose steps run N at a time with each step's output replayed whole). Every scaffolded app ships a default list plus aciscript, the scaffold's GitHub workflow collapses to one job that runs the same list throughnpm run ciso the two cannot drift,webjs create --skip-ciomits that workflow, and the monorepo root and its three in-repo apps declare their own lists.Research record: #1470.
What changed
webjs.ciin the schema (draft-07definitionsfor the recursive step shape, a nested group cannot declareparallel), theWebjsConfigtype,KNOWN_KEYSplus a nested guard, and the type fixture. The pure readerpackages/cli/lib/ci-config.jsnormalizes the shorthand and reports every malformed entry by JSON path, because the boot validator never follows a$refand a silently dropped step is a check that never ran.packages/cli/lib/ci-runner.js. Sequential steps inherit stdio and are not detached (Ctrl-C reaches them natively). Captured steps are detached with stdin ignored, buffered in arrival order, resolved onclosewith a bounded grace afterexit.FORCE_COLORreaches captured children only when the parent stdout is a TTY. Under GitHub Actions each step is a::group::, a failure an::error::annotation, and a step table lands in$GITHUB_STEP_SUMMARY, so one job still names the layer that broke. Pure ofprocess.exit,console, and the clock.webjs ci. The predicate is the config, not anapp/directory (unlikewebjs check), so a workspace root is a legitimate target. Nothing declared is exit 1 naming the members that declare one; a malformed block is exit 1 with every problem;--jsonputs one document on stdout and the human report on stderr; the exit code goes throughexitCodeso a non-TTY stdout is never truncated.Checksgroup two at a time: check, doctor, typecheck,npm audit --audit-level=highorbun auditon Bun, and a sequentialTestssub-group of server, browser, e2e), aciscript, a one-job workflow with a read-only token, a timeout, and the concurrency cancel,--skip-ci, and the agent rule files, playbooks, PR template, and pre-commit comment now namenpm run cias the pre-push gate. The pre-commit hook itself is unchanged (Lighten pre-commit to convention-check only; move test gate to CI #174).npm run ci(the jobs themselves stay the required checks),gallery/examples/blog/websiteeach declare a shorter one, andtest/repo-health/in-repo-ci-blocks.test.mjspins all four.Deliberately excluded
webjs.ci,gh signoff failposting, MCP exposure.npm run ci(they are the required merge checks).node --testunder a pipe reports as TAP, documented.Test plan
packages/cli/test/ci-config(7),packages/cli/test/ci-runner(12, scripted fake child, fake clock, fake timers, pool cap, replay atomicity, exit-then-data, grace, interrupt, Actions mode),packages/server/test/config(schema lockstep + nested guard),test/types/webjs-config.test-d.tswith three@ts-expect-errorcounterfactuals (type-checked against the branch copy of core via apathsmap; the in-tree run resolves the primary checkout through the linked node_modules and is the known worktree artifact)test/cli/ci.test.mjs(9: refusals, green, red, fail-fast,--json,--only, Actions groups + step summary, help)test/bun/ci-runner.mjson Node and on Bun 1.3.14 (real children, captured stderr, a detachedsleepreaped on interrupt); the Bun matrix filtered toci-runnerandwebjs-configreports no genuine failureshelp,check-target, published-package descriptions (96 pass)test/scaffolds/*(61 + the--skip-cicounterfactual for the library option and the CLI flag),runtime-rewrite(npm run cibecomesbun run ci,npm cistill becomesbun install); a full-stack app generated WITH install runsnpm run ciend to end green (Setup, Conventions, Health, Types, audit, server, browser, e2e) once Playwright Chromium is present, which is the workflow's own install stepnpm run ci -- --only Conventionsgreen (8 steps, three at a time, 2.8s);gallery,examples/blog,websiteeach green on their own list through the branch CLI (the website run after the docs edits, which covers the docs-links and llms guards)npm test, full Node suite. Eight failures, all accounted for: the five that always fail in a linked worktree (listener, listener-overhead, three elision assertions), the config type fixture (bare specifier resolving into the primary, verified separately as above), and two#1451no-any guards that parse tsc's plain output while this shell exportsFORCE_COLOR=3; that file passes 4 of 4 with the variable unsetTwo things a reviewer will notice in a generated app before the next release: the published
@webjsdev/serverschema does not know thecikey yet, so a scaffolded app booted against the published packages prints one boot warning (unknown key "ci") that disappears on release, andnpm run ciin that app must be run through this branch's CLI until the CLI ships.Definition of done
AGENTS.mdcreate --skip-ci, thewebjsblock bullet, code-workflow items 1 and 4).agents/skills/webjs/references/built-ins.mdLocal CI section,references/testing.mdone-command section,SKILL.mdTesting Defaults)packages/cli/AGENTS.md,packages/server/AGENTS.mdwebsite/app/docs/configurationCLI Options entry + key count,testingsection,deploymentchecklist)README.md,packages/cli/README.mdframework-dev.mdworkflow.md, both playbooks, PR template) pluscreate.jsand the CLI helpwebsite/feat:titleexamples/blog/CONVENTIONS.md