fix(governance): make commit-time guard verdicts survive to CI

Canonical Governance failed on nearly every push today (6 of the last 8
commits) because the local pre-commit hook and the CI workflow disagreed
about the canonical completion guard in two ways.

Frontend-only commits skipped the guard entirely: the hook gated ALL
governance checks behind governance-path detection as a perf
optimization, but subsystem contracts name canonical frontend files, so
those commits landed locally and failed the same guard in CI. The hook
now always runs the (cheap) completion guard; only the multi-minute Go
test and audit battery stays path-gated.

Contract-neutral bypasses did not travel: PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT
authorized the commit in the local shell and vanished, so CI re-ran the
guard without it and went red. A new prepare-commit-msg hook records the
reason as a Contract-Neutral trailer in the commit message, and the
workflow now evaluates the guard per commit (each commit's file list
against its own parent), honoring the trailer exactly as the commit-time
hook honored the env var. Per-commit evaluation also stops a compliant
commit from being blamed for range-mates.

The new hook is registered as a worktree-sensitive governance file in
governance_stage_guard.py with a matching unit test assert.
This commit is contained in:
rcourtman 2026-07-17 14:35:34 +01:00
parent 63c2118a16
commit eb5a477dc9
5 changed files with 68 additions and 11 deletions

View file

@ -79,22 +79,52 @@ jobs:
fi
echo "range=${range}" >> "$GITHUB_OUTPUT"
- name: Run canonical completion guard against changed files
- name: Run canonical completion guard against changed commits
shell: bash
run: |
set -euo pipefail
range="${{ steps.diff.outputs.range }}"
if [ -n "${range}" ]; then
# Pass the range base so the guard compares contract texts
# base-vs-HEAD; the CI index equals HEAD, so the default
# index comparison would misreport every contract update
# in the range as insubstantial.
git diff --name-only "${range}" \
| python3 scripts/release_control/canonical_completion_guard.py \
--files-from-stdin --diff-base "${range%%...*}"
else
if [ -z "${range}" ]; then
printf '' | python3 scripts/release_control/canonical_completion_guard.py --files-from-stdin
exit 0
fi
base="${range%%...*}"
head_sha="${range##*...}"
# Evaluate each commit the way the commit-time hook did: its own
# file list against its own parent. A range-wide evaluation
# cannot honor per-commit Contract-Neutral trailers (recorded by
# .husky/prepare-commit-msg when the author sets
# PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT), so bypassed commits that
# passed locally would fail here.
if ! commits=$(git rev-list --reverse --no-merges "${base}..${head_sha}" 2>/dev/null); then
echo "Range base ${base} unavailable (force push?); evaluating ${head_sha} against its parent."
commits="${head_sha}"
fi
if [ -z "${commits}" ]; then
echo "No commits to evaluate in range ${range}."
exit 0
fi
status=0
for commit in ${commits}; do
if ! git rev-parse --verify --quiet "${commit}^" >/dev/null; then
echo "Skipping root commit ${commit} (no parent to diff against)."
continue
fi
reason=$(git log -1 --format='%(trailers:key=Contract-Neutral,valueonly,separator=; )' "${commit}" | tr '\n' ' ')
echo "::group::canonical completion guard @ ${commit}"
# Pass the parent as the diff base so the guard compares
# contract texts parent-vs-commit; the CI index equals HEAD,
# so the default index comparison would misreport contract
# updates in earlier commits as insubstantial.
if ! git diff-tree --no-commit-id --name-only -r "${commit}" \
| PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT="${reason}" python3 scripts/release_control/canonical_completion_guard.py \
--files-from-stdin --diff-base "${commit}^"; then
echo "Canonical completion guard failed for commit ${commit}."
status=1
fi
echo "::endgroup::"
done
exit ${status}
- name: Run status audit
env: