Skip to content

Commit a44b45b

Browse files
Mark Shorsclaude
andcommitted
[LAT-1878] Add pre-push hook — the CI gate cannot run (org billing lock)
.github/workflows/docs-drift.yml is correct but cannot execute: GitHub Actions is locked org-wide. Every job returns "the job was not started because your account is locked due to a billing issue". 100/100 runs on mcpanalytics-operations are startup_failure, its link-check.yml has not run since 2026-05-02, and the last successful run on any org repo was 2026-06-20. A gate that cannot execute is not a gate, so this adds the mechanism that works today: a pre-push hook, the same pattern that has kept api-mcp-prod's SSOT invariants honest (LAT-1686). The workflow stays for when billing is restored. Splits the exit codes, because this check needs NETWORK where api-mcp-prod's invariant hook deliberately does not: 1 = drift (or nothing found to compare) -> BLOCKS the push 3 = platform unreachable -> WARNS, allows the push Blocking every push made offline would just teach everyone to reach for --no-verify, and then the gate is gone for the case that actually matters. Verified end-to-end: with README set back to "2,000 welcome credits", the installed hook blocks a real `git push` and names the file and line. Install: ./scripts/hooks/install.sh Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent cde1175 commit a44b45b

3 files changed

Lines changed: 103 additions & 3 deletions

File tree

scripts/check-docs-drift.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,13 @@
3939
4040
Exit codes:
4141
0 docs agree with the platform
42-
1 drift found, or nothing found to compare, or the platform was unreachable
42+
1 DRIFT — a doc claim disagrees with the platform, or nothing was found to
43+
compare (the check has gone blind). Both are real problems.
44+
3 COULD NOT VERIFY — the platform was unreachable. Distinct from 1 on
45+
purpose: CI treats any non-zero as failure (a green check on an
46+
unreachable API would be a lie), but the pre-push hook lets 3 through
47+
with a warning. A gate that blocks every push made on a train gets
48+
disabled with --no-verify, and then it is not a gate at all.
4349
"""
4450

4551
from __future__ import annotations
@@ -92,10 +98,12 @@ def fetch_platform(api_base: str) -> dict:
9298
return json.load(r)
9399
except (urllib.error.URLError, urllib.error.HTTPError,
94100
TimeoutError, OSError, ValueError) as e:
95-
print(f"FATAL: could not read {url}: {e}", file=sys.stderr)
101+
print(f"COULD NOT VERIFY: {url}: {e}", file=sys.stderr)
96102
print(" Refusing to pass — an unreachable platform is not "
97103
"evidence that the docs are correct.", file=sys.stderr)
98-
raise SystemExit(1)
104+
print(" (exit 3, distinct from drift; see the module docstring)",
105+
file=sys.stderr)
106+
raise SystemExit(3)
99107

100108

101109
def main() -> int:

scripts/hooks/install.sh

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
#!/usr/bin/env bash
2+
# Install this repo's git hooks (LAT-1878).
3+
#
4+
# Hooks live in scripts/hooks/ (tracked) and are symlinked into .git/hooks/
5+
# (untracked). Symlink rather than copy, so an updated hook takes effect on the
6+
# next pull instead of silently running a stale version — a hook that everyone
7+
# has a different copy of is worse than no hook.
8+
set -euo pipefail
9+
10+
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
11+
SRC="$REPO/scripts/hooks"
12+
DST="$REPO/.git/hooks"
13+
14+
[ -d "$DST" ] || { echo "no .git/hooks — not a git checkout?" >&2; exit 1; }
15+
16+
for hook in pre-push; do
17+
if [ -e "$DST/$hook" ] && [ ! -L "$DST/$hook" ]; then
18+
echo "backing up existing $hook -> $hook.bak"
19+
mv "$DST/$hook" "$DST/$hook.bak"
20+
fi
21+
ln -sf "../../scripts/hooks/$hook" "$DST/$hook"
22+
chmod +x "$SRC/$hook"
23+
echo "installed: .git/hooks/$hook -> scripts/hooks/$hook"
24+
done
25+
26+
echo
27+
echo "Verify with: python3 scripts/check-docs-drift.py"
28+
echo "Bypass once: git push --no-verify"

scripts/hooks/pre-push

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
#!/usr/bin/env bash
2+
# Pre-push gate: this repo's public claims must match the live platform.
3+
#
4+
# WHY A HOOK AND NOT (ONLY) CI — LAT-1878/LAT-1883:
5+
# .github/workflows/docs-drift.yml exists and is correct, but GitHub Actions
6+
# cannot run in this org at all: every job returns "the job was not started
7+
# because your account is locked due to a billing issue". 100/100 runs on
8+
# mcpanalytics-operations are startup_failure; the last successful run on any
9+
# org repo was 2026-06-20. A gate that cannot execute is not a gate.
10+
# This hook works today, locally, with no billing dependency. The workflow
11+
# stays for when billing is restored — belt and braces, not either/or.
12+
#
13+
# WHY IT IS SAFE TO GATE ON:
14+
# One HTTPS GET to a public, cached endpoint and a regex over five markdown
15+
# files. ~1s. It needs NETWORK, which the api-mcp-prod invariant hook
16+
# deliberately does not — so unreachable (exit 3) WARNS and lets the push
17+
# through, while actual drift (exit 1) blocks. Blocking a push made on a
18+
# plane would just teach everyone to type --no-verify, and then the gate is
19+
# gone for the case that matters.
20+
#
21+
# INSTALL: ./scripts/hooks/install.sh
22+
# BYPASS: git push --no-verify (consciously, not reflexively)
23+
24+
set -uo pipefail
25+
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
26+
cd "$REPO" || exit 0
27+
28+
# Nothing to check on an old branch / fresh clone that predates the script.
29+
[ -f scripts/check-docs-drift.py ] || exit 0
30+
31+
PY="$(command -v python3 || true)"
32+
if [ -z "$PY" ]; then
33+
echo "pre-push: python3 not found — skipping docs-drift check" >&2
34+
exit 0
35+
fi
36+
37+
echo "pre-push: docs drift vs live platform…"
38+
OUT="$("$PY" scripts/check-docs-drift.py 2>&1)"
39+
RC=$?
40+
41+
case "$RC" in
42+
0)
43+
echo "pre-push: docs drift OK"
44+
;;
45+
3)
46+
# Could not reach the platform. Warn, do not block — see header.
47+
echo "pre-push: ⚠️ could not verify docs against the platform (offline?)" >&2
48+
echo "$OUT" | sed 's/^/ /' >&2
49+
echo "pre-push: allowing push — re-run manually when back online:" >&2
50+
echo " python3 scripts/check-docs-drift.py" >&2
51+
;;
52+
*)
53+
echo "$OUT" | sed 's/^/ /' >&2
54+
echo >&2
55+
echo "pre-push: BLOCKED — this repo's public docs disagree with the live" >&2
56+
echo " platform. These files are read by prospects deciding" >&2
57+
echo " whether we are real; shipping a wrong number here has" >&2
58+
echo " already happened twice (LAT-1680, LAT-1876)." >&2
59+
echo " Fix the docs, or 'git push --no-verify' if you are sure." >&2
60+
exit 1
61+
;;
62+
esac
63+
64+
exit 0

0 commit comments

Comments
 (0)