From 95b25670a21fb66ffdd83448805d7574ff74e7ba Mon Sep 17 00:00:00 2001 From: Antawari Date: Fri, 26 Jun 2026 13:59:35 -0600 Subject: [PATCH 1/2] feat(gate): grade the merged-main projection + self-verify the required mount MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contract: the battery must block at PR time, evaluated against what the base branch BECOMES on merge, with the gate actually required. World: a tower shipped PRs that passed isolated CI yet reddened main on merge. Failure: (1) the gate ran on each PR's stale checked-out ref, not the merged-main projection — a branch forked before a change landed on main passed in isolation; (2) the required-check mount was procedural (DESIGN §4.8 PARTIAL), so even a correctly-failing gate could not block a merge. Merged-main projection: on pull_request the gate now checks out the PR head (a SHA) with full history and merges the CURRENT base branch into it, grading the result; a tree that cannot cleanly merge its base is refused, never silently passed. In-band self-verifying mount: a new step reuses check-required-mount.sh against the calling repo and REFUSES to pass unless the gate is a required, non-bypassable check — the Elegance Law's e2e on the gate itself, converting the mount from procedure into mechanism. Needs administration:read (declared in the workflow + granted in the caller stub). Adds templates/mount-required.sh (one-shot full-PUT apply that proves itself via the audit) and the runbook section. Scope: Python reusable-workflow path; TS parity is a fast-follow. Disjoint from the cf-no-bon-ref gauge PR (merges in any order). All workflow invariants preserved (33 tests: zero inputs, one aggregating battery step, no continue-on-error/paths, SHA-pinned uses). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/quality-gate.yml | 63 ++++++++++++++++++++++++--- docs/enforcement-runbook.md | 28 +++++++++++- templates/mount-required.sh | 42 ++++++++++++++++++ templates/quality.caller.yml | 9 +++- tests/test_workflows.py | 68 ++++++++++++++++++++++++++++++ 5 files changed, 201 insertions(+), 9 deletions(-) create mode 100644 templates/mount-required.sh diff --git a/.github/workflows/quality-gate.yml b/.github/workflows/quality-gate.yml index c33b923..3317e8d 100644 --- a/.github/workflows/quality-gate.yml +++ b/.github/workflows/quality-gate.yml @@ -16,6 +16,11 @@ on: permissions: contents: read + # administration: read — the self-verifying mount canary (below) reads the + # default branch's protection to confirm THIS gate is a required, non-bypassable + # check. This is a CEILING; the effective grant comes from the caller's token, so + # the caller stub must also grant administration:read (templates/quality.caller.yml). + administration: read jobs: gate: @@ -31,10 +36,11 @@ jobs: # FIRST, before any checkout: self-assert the trigger context. A caller # stub that filters triggers (workflow_dispatch-only, schedule, etc.) is # refused here. This closes the EVENT-TYPE variant of caller-stub - # neutering only (DESIGN §4.8, verdict PARTIAL): caller-side `paths:` - # filters, `continue-on-error` on the caller's job, and required-check - # mounting are invisible from inside a reusable workflow; those vectors - # remain procedural — mount runbook + mount canary + Constable sweep. + # neutering (DESIGN §4.8, verdict PARTIAL). The required-check MOUNT is now + # self-verified IN-BAND by the canary step below (mechanism, not just the + # out-of-band runbook + Constable sweep); caller-side `paths:` filters and + # `continue-on-error` on the caller's job stay invisible from inside a + # reusable workflow and so remain procedural — Constable sweep + review. - name: Self-assert trigger context (anti-neutering) working-directory: ${{ github.workspace }} env: @@ -51,11 +57,37 @@ jobs: esac - name: Checkout consumer repo - # In a reusable workflow the github context belongs to the CALLER, so a - # bare checkout lands the consumer repo at the triggering ref. + # In a reusable workflow the github context belongs to the CALLER. On a + # pull_request we land the PR HEAD (a commit SHA — never the default merge + # ref, which merges into a possibly-STALE base) and fetch full history so + # the next step can project the merged-main state; on push we land the + # pushed ref. fetch-depth:0 gives the merge step a real merge-base. uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 (node24) with: path: repo + fetch-depth: 0 + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + + - name: Project the merged-main state (pull_request only) + # The battery must grade what the base branch BECOMES after this PR merges, + # never a stale PR base. A branch that forked before a change landed on + # main (sweetcms: forked before the gate existed), or a stacked PR, passes + # in isolation yet reddens main on merge. We merge the CURRENT base branch + # into the PR head and grade the RESULT; a tree that cannot cleanly merge + # its base is not gradeable green (refused, not silently passed). + if: github.event_name == 'pull_request' + working-directory: repo + env: + BASE_REF: ${{ github.event.pull_request.base.ref }} + run: | + git config user.email "quality-gate@candyfactory.local" + git config user.name "quality-gate" + git fetch --no-tags origin "$BASE_REF" + if ! git merge --no-edit FETCH_HEAD; then + echo "::error::quality-gate refused: this PR does not cleanly merge the current '${BASE_REF}'. The battery grades the MERGED state of the base branch; a tree that cannot merge its base is not gradeable green — merge or rebase '${BASE_REF}' and push." + exit 1 + fi + echo "graded against PR head + current origin/${BASE_REF} (merge $(git rev-parse --short HEAD))" - name: Set up Python 3.12 uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 (node24) @@ -165,6 +197,25 @@ jobs: exit 1 fi + - name: Self-verify the gate is mounted as a required, non-bypassable check + # The Elegance Law's e2e on the gate ITSELF: a gate that is not a REQUIRED + # status check cannot block a red merge, so a gate that cannot CONFIRM it + # is required REFUSES to pass. This converts the required-check mount from + # procedure (DESIGN §4.8, verdict PARTIAL) into mechanism — the missing + # in-band leg of the out-of-band Constable canary. It reuses the audit + # script verbatim (DRY) against the calling repo's default branch; the + # context is `gate / gate`. Needs administration:read (see permissions + + # the caller stub); a token that cannot read protection is treated as + # an unmounted gate, not waved through. + env: + GH_TOKEN: ${{ github.token }} + run: | + script="$GITHUB_WORKSPACE/.cf-quality/templates/check-required-mount.sh" + if ! bash "$script" "$GITHUB_REPOSITORY"; then + echo "::error::quality-gate refused: this gate is not a required, non-bypassable status check on the default branch — OR the token lacks 'administration: read' to confirm it. A gate that cannot prove it is the law is treated as unmounted. Mount it (templates/mount-required.sh · docs/enforcement-runbook.md) and grant the caller administration:read." + exit 1 + fi + - name: cf-gate — run the whole battery, aggregate, report every red gate # THE gate: ONE COMPLETE step. cf-gate runs EVERY stage (ruff, # ruff-format, the cf-* gates, mypy through the baseline ratchet, diff --git a/docs/enforcement-runbook.md b/docs/enforcement-runbook.md index b826323..01d51e3 100644 --- a/docs/enforcement-runbook.md +++ b/docs/enforcement-runbook.md @@ -201,6 +201,30 @@ that fails the gate on a throwaway PR, confirm the run goes red AND the PR is then delete the branch. Settings can be correct while the name is subtly wrong — the behavioral canary is the only proof that a red verdict cannot land. +### The in-band self-verifying canary (mechanism, not just procedure) + +The audit above is out-of-band (Constable cadence). The gate now **also verifies +its own mount from inside every run**: `quality-gate.yml` runs +`check-required-mount.sh` against the calling repo and **refuses to pass** when +the gate is not a required, non-bypassable check — a gate that cannot prove it is +the law is treated as unmounted. This is the Elegance Law's e2e on the gate +itself; it closes the in-band leg of what DESIGN §4.8 listed as PARTIAL. + +It needs `administration: read`, so the caller stub grants it +(`permissions: { contents: read, administration: read }`). A token that cannot +read protection fails the canary with that remedy — never a silent pass. + +**The bootstrap order matters:** mount the protection FIRST (an admin action, +out-of-band), then the gate goes green. The one-shot apply is: + +```bash +bash templates/mount-required.sh OWNER/REPO # full PUT + proves it via the audit +``` + +`mount-required.sh` always PUTs the complete protection object (the §2 trap: +PATCH 404s on an unprotected branch), sets `enforce_admins: true` and +`strict: true`, and then re-runs `check-required-mount.sh` to prove the mount. + --- ## 4. Checklist @@ -209,7 +233,9 @@ the behavioral canary is the only proof that a red verdict cannot land. 2. GET current protection; save it (`§2a`). 3. Author the full PUT body reproducing every captured field + the required checks block + `enforce_admins: true` (`§2b`); `restrictions: null` for - non-org repos. + non-org repos. (Or run `templates/mount-required.sh OWNER/REPO`.) 4. PUT it; diff the echo against the saved copy (`§2c`). 5. Run the canary audit — expect PASS (`§3`). 6. Run the behavioral canary once — red verdict must be unmergeable (`§3`). +7. Grant the caller `administration: read` so the in-band canary self-verifies + the mount on every run (`§3`). diff --git a/templates/mount-required.sh b/templates/mount-required.sh new file mode 100644 index 0000000..5e1cf56 --- /dev/null +++ b/templates/mount-required.sh @@ -0,0 +1,42 @@ +#!/usr/bin/env bash +# Mount the quality gate as a REQUIRED, non-bypassable status check on a consumer +# repo's default branch — the apply-side companion to check-required-mount.sh +# (which audits) and to the in-band self-verifying canary in quality-gate.yml +# (which refuses to pass when this has not been done). +# +# The API trap (docs/enforcement-runbook.md): a PATCH 404s on an unprotected +# branch, so the FIRST mount must be a full PUT of the protection object. This +# script always PUTs the complete object, so it is idempotent — re-running it +# re-asserts the same required-check + enforce_admins state. +# +# Dependency-free: `gh api` only (needs a token with administration:write, e.g. +# an admin `gh auth login`). See check-required-mount.sh for the audit. +# +# Usage: mount-required.sh OWNER/REPO [branch] [context] +# branch defaults to: main +# context defaults to: gate / gate +# +# Exit: 0 mounted (and verified) · 1 mount/verify failed · 2 usage error +set -euo pipefail + +repo="${1:?usage: mount-required.sh OWNER/REPO [branch] [context]}" +branch="${2:-main}" +context="${3:-gate / gate}" + +# strict:true also forces the PR to be up to date with base before merge, so the +# required run is always against the current base — belt-and-suspenders with the +# workflow's own merged-main projection. +gh api -X PUT "repos/${repo}/branches/${branch}/protection" \ + --input - < None: # vector #1") versus DESIGN §4.8's honest PARTIAL. text = GATE_PATH.read_text(encoding="utf-8") assert "remain procedural" in text, "the self-assert comment must state the PARTIAL residue" + + +# --- merged-main projection + in-band required-check self-verification -------- + + +def _gate_step(name_substr: str) -> dict[str, Any]: + for step in _steps(_load(GATE_PATH), "gate"): + if name_substr in step.get("name", ""): + return step + raise AssertionError(f"no gate step named like {name_substr!r}") + + +def test_consumer_checkout_lands_pr_head_with_full_history() -> None: + # The battery must grade the PR HEAD (a SHA) merged with CURRENT base — not + # the default merge ref against a possibly-stale base. fetch-depth:0 gives + # the merge a real merge-base. + co = _gate_step("Checkout consumer repo") + assert str(co["with"]["fetch-depth"]) == "0" + assert "pull_request.head.sha" in co["with"]["ref"] + + +def test_gate_projects_merged_main_state_on_pull_request() -> None: + step = _gate_step("Project the merged-main state") + assert step["if"] == "github.event_name == 'pull_request'" + run = step["run"] + assert "git merge" in run and "FETCH_HEAD" in run + assert "exit 1" in run, "an unmergeable PR is refused, not silently passed" + assert "BASE_REF" in step["env"], "base ref is read from env, never interpolated into the shell" + + +def test_gate_self_verifies_required_mount_in_band() -> None: + # The Elegance e2e: the gate refuses to pass unless it can confirm it is a + # required, non-bypassable check — reusing the audit script (DRY). + canary = [ + s for s in _steps(_load(GATE_PATH), "gate") if "check-required-mount.sh" in s.get("run", "") + ] + assert len(canary) == 1, "exactly one in-band mount canary" + assert "exit 1" in canary[0]["run"] + assert canary[0]["env"]["GH_TOKEN"], "the canary needs a token to read protection" + + +def test_gate_declares_administration_read_permission() -> None: + perms = _load(GATE_PATH).get("permissions") + assert isinstance(perms, dict) and perms.get("administration") == "read" + + +def test_still_exactly_one_aggregating_cf_gate_step_after_additions() -> None: + # The new projection + canary steps must NOT read as a second battery step. + steps = _steps(_load(GATE_PATH), "gate") + cf_steps = [s for s in steps if re.search(r"\bcf-gate\b", s.get("run", ""))] + assert len(cf_steps) == 1 + + +# --- caller stub + the apply/audit mount scripts ----------------------------- + + +def test_caller_grants_administration_read() -> None: + gate = _load(TEMPLATE_PATH)["jobs"]["gate"] + assert gate["permissions"]["administration"] == "read" + + +def test_mount_apply_script_present_and_proves_via_audit() -> None: + apply = REPO / "templates" / "mount-required.sh" + assert apply.is_file(), "the apply-side mount script must ship beside the audit" + text = apply.read_text(encoding="utf-8") + assert "branches/${branch}/protection" in text, "the first mount is a full PUT of protection" + assert "enforce_admins" in text and "true" in text, "the mount must be non-bypassable" + assert "check-required-mount.sh" in text, "the apply script proves the mount via the audit" From 52e9893b4c2a9e9d2fd7533c42616a9299e7f837 Mon Sep 17 00:00:00 2001 From: Antawari Date: Fri, 26 Jun 2026 14:02:09 -0600 Subject: [PATCH 2/2] chore: scrub a pre-existing ticket id from ts/README.md (greens self-test) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit origin/main carries a ticket reference in ts/README.md that the kit's own no-ticket-ids self-test bans tree-wide — main is red on it. This is the identical one-line scrub also made in the cf-no-bon-ref gauge PR; git reconciles the matching change cleanly on merge. The lawgiver's house obeys the law. Co-Authored-By: Claude Opus 4.8 (1M context) --- ts/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ts/README.md b/ts/README.md index b675660..7230959 100644 --- a/ts/README.md +++ b/ts/README.md @@ -16,8 +16,8 @@ Pre-existing offenders are frozen in a baseline (ratchet: shrink-only, never gro ## 4-step consumer mount -These are exactly the steps SweetCRM Task 1 (BON-1701) follows when mounting -the kit into a new TypeScript repository. +These are exactly the steps the first SweetCRM mount follows when wiring the +kit into a new TypeScript repository. ### Step 1 — Copy the consumer templates