From a5b8334a1ba02c613f8f90af00fb40ffa0b7043f Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 22 Sep 2026 10:57:15 +0100 Subject: [PATCH] feat(ci): ratchet ci-pipeline adoption behind a shrink-only ledger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner ruling: adopt ci-pipeline.yml estate-wide as a ratchet, "like Deno and the lock gate" — blocking where the pipeline is green, ledgered where it is not. This adds the mechanism only. No repository adopts it in this commit. The gate lives where the verdict already lived: the `report` job's final step, which until now was an unconditional `exit 1`. It now consults a central shrink-only ledger, `.machine_readable/pipeline-allow.txt`, registered in LEDGERS=() in scripts/check-exemption-ratchet.sh. No new gate machinery — the ledger is read exactly the way the Deno gate reads deno-allow.txt. Three properties are load-bearing: 1. The ledger is consulted ONLY on the failure path. A green repository never reads it, never pays for the checkout, and cannot be affected by the ledger being stale, empty or unreachable. This is also why the pinned ref can safely predate the ledger: on the failure path an unreadable ledger and an empty ledger give the SAME verdict — blocked. The gate fails closed for free, and "could not read the ledger" can never read as "is exempt". 2. The denominator is printed on BOTH paths, as `pipeline debt: N repos`. An exemption is never a silent pass, and the exempt step summary says in as many words that the gates really failed and that an exemption is debt someone agreed to carry, not a pass. 3. THE LEDGER IS SEEDED EMPTY, AND DELIBERATELY SO. An entry must be earned by observation — the pipeline ran there and went red. It is explicitly NOT seeded from the estate census, which measured `statusCheckRollup` on each default branch (388 of 447 red). That answers "does this repo's EXISTING CI pass", not "does ci-pipeline.yml pass here", and the two populations are close to independent: a repo with four dead mirror jobs and clean code passes gitleaks, semgrep and the lint gates, while a green-rollup repo can fail the format gate on day one. Seeding 388 slugs from that number would have made the ledger a rubber stamp on its first commit and the denominator it prints a lie. Verdict matrix, measured against the run: body extracted from the parsed YAML, so the test exercises exactly what CI executes: ledger MISSING rc=1 pipeline debt: 0 repos (fails closed) ledger comments-only rc=1 pipeline debt: 0 repos slug IS ledgered rc=0 pipeline debt: 3 repos slug NOT ledgered rc=1 pipeline debt: 2 repos substring near-miss rc=1 pipeline debt: 2 repos (grep -Fxq, not -q) The substring case is the one that would have made this gate a rubber stamp quietly: `hyperpolymath/standards-extra` and `prefix-hyperpolymath/standards` must NOT exempt `hyperpolymath/standards`. scripts/tests/exemption-ratchet-test.sh: 14 passed, 0 failed with the new ledger registered. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ji1bq3TypfycfUPAR7hSxR --- .github/workflows/ci-pipeline.yml | 99 +++++++++++++++++++++++++++- .machine_readable/pipeline-allow.txt | 49 ++++++++++++++ scripts/check-exemption-ratchet.sh | 1 + 3 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 .machine_readable/pipeline-allow.txt diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index d831065c7..fddcf12fc 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -751,8 +751,103 @@ jobs: echo "" } >> "$GITHUB_STEP_SUMMARY" - - name: Fail if any gate failed + # ── The pipeline adoption ratchet ──────────────────────────────── + # + # Both steps below run ONLY when a gate has already failed. A green + # repository never reads the ledger, never pays for the checkout, and + # cannot be affected by the ledger being stale, empty or unreachable. + # + # That is what makes the pin harmless: on the failure path an unreadable + # ledger and an empty ledger give the SAME verdict — blocked. The gate + # therefore fails closed for free, and "could not read the ledger" can + # never be mistaken for "is exempt". + - name: Checkout the pinned Standards pipeline ledger if: ${{ contains(needs.*.result, 'failure') }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: hyperpolymath/standards + # ⚠ This ref DELIBERATELY PREDATES the ledger it reads. At a2ff696a + # `.machine_readable/pipeline-allow.txt` does not exist, so the next + # step fails closed — which is exactly the verdict the empty ledger + # would give anyway. The pin and the first entry move in the SAME + # commit: whoever adds a slug must bump this ref to the commit that + # carries it, or their own exemption will not be read. + # + # Following `main` here instead is not an option, for the same reason + # the Deno ledger does not: an edit in `standards` would change the + # verdict of every already-pinned caller with no review in their + # repositories. + ref: a2ff696a + path: .standards-pipeline-ledger + sparse-checkout: | + .machine_readable/pipeline-allow.txt + sparse-checkout-cone-mode: false + # Not fatal: the next step names what was missing and then FAILS CLOSED. + continue-on-error: true + + - name: Verdict — block unless this repository is ledgered + if: ${{ contains(needs.*.result, 'failure') }} + shell: bash + env: + LEDGER: .standards-pipeline-ledger/.machine_readable/pipeline-allow.txt + SLUG: ${{ github.repository }} run: | - echo "::error::At least one pipeline gate failed. See the report table in the step summary." + # LEDGER and SLUG come from the step `env:` map above. + set -euo pipefail + + # Strip comments and blanks; this is also the ledger's own + # denominator. A missing file yields an empty list and N=0, which is + # the fail-closed path: nothing can match an empty list. + if [ -f "$LEDGER" ]; then + ALLOWED="$(grep -vE '^[[:space:]]*(#|$)' "$LEDGER" || true)" + else + ALLOWED="" + echo "::warning::Pipeline ledger not readable ($LEDGER). Treating as EMPTY, which blocks. This is not an exemption." + fi + N_ALLOWED="$(printf '%s' "$ALLOWED" | grep -c . || true)" + + # ALWAYS print the denominator, on both paths. A gate that only ever + # says yes proves nothing, and a `::warning::` cannot fail a job. + echo "pipeline debt: ${N_ALLOWED} repos" + + if printf '%s\n' "$ALLOWED" | grep -Fxq "$SLUG"; then + echo "::notice::${SLUG} is on the pipeline exemption ledger (pipeline debt: ${N_ALLOWED} repos). The gates below REALLY FAILED — this is grandfathered debt, not a pass. Fix what the report table names, then delete this repository's line from .machine_readable/pipeline-allow.txt." + { + echo "### Pipeline verdict — ledgered debt" + echo "" + echo "| | |" + echo "|---|---|" + echo "| Repository | \`${SLUG}\` |" + echo "| Verdict | **exempt** (shrink-only ledger) |" + echo "| pipeline debt | **${N_ALLOWED} repos** |" + echo "" + echo "**At least one gate above really failed.** An exemption is not a pass;" + echo "it is debt that someone agreed to carry. The report table names every" + echo "failing gate. Fix them, then DELETE this repository's line from" + echo "\`.machine_readable/pipeline-allow.txt\` in \`hyperpolymath/standards\` —" + echo "deletion needs no \`Ratchet-exception\` trailer, because shrinking is" + echo "the point." + echo "" + } >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + echo "::error::At least one pipeline gate failed, and ${SLUG} is not on the exemption ledger (pipeline debt: ${N_ALLOWED} repos). See the report table in the step summary." + { + echo "### Pipeline verdict — BLOCKED" + echo "" + echo "| | |" + echo "|---|---|" + echo "| Repository | \`${SLUG}\` |" + echo "| Verdict | **blocked** (not ledgered) |" + echo "| pipeline debt | **${N_ALLOWED} repos** |" + echo "" + echo "The report table above names every gate that failed. Fix them, or — if" + echo "this is debt the estate has agreed to carry — add \`${SLUG}\` to" + echo "\`.machine_readable/pipeline-allow.txt\` in \`hyperpolymath/standards\`," + echo "bumping the ledger pin in the same commit, with a" + echo "\`Ratchet-exception: .machine_readable/pipeline-allow.txt — \`" + echo "trailer in column 0." + echo "" + } >> "$GITHUB_STEP_SUMMARY" exit 1 diff --git a/.machine_readable/pipeline-allow.txt b/.machine_readable/pipeline-allow.txt new file mode 100644 index 000000000..f99c18f31 --- /dev/null +++ b/.machine_readable/pipeline-allow.txt @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) +# +# pipeline-allow.txt — SHRINK-ONLY exemption ledger for the pipeline verdict +# gate (the `report` job of .github/workflows/ci-pipeline.yml). +# +# Owner ruling: adopt ci-pipeline.yml estate-wide as a RATCHET, "like Deno and +# the lock gate" — blocking where the pipeline is green, ledgered where it is +# not. A listed repository still RUNS every gate and still reports each one; it +# is spared only the final `exit 1`, and gets a `::notice::` naming its debt. +# Both paths print the denominator, so an exemption is never a silent pass. +# +# One `owner/name` slug per line, sorted. The gate compares +# `${{ github.repository }}` — a SLUG, not a path. Local checkout paths are not +# a valid key: the estate's ~240 linked worktrees check the same repository out +# many times over. +# +# ⚠ SHRINK-ONLY. Registered in LEDGERS=() in scripts/check-exemption-ratchet.sh, +# which line-counts this file against BASE_REF and FAILS on growth unless a +# commit in the range carries a column-0 trailer naming this exact path: +# +# Ratchet-exception: .machine_readable/pipeline-allow.txt — +# +# ── SEEDED EMPTY, DELIBERATELY ───────────────────────────────────────────── +# +# An entry here must be EARNED BY OBSERVATION: the pipeline ran in that +# repository and went red. It must never be seeded by inference. +# +# In particular it is NOT seeded from the estate census in +# `2026-09-22 STEP4-CENSUS`. That census measured `statusCheckRollup` on each +# default branch — "does this repository's EXISTING CI pass" — and found 388 of +# 447 red. That is a different question from "does ci-pipeline.yml pass here", +# and the two populations are close to independent: a repository whose four +# mirror jobs are dead but whose code is clean passes gitleaks, semgrep and the +# lint gates; a green-rollup repository can fail the format gate on day one. +# Seeding 388 slugs from that number would have made the ledger a rubber stamp +# on its first commit, and the denominator it prints would have been a lie. +# +# The honest seed is a dry-run wave: adopt the caller in a deliberate sample, +# record the real verdict per repository, and add only what actually went red — +# in a commit carrying the trailer above. +# +# ── HOW TO LEAVE THIS LEDGER ─────────────────────────────────────────────── +# +# Fix what the pipeline reported (the `report` job's table names every failing +# gate), then DELETE the line. Deletion needs no trailer: shrinking is the +# point. +# +# slugs below this line, sorted, one per line diff --git a/scripts/check-exemption-ratchet.sh b/scripts/check-exemption-ratchet.sh index e2230c879..06be43fc3 100755 --- a/scripts/check-exemption-ratchet.sh +++ b/scripts/check-exemption-ratchet.sh @@ -131,6 +131,7 @@ LEDGERS=( ".machine_readable/root-allow.txt" ".machine_readable/lock-allow.txt" ".machine_readable/deno-allow.txt" + ".machine_readable/pipeline-allow.txt" ) echo "Exemption ratchet — comparing against ${BASE_REF}"