diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml new file mode 100644 index 0000000..838d3d0 --- /dev/null +++ b/.github/actions/setup/action.yml @@ -0,0 +1,54 @@ +name: Adapter setup +description: > + Toolchain setup + dependency install, driven entirely by .claude/gates.json so the + same workflow works on any downstream repo without hand-edited YAML. Reads + project.packageManager / project.language / gates.coverage_threshold, sets up the + matching toolchain, runs the `install` gate, and exports COVERAGE_THRESHOLD. + +runs: + using: composite + steps: + # Read the adapter. `node` is preinstalled on GitHub runners, so this works even + # before any toolchain setup — same dependency gate.sh already relies on. + - id: adapter + shell: bash + run: | + gates=".claude/gates.json" + read_json() { node -e "try{const g=require('./$gates');process.stdout.write(String($1||''))}catch(e){process.stdout.write('')}"; } + pm="$(read_json "g.project&&g.project.packageManager")" + lang="$(read_json "g.project&&g.project.language")" + thr="$(read_json "g.gates&&g.gates.coverage_threshold")" + echo "package-manager=$pm" >> "$GITHUB_OUTPUT" + echo "language=$lang" >> "$GITHUB_OUTPUT" + # Export the threshold so coverage gate commands can read $COVERAGE_THRESHOLD. + [ -n "$thr" ] && echo "COVERAGE_THRESHOLD=$thr" >> "$GITHUB_ENV" + echo "adapter: packageManager='$pm' language='$lang' coverage_threshold='$thr'" + + # --- JavaScript/TypeScript toolchain (the common case) ------------------------- + - if: contains(fromJSON('["pnpm","npm","yarn"]'), steps.adapter.outputs.package-manager) + uses: actions/setup-node@v4 + with: + node-version: 20 + + - if: steps.adapter.outputs.package-manager == 'pnpm' + shell: bash + run: corepack enable + + # --- Extension point: add stack-specific toolchain setup here ------------------ + # The template ships the JS path above. Downstream repos on other stacks add their + # setup here (it's a config edit, like gates.json). Examples: + # + # - if: contains(steps.adapter.outputs.language, 'olidity') # Solidity / Foundry + # uses: foundry-rs/foundry-toolchain@v1 + # + # - if: contains(steps.adapter.outputs.language, 'ython') # Python + # uses: actions/setup-python@v5 + # with: { python-version: '3.12' } + # + # See issue #9 (per-worktree lifecycle) — the same bootstrap that lets isolated + # worktree workers run a gate is what CI needs here. + + # Install dependencies via the adapter's `install` gate (skips cleanly if empty). + - name: Install dependencies + shell: bash + run: bash .claude/scripts/gate.sh install diff --git a/.github/workflows/gates.yml b/.github/workflows/gates.yml new file mode 100644 index 0000000..a66d73c --- /dev/null +++ b/.github/workflows/gates.yml @@ -0,0 +1,44 @@ +# Server-side gate enforcement. Mirrors the local gates (settings.json hooks + +# gate.sh) at the GitHub PR level, so a merge is gated by CI — not only by the +# orchestrator remembering to run the gates before it opens a PR. +# +# Adapter-driven: each job just runs `gate.sh `, which reads the command from +# .claude/gates.json (and skips cleanly when a gate is left ""). You should not need +# to edit this file per project — configure gates.json instead. Toolchain setup lives +# in .github/actions/setup (also adapter-driven). See docs/PROMPTS.md to (re)generate. +# +# Enforcement teeth: turn these checks into REQUIRED status checks via branch +# protection on the base branch — see docs/GETTING_STARTED.md. +name: gates + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +# A newer push to the same PR cancels the in-flight run. +concurrency: + group: gates-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + gates: + name: ${{ matrix.gate }} + runs-on: ubuntu-latest + strategy: + fail-fast: false # one red gate shouldn't hide the others + matrix: + # One job per gate → each surfaces as its own PR check (what visual + # front-ends like emdash monitor). `install` is not here — it runs once in + # the setup action as a prerequisite for every gate. + gate: [build, lint, typecheck, test, coverage, security] + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + - name: gate.sh ${{ matrix.gate }} + # An unconfigured gate ("") exits 0 (skip), so the check is green-but-trivial + # rather than red — same semantics as the local hooks. The coverage gate + # command can read $COVERAGE_THRESHOLD (exported by the setup action). + run: bash .claude/scripts/gate.sh ${{ matrix.gate }} diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md index 6ebc982..5460df6 100644 --- a/docs/GETTING_STARTED.md +++ b/docs/GETTING_STARTED.md @@ -73,9 +73,35 @@ Scope it, show me the plan, and wait for my approval before writing code. Approve the plan, let one implementer run in its worktree, watch the reviewers gate it, review the PR. Then read [`USAGE.md`](USAGE.md) to scale up, and [`TOKEN_BUDGET.md`](TOKEN_BUDGET.md) before you go parallel. +## Step 7 — Enforce gates in CI (server-side) +The hooks and `gate.sh` enforce gates *locally*, and the orchestrator runs them before opening a PR — but +nothing stops a human (or a bot) merging a PR whose gates never ran. The template ships +[`.github/workflows/gates.yml`](../.github/workflows/gates.yml) + a `.github/actions/setup` composite action +that run **the same `gate.sh` gates** on every `pull_request`, reading commands from `gates.json`. It's +adapter-driven — you configure `gates.json`, not the YAML. + +1. **It works out of the box for JS/TS.** For other stacks, add the toolchain at the *Extension point* comment + in `.github/actions/setup/action.yml` (e.g. `foundry-rs/foundry-toolchain` for Solidity, + `actions/setup-python`), keyed off `project.language`. Empty gates skip, so unconfigured checks stay green. +2. **Make the checks required** — this is the actual enforcement. On the base branch (`merge.baseBranch`): + **Settings → Branches → Add branch protection rule** → *Require status checks to pass before merging*, then + select the gate checks (`build`, `lint`, `typecheck`, `test`, `coverage`, `security`). Or via CLI: + ```bash + gh api -X PUT repos///branches//protection \ + -f 'required_status_checks[strict]=true' \ + -f 'required_status_checks[checks][][context]=build' \ + -f 'required_status_checks[checks][][context]=lint' \ + -f 'required_status_checks[checks][][context]=typecheck' \ + -f 'required_status_checks[checks][][context]=test' \ + -f 'required_status_checks[checks][][context]=coverage' \ + -f 'enforce_admins=true' -F 'required_pull_request_reviews=null' -F 'restrictions=null' + ``` + Without this step the workflow only *reports* pass/fail; required checks are what block the merge button. + ## Verification checklist - [ ] `CLAUDE.md` describes the project and lists modules. - [ ] `.claude/gates.json` has real commands; `gate.sh build|lint|test` behave correctly. - [ ] `/agents` lists orchestrator, implementer, reviewer, test-runner. - [ ] A pilot task produced a branch/PR that passed gates + review. +- [ ] CI gates run on PRs and are set as **required** status checks on the base branch (Step 7). - [ ] You've checked spend with `/cost` or `npx ccusage`. diff --git a/docs/PROMPTS.md b/docs/PROMPTS.md index 3f9c7a3..5c90674 100644 --- a/docs/PROMPTS.md +++ b/docs/PROMPTS.md @@ -71,3 +71,15 @@ Run `npx ccusage` and summarize token/cost by model for this session. Given the changes to: max_parallel_workers, model routing, the module map, and any agent prompt that caused rework or overlap. Propose concrete edits to gates.json. ``` + +## 10. Wire up CI gate enforcement (server-side) +``` +The repo ships .github/workflows/gates.yml + .github/actions/setup — an adapter-driven CI that runs each +gate.sh gate on pull_request, reading commands from gates.json. Don't hand-edit the workflow. Instead: +1. Confirm gates.json has working gate commands (run `bash .claude/scripts/gate.sh ` for each). +2. If this repo isn't pure JS/TS, add the toolchain to .github/actions/setup at the "Extension point" comment + (e.g. foundry-toolchain for Solidity, setup-python for Python), keyed off project.language. Keep it a + config edit — don't fork the workflow. +3. Make the matrix checks REQUIRED via branch protection (see GETTING_STARTED "Step 7 — Enforce gates in CI"), + so a red gate actually blocks merge. Show me the branch-protection settings to apply. +```