mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user