diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml index 1753044d..f7c7f017 100644 --- a/.github/workflows/ci-pipeline.yml +++ b/.github/workflows/ci-pipeline.yml @@ -752,8 +752,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 00000000..f99c18f3 --- /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 e2230c87..06be43fc 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}"