Skip to content

feat: run every CI check locally from the monorepo root, with an unrun switch to make it the gate #1474

Description

@vivek7405

Problem

After #1471 the monorepo declares its own local CI (npm run ci, the root webjs.ci list) and a full run on a 24-core machine takes 4m35s with three parallel slots, beating every CPU-bound GitHub Actions job two to three times over. But the merge gate is still the GitHub Actions workflow: main's branch protection requires six ci.yml job names, the local list is a mirror by shape rather than by content (it lacks the Bun proof scripts, the browser-harness e2e, the Bun-served blog e2e, the Postgres round-trip, and the Docker build), and nothing runs the list before a merge. Rails 8.1's posture, which #1470 chose for the scaffold, is that the developer machine is the first CI runner and a green local run is what unblocks the merge (gh signoff). The monorepo should follow the same posture it ships.

Design / approach

Make the local run the merge gate, complete the root list so it is the whole gate, and demote the Actions workflow rather than delete it during the transition.

  • Complete the root list (package.json webjs.ci.steps) so it matches .github/workflows/ci.yml by content, not only by shape. Missing today: the 27 individual Bun proof scripts the Bun job runs before its matrix (bun test/bun/proof-exit-code.mjs through bun test/bun/vendor-scan.mjs, ci.yml L124-330), the browser-harness e2e (WEBJS_E2E=1 node --test test/e2e/browser-harness.test.mjs, the e2e job's second run step), the blog served on Bun e2e (the e2e-bun job, L518-545), the Postgres round-trip (node --test test/pg/pg-roundtrip.test.mjs against a postgres:16, the db-postgres job L333-370), and the Docker image build (the docker job, L633+). The CHROMIUM_PATH resolve the e2e jobs do must be inlined into the e2e step's run, since a step cannot export env to a sibling. Postgres has no local service on the dev machine (pg_isready reports none) but Docker is present, so the step runs a throwaway container around the test (docker run -d --rm -e POSTGRES_PASSWORD=... -p 5433:5432 postgres:16, wait for ready, run, stop). Keep the Gate group at three slots: six overloaded the box (web-test-runner's Firefox timed out launching a test page at 30s) and three costs no wall-clock because the blog e2e (4m30s) is the floor.
  • Merge gate. gh extension install basecamp/gh-signoff, then gh signoff install once (adds a branch-protection rule requiring the signoff commit status). npm run ci -- --signoff posts that status only after a green local run, on the pushed head only. Update scripts/protect-main.sh so the required contexts become signoff (plus the Actions checks during the transition, dropped in the last step).
  • Transition order, so main never loses its gate: (1) land the completed list; (2) require signoff while the six Actions checks stay required, and sign off two or three PRs to prove the loop; (3) drop the six Actions contexts from protect-main.sh and re-apply; (4) either delete .github/workflows/ci.yml (Omarchy posture) or keep it as a non-required second opinion (Rails posture). Recommend keeping it non-required for one release cycle, then deleting. The other three workflows (release.yml, purge-cdn.yml, vendor-cdn.yml) are not CI and stay.
  • Docs and rules follow the gate: the start-work skill's merge section, the AGENTS.md gh pr checks paragraph, framework-dev.md's local-CI section, and the check-target test that reads the workflow.

Implementation notes (for the implementing agent)

Where to edit

  • package.json (root) webjs.ci.steps: add a Bun proofs nested group under Gate with the 27 scripts in ci.yml order, add the browser-harness step and the Bun-served blog e2e step, a Postgres round-trip step (Docker container), and a Docker image build step (docker build -t webjs-monorepo .). Every root step must invoke the CLI by path (node packages/cli/bin/webjs.js), never a bare webjs bin; test/repo-health/in-repo-ci-blocks.test.mjs pins that and that every npm run <x> a step names exists.
  • scripts/protect-main.sh: the contexts array (L36-43). Step (2) adds "signoff"; step (3) removes the six job names. Job names are keyed exactly on the name: of each ci.yml job, which is how the "In-repo app tests (website + blog)" name went stale in feat(gallery): promote scaffold gallery to root gallery/ app #1371 and blocked merges until feat: add webjs ci, a local CI runner the scaffold workflow reuses #1472 corrected it.
  • .claude/skills/webjs-start-work/SKILL.md L299-305 ("Merge is gated on green CI"): the gate becomes the signoff status; "read gh pr checks once at merge" becomes gh signoff status (or gh api repos/webjsdev/webjs/commits/<sha>/status); the --admin warning stays.
  • AGENTS.md L89 (the gh pr checks porcelain carve-out) and the Code-workflow item 4 paragraph on npm run ci; framework-dev.md "Local CI" section (the sentence that says the workflow stays the required gate and is not converted).
  • test/cli/check-target.test.mjs L191-200: it parses the webjs check loop out of ci.yml to cross-check the derived workspace app list. Re-point it at the root webjs.ci list (the webjs check (<app>) steps) so it survives the workflow's removal.
  • .claude/hooks/cleanup-merged-worktree.sh and release-global-update.sh match the literal gh pr merge; the merge command itself does not change, so they are untouched.
  • Local-machine prerequisites to document in framework-dev.md: Playwright browsers (npx playwright install chromium firefox webkit), puppeteer-core (a root devDependency already), Docker for the Postgres and image steps, Bun, and gh with the signoff extension.

Landmines

  • The bare-specifier trap in a linked worktree. node_modules/.bin/webjs and every @webjsdev/* import resolve into the PRIMARY checkout, so a root run inside a worktree tests the primary's framework for those paths and always reds the listener test and the config type fixture (see reference_linked_worktree_five_test_failures). A signoff must be run from the primary checkout of the branch, or from a worktree whose node_modules symlinks were deleted and installed for real, or the green is partly vacuous. Decide and document which; the simplest rule is "sign off from a real install".
  • Six slots overload the machine. Measured on feat: add webjs ci, a local CI runner the scaffold workflow reuses #1472: Firefox under web-test-runner failed to launch a page within 30s at six slots; three slots were clean at the same wall-clock.
  • FORCE_COLOR in the invoking shell flips tsc to pretty output and reds the two #1451 no-any guards, and makes tools print a NO_COLOR ignored warning. The runner sets FORCE_COLOR only for captured children when it colours itself; the developer's shell must not export it globally.
  • Postgres needs a real engine; the pg driver is installed ad hoc in the job (npm install --no-save pg@^8.13.0), which a linked worktree must NEVER do (fix: npm install in a linked worktree corrupts the shared node_modules #1442). Add pg as a root devDependency instead so the step needs no install.
  • The Docker build is slow and cache-dependent (56s in Actions with a warm cache); consider making it its own --only-able top-level step rather than part of Gate, so a routine run can skip it while the signoff run includes it.
  • Signoff refuses an unpushed head, and a later push has no signoff until re-run; that is the feature, not a bug, so the workflow rule must say "push, then sign off", not the reverse.
  • gh signoff install and protect-main.sh both write branch protection; make the script the single source (add the signoff context there) so the two cannot disagree.

Invariants

  • main is never without a required check during the transition (step order above).
  • No AI attribution in commits; conventional-prefixed PR title; the PR body carries the definition-of-done table.
  • Invariant 11 prose rules in every doc touched; webjs lowercase only as a command or key.

Tests and doc surfaces

  • test/repo-health/in-repo-ci-blocks.test.mjs (already guards the block; extend for the new steps if a shape rule is added), test/cli/check-target.test.mjs re-pointed, a full local run recorded in the PR body with its timing, and two or three signed-off merges before step (3).
  • Docs: framework-dev.md, AGENTS.md, .claude/skills/webjs-start-work/SKILL.md, scripts/protect-main.sh header comment.

Acceptance criteria

  • The root webjs.ci list runs every check .github/workflows/ci.yml runs today (the Bun proof scripts, both e2e suites on Node and the blog on Bun, the Postgres round-trip, the Docker build included), and a full run from a real install is green
  • npm run ci -- --signoff posts a green signoff status on the pushed head, and main requires it
  • Two or three PRs merge on signoff while the Actions checks are still required, then the Actions contexts are removed from scripts/protect-main.sh and the applied protection
  • test/cli/check-target.test.mjs reads the app list from the root webjs.ci block, not from the workflow
  • The start-work skill, AGENTS.md, and framework-dev.md describe the signoff gate and the local prerequisites; the workflow is either deleted or documented as non-required
  • A counterfactual proves the gate: a PR whose head has no signoff cannot be merged

Research: #1470. Runner and the first root list: #1471 / PR #1472.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

  • Status
    Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions