feat: run every CI check locally from the monorepo root - #1475
Conversation
|
Decisions: how the list reaches parity, and what the gate refuses Three calls worth recording. The The Gate stays at three slots and the Docker build sits outside it. Six slots overloaded a 24-core machine (Firefox could not launch a web-test-runner page inside its 30s start timeout), three were clean at the same wall-clock, and the blog e2e is the floor either way. The image build is its own top-level step so a routine The signoff refuses what it should. Two machine prerequisites the docs now name: the Playwright browsers, and a user in the |
vivek7405
left a comment
There was a problem hiding this comment.
The gate's shape is right (branch protection plus a ruleset, --signoff only after a green list, push-then-sign-off), and the atomic dist build is a real fix for a real race. One thing blocks the plan as written: gh signoff install --repo is rejected by the extension, so scripts/protect-main.sh never reaches the ruleset and the transition step would silently leave main on the Actions checks alone (or, with --local-only, on nothing). Inline comment has the one-token fix.
The rest are local-run hazards the CI-shaped steps did not have on a fresh runner: the Postgres readiness probe passing on the init-phase socket server, the website e2e trusting whatever already answers on 5001, the interrupt test's pgrep matching the other slot's copy of itself, and a failed dist build leaving an un-ignored staging dir. Plus one doc sentence in framework-dev.md that describes the test/pg/node_modules link the PR body says did not work.
Two notes with no line to hang them on. scripts/ci-e2e-website.sh and the interrupt test need setsid and pgrep, which macOS does not ship by default; worth a word in the prerequisites list. And scripts/ci-postgres.sh runs npm install into the shared root node_modules from inside a parallel Gate slot while the other slots import from that tree; the diff is additive so I could not construct a failure, but a Setup step would take the question off the table.
The root webjs.ci list mirrored the Actions jobs by shape, not by content (#1474). It now runs the same checks step for step: the Node e2e job's eight runs (the blog in a real browser, the browser-test harness, dev overlay nav, seed observability, morph, the form submission run that needs a live website dev server, the two ui e2e), the blog e2e served on Bun, all 31 Bun proof scripts ahead of the matrix, the Postgres round-trip, and the Docker image build. Two steps needed scripts. scripts/ci-e2e-website.sh boots the website in its own process group with the log in a file and reaps the group on exit, the shape the CI step arrived at after a background child once held the step open for ten minutes. scripts/ci-postgres.sh runs the round-trip against a throwaway postgres:16 container and installs the pg driver into a scratch prefix linked under test/pg/node_modules, which keeps the shared lock without it (the CI job's own posture) and never installs through a linked worktree's node_modules (#1442). The test imports pg as an ES module, so NODE_PATH would not have done. The Gate stays at three slots, longest first. The blog e2e picks a random free port, so the Node and Bun runs share the Gate. The Docker build is its own top-level step so a routine `--only Gate` skips it while a signoff run includes it.
scripts/protect-main.sh now declares the whole merge gate (#1474): the CODEOWNER review, the `signoff` commit status that `npm run ci -- --signoff` posts after a green local run (installed as a repository ruleset through gh signoff install, idempotent), and, during the transition, the six Actions job names. `--local-only` is the final state with no Actions context required, so the workflow can be demoted to a second opinion or deleted once a few PRs have merged on signoff. test/cli/check-target.test.mjs used to cross-check the workspace app list against the workflow's `webjs check` loop; it now reads the `webjs check (<app>)` steps of the root webjs.ci list, so it survives the workflow's removal and pins that every in-repo app is checked locally.
…econd opinion framework-dev.md's Local CI section now describes the complete root list, the signoff gate and the push-then-sign-off rule, the transition to --local-only, and the local prerequisites (Bun, Docker, the Playwright browsers, gh with the signoff extension, no FORCE_COLOR in the shell). AGENTS.md's code-workflow item and its gh porcelain carve-out, and the start-work skill's merge section, say the gate is a signed-off local run read with gh signoff status, with gh pr checks kept for the Actions half while it is still required. The workflow's header states its new role: the cloud second opinion, informational once --local-only is applied, its job names kept in sync with the gate script.
…the daemon The first local Postgres step linked a scratch-installed pg under test/pg/node_modules, which satisfied the test's own import but not drizzle-orm/node-postgres, whose session.js resolves pg from inside the repo's node_modules (measured: ERR_MODULE_NOT_FOUND from that file). The step now takes the CI job's exact posture, `npm install --no-save` into the repo's node_modules, and refuses when node_modules is a symlink so it can never write through a linked worktree into the primary (#1442). That is where the "sign off from a real install" rule bites, and the message says so. Both Docker steps read WEBJS_DOCKER for the command that reaches a daemon (`sudo -n docker`, `podman`); the default `docker` needs the user in the docker group, which framework-dev.md now names. The JSON report note says `npm run -s ci -- --json`, since npm's own run banner otherwise precedes the document.
…terrupt test Running the whole gate in one tree surfaced a race the per-job CI never could: scripts/build-framework-dist.js removed packages/core/dist before writing it, and two Bun proofs rebuild that directory while other slots of the same run import @webjsdev/core/dist/webjs-core.js, so four unrelated unit tests red with ERR_MODULE_NOT_FOUND (#1474). The build now goes into a staging sibling and each bundle is renamed into place, then stale files are pruned, so dist never disappears and a concurrent reader sees the old bundle or the new one. The Ctrl-C test in test/cli/ci.test.mjs sent its first signal after a fixed 700ms, which on a loaded machine arrived before the bin had spawned its child, leaving nothing to interrupt. It now waits for the child process itself (pgrep on a unique sleep length) before signalling.
gh signoff install accepts only --branch; any other dashed option is an unknown-option failure. Under set -e the script stopped there, after the branch-protection PUT had landed and before the signoff ruleset existed, so a transition run would have kept main on the Actions checks alone and a --local-only run would have left it with no status requirement at all. The extension resolves the repository through gh api repos/:owner/:repo, which GH_REPO overrides. Claude-Session: https://claude.ai/code/session_0125BCU48L9AmmM1JbBKeyQe
…ever sees The Postgres readiness probe now goes over TCP inside the container. The image's entrypoint runs a temporary init-phase server on the unix socket only, so a socket probe read ready before the real server listened on the port the test connects to, and every run is a fresh container so the window is there warm or not. The website e2e step refuses when something already answers on :5001 (the step would otherwise test that server while its own died with EADDRINUSE) and pins PORT=5001, since the website dev script honours an exported PORT and the probe would wait out its two minutes. framework-dev.md described the pg driver as linked under test/pg/node_modules, the approach that was tried and did not satisfy drizzle's resolution; it now describes the --no-save install the script does, and the prerequisites name setsid and pgrep. Claude-Session: https://claude.ai/code/session_0125BCU48L9AmmM1JbBKeyQe
A build that threw left packages/core/dist.building-<pid> behind, and the .gitignore pattern is the bare dist, which matches only a path component named exactly that, so the leftover showed up untracked and would ride a git add -A. The build-and-move now runs inside try/finally. Claude-Session: https://claude.ai/code/session_0125BCU48L9AmmM1JbBKeyQe
This file runs twice in one local CI run (npm test in one Gate slot, the Bun matrix in another), and pgrep -f on a fixed length matched the other run's child, firing SIGINT before this run had spawned anything, the same failure the wait was added to fix. Claude-Session: https://claude.ai/code/session_0125BCU48L9AmmM1JbBKeyQe
The root browser suite and the website and gallery suites each start a web-test-runner on its default port 8000. With the root suite in one Gate slot and the in-repo app suites in another, the two overlapped on a signoff run and the later one fetched its test modules from the wrong server (every test failed with "Failed to fetch dynamically imported module ... localhost:8000"). Two earlier runs missed it only by timing. The root browser suite and the in-repo app group now share one sequential slot, which costs no wall-clock because the e2e group is the floor, and framework-dev.md names the rule.
The workflow header, framework-dev.md, AGENTS.md and the start-work skill said local CI is the merge gate and a PR must not merge without a signed off head. The decision is to merge the runner and the complete list without running scripts/protect-main.sh, so main keeps requiring the six Actions checks and nothing requires the signoff status. An agent reading the old wording would look for a gate that does not exist. The docs now say what main requires today, what the signoff status is, and that the script is the switch that has not been run.
55aeaec to
f9f2ce4
Compare
Closes #1474
The monorepo's local CI becomes complete, the Rails 8.1 posture the scaffold already ships: the root
webjs.cilist now runs everything the GitHub workflow runs, step for step (both e2e suites, the Bun proofs and matrix, the Postgres round-trip against a throwaway container, the Docker image build),npm run ci -- --signoffposts a greensignoffstatus on the pushed head after a green run, andscripts/protect-main.shbecomes the switch that would make that status required (the CODEOWNER review, the signoff ruleset, and the six Actions checks during a transition, with--local-onlyas the final state). The switch is not run by this PR and is not planned for now:mainkeeps requiring the six Actions checks, the workflow's jobs are byte-identical tomain, and contributing keeps running GitHub CI exactly as today. The docs describe that state.Research: #1470. Runner and the first root list: #1471 (PR #1472), which this branch stacks on until it merges.
What changed
package.jsonwebjs.ci): Setup, then oneGategroup three slots wide holding the Node e2e group (the blog in a real browser, the browser-test harness, dev overlay nav, seed observability, morph, the form-submission run against a live website dev server, the two ui e2e), the Bun group (all 31 proof scripts in workflow order, then the matrix), the blog e2e served on Bun, the browser suite, the root test suite, the in-repo app typechecks and suites with the website boot-check, the Conventions group, and the Postgres round-trip; then the Docker image build as its own top-level step so--only Gateskips it on a routine run. 63 command steps.scripts/ci-e2e-website.shboots the website in its own process group with the log in a file and reaps the group on exit, the shape the CI step arrived at after a background child once held it open for ten minutes.scripts/ci-postgres.shruns the round-trip against a throwawaypostgres:16container, installs thepgdriver--no-saveinto the repo'snode_modulesexactly as the CI job does (the shared lock stays without it), and refuses whennode_modulesis a symlink so it can never write through a linked worktree into the primary (fix: npm install in a linked worktree corrupts the shared node_modules #1442). A link undertest/pg/was tried first and satisfied the test's own import but notdrizzle-orm/node-postgres, which resolvespgfrom insidenode_modules.WEBJS_DOCKERnames the command that reaches a Docker daemon for both Docker steps (sudo -n docker,podman); the default needs the user in thedockergroup.scripts/protect-main.sh: the review,gh signoff install(a repository ruleset requiring thesignoffstatus, idempotent, the extension installed if missing), and the six Actions job names during the transition;--local-onlydrops the Actions contexts.test/cli/check-target.test.mjscross-checks the workspace app list against thewebjs check (<app>)steps of the root list instead of the workflow's loop, so it survives the workflow's removal.scripts/build-framework-dist.jsremovedpackages/core/distbefore rewriting it, so two Bun proofs rebuilding it in one slot made four unrelated unit tests in another slot fail withERR_MODULE_NOT_FOUND(it now stages and renames each bundle into place, so the directory never disappears); and the Ctrl-C test intest/cli/ci.test.mjssignalled after a fixed delay that a loaded machine outran (it now waits for the child process itself). CI never saw either because each job has its own tree.framework-dev.md(the complete list, the gate, push-then-sign-off, the transition, the local prerequisites,npm run -s ci -- --jsonfor one JSON document), AGENTS.md (the code-workflow item and thegh pr checkscarve-out), the start-work skill's merge section (read the gate withgh signoff status), and the workflow header (its role as the cloud second opinion, job names kept in sync with the gate script).The switch (not applied by this PR, and deferred)
bash scripts/protect-main.shrequiressignoffwhile the six Actions checks stay required.bash scripts/protect-main.sh --local-onlydrops the Actions contexts; the workflow becomes a non-required second opinion, then goes.Neither script invocation runs on this PR. The decision (2026-09-11) is to merge without flipping: GitHub Actions stays the required gate and
npm run ciis the pre-push loop, with--signoffavailable.Measured
Complete list, 63 command steps, on a 24-core machine from a real install: 7m06s, every step green, signed off on 0bbe37e (
gh signoff statusreads ✓); the run before the review round was 6m57s on 69b79f2. The wall-clock is the Node e2e group; the Bun group and the Bun-served blog e2e run beside it in the other two slots. Six slots overloaded the box on #1472 (Firefox timed out launching a page), so three is what ships.Test plan
--signoffposted thesignoffstatus on the pushed head 0bbe37e (after the review round; 69b79f2 before it)scripts/ci-postgres.shgreen throughWEBJS_DOCKER='sudo -n docker'(2.7s with the image warm); the Docker image build green the same way (webjs-monorepo:ci, 523 MB)gh signoffitself; a green partial run from a clean pushed tree postssignoff(retracted afterwards withgh signoff fail, since a partial run must not stand)test/cli/check-target.test.mjsre-pointed and green,test/repo-health/in-repo-ci-blocks.test.mjsgreen,test/packaging/build-dist.test.jsgreen against the atomic build,test/cli/ci.test.mjsgreen with the child waitReview round
The review on this PR posted six findings; all six are fixed in f9a6691a, 8469706e, e391e9ee, and 4dc319b2 (
GH_REPOfor the signoff install, the TCP readiness probe, the website port pinned and a squatter refused, per-pid sleep lengths in the interrupt test, the staging dir dropped on a failing build, the Postgres prose corrected), answered in their threads, and the threads resolved. Re-running the gate on that head then found one more one-tree collision, fixed in 0bbe37e8: every web-test-runner suite (the root browser suite, the website's, the gallery's) binds port 8000, so they now share one sequential Gate slot.Definition of done
AGENTS.mdgh pr checkscarve-out; Actions named as the gate that stands, the signoff as the switch)framework-dev.mdmainrequires today, the switch and how to reverse it).claude/skills/webjs-start-work/SKILL.mdnpm run cibefore pushing, the switch is the maintainer's).github/workflows/ci.ymlmainscripts/protect-main.sh,scripts/build-framework-dist.js--local-only; the atomic build).agents/skills/webjs/, docs site, README, scaffoldfeat:title kept for the squash subject)