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
+40 -10
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:
+9 -1
View File
@@ -135,7 +135,15 @@ python3 scripts/release_control/subsystem_lookup_test.py
unset PULSE_READ_STAGED_GOVERNANCE
elif [ -f "docs/release-control/v6/internal/status.json" ]; then
echo "Skipping governance checks (no staged governance, Go, repoctl, or hook changes)."
# The completion guard still runs on frontend-only commits: subsystem
# contracts name canonical frontend files (alerts, platform pages, ...),
# and the canonical-governance workflow re-runs this guard per commit in
# CI. Skipping it here lets a frontend commit land locally and then fail
# CI on the very same check. The guard itself is cheap (seconds); only
# the Go test + audit battery above stays gated on governance paths.
echo "Running canonical completion guard..."
python3 scripts/release_control/canonical_completion_guard.py
echo "Skipping remaining governance checks (no staged governance, Go, repoctl, or hook changes)."
else
echo "Governance files not present, skipping governance checks."
fi
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env sh
set -eu
# Record the contract-neutral bypass in the commit itself. The canonical
# completion guard re-runs per commit on GitHub Actions, where the
# PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT env var from the local commit shell
# does not exist; this trailer is the auditable, machine-readable carrier
# the canonical-governance workflow reads back for each commit in the
# pushed range. Without it, every legitimately bypassed commit fails CI.
msg_file="$1"
reason="${PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT:-}"
reason=$(printf '%s' "$reason" | tr '\n' ' ' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
if [ -n "$reason" ]; then
git interpret-trailers --in-place --if-exists replace \
--trailer "Contract-Neutral: $reason" "$msg_file"
fi
@@ -17,6 +17,7 @@ WORKTREE_SENSITIVE_PREFIXES = (
)
WORKTREE_SENSITIVE_EXACT_FILES = (
".husky/pre-commit",
".husky/prepare-commit-msg",
CONTROL_PLANE_REL,
)
STAGED_EXECUTION_EXACT_FILES = (
@@ -27,6 +27,7 @@ class GovernanceStageGuardTest(unittest.TestCase):
def test_is_worktree_sensitive_governance_path_matches_expected_scope(self) -> None:
self.assertTrue(is_worktree_sensitive_governance_path(".husky/pre-commit"))
self.assertTrue(is_worktree_sensitive_governance_path(".husky/prepare-commit-msg"))
self.assertTrue(is_worktree_sensitive_governance_path("docs/release-control/control_plane.json"))
self.assertTrue(is_worktree_sensitive_governance_path("scripts/release_control/status_audit.py"))
self.assertFalse(