R-404: block the push that can create the golden debt, notify the one that cannot

push_scope.py classifies a push as code or documents from an ALLOW-LIST of document paths -
everything else, including any new top-level directory, is code. Every uncertainty (first push,
force-push, merge commit, empty range, unreadable stdin) answers code: guessing 'documents' would
hand out the exemption by accident.

repo_gates.py gains a fifth GATES field and --scope=code|docs. On a documents-only push a
golden-currency CONVICTION prints as ADVISORY in its own block and does not refuse; every other
gate still refuses every push, and golden-currency still refuses a push touching code. The gate
itself is UNCHANGED - its verdict, exit codes and wording are byte-identical. What changed is who
is refused.

Measured on git 2.47.3: a pre-push hook receives <local ref> <local sha> <remote ref> <remote sha>
on stdin, one line per ref; a first push carries an all-zero remote sha and a deletion an all-zero
local sha. Both land on code.
This commit is contained in:
2026-09-01 11:53:18 +02:00
parent a91c0580eb
commit 1c00af607c
6 changed files with 773 additions and 31 deletions
+67 -1
View File
@@ -92,11 +92,77 @@ jobs:
git checkout -q FETCH_HEAD
echo "controller CHANGELOG at $(git rev-parse --short=12 HEAD): $(head -1 CHANGELOG.md)"
- name: Classify the push - code or documents (R-404)
# ONE RULE, NOT TWO. The pre-push hook exempts a golden-currency CONVICTION on a
# documents-only push; if CI did not do the same, a drill night would still produce red CI
# runs indistinguishable from real ones, which is R-417 exactly and is half the reason this
# change exists.
#
# CI CANNOT USE A COMMIT RANGE. The checkout above is `--depth 1` of a single SHA, so there
# is no history here to diff against — `git diff before..after` would fail, and deepening
# the fetch to make it work would slow every run to solve a problem the push event has
# already answered. So the file list comes from the push event payload instead, and is fed
# to the SAME classifier the hook uses (`--files-from`), so there is one implementation of
# "what counts as a document" and not two.
#
# FAIL CLOSED, EVERY PATH. No payload, no `commits` array, an empty array, unreadable JSON,
# a missing classifier — all write `code`, which is exactly today's behaviour. This step can
# therefore only ever make CI as strict as it is now, never looser. That is also why it is
# safe to ship before it has been observed on a real push: the untested direction is the
# safe one.
run: |
set -u
python3 - > /tmp/pushed-files.txt <<'PY' || : > /tmp/pushed-files.txt
import json, os, sys
path = os.environ.get("GITHUB_EVENT_PATH", "")
if not path or not os.path.isfile(path):
sys.stderr.write("no GITHUB_EVENT_PATH - the file list is unknown\n")
raise SystemExit(0)
try:
ev = json.load(open(path))
except Exception as e:
sys.stderr.write("event payload unreadable: %s\n" % e)
raise SystemExit(0)
commits = ev.get("commits") or []
if not commits:
sys.stderr.write("the payload carries no commits array - unknown\n")
raise SystemExit(0)
seen = []
for c in commits:
for key in ("added", "modified", "removed"):
for f in (c.get(key) or []):
if f not in seen:
seen.append(f)
sys.stderr.write("%d commit(s), %d distinct path(s) in the payload\n"
% (len(commits), len(seen)))
for f in seen:
print(f)
PY
echo "--- paths the push event reported ---"
cat /tmp/pushed-files.txt
echo "-------------------------------------"
if [ -s /tmp/pushed-files.txt ] && [ -f scripts/push_scope.py ]; then
SCOPE=$(python3 scripts/push_scope.py --files-from /tmp/pushed-files.txt) || SCOPE=code
else
echo "no usable file list - treating this push as CODE (fail-closed)"
SCOPE=code
fi
[ "$SCOPE" = "docs" ] || SCOPE=code
echo "PUSH_SCOPE=$SCOPE" >> "$GITHUB_ENV"
echo "scope: $SCOPE"
- name: Run the gate entry point
# The ONLY thing CI runs. No go build, no go test, no linting, no deploy — those are either
# already reliably run by a person or none of CI's business. The exit code IS the result:
# no `|| true`, no pipe that could swallow it.
run: python3 scripts/repo_gates.py --fast
#
# A documents-only run that convicts ONLY on golden-currency prints the advisory and stays
# green. THE DEBT IS NOT HIDDEN WHEN THAT HAPPENS — three things still carry it: the
# advisory block in this run's own log, `STATUS.md`, and the controller repo's golden-notice,
# which prints at the moment a release is committed, where someone can actually act on it.
# Those are the compensating controls that make this green honest. Every other gate still
# fails this job on any push, and golden-currency still fails it on a push touching code.
run: python3 scripts/repo_gates.py --fast --scope="${PUSH_SCOPE:-code}"
- name: Alarm on failure
# THE POINT OF THE WHOLE THING. Probe P5 measured that a failed run produces NO mail, NO