Archived
chore(gate): commit pre-push gate (secret -> build -> test, fail-closed)
- scripts/gate/gate.sh: 3-stage gate — 1) gitleaks, 2) build
(tsc on commit, full next build on push), 3) LLM parse contract
tests. Fail-closed: setup errors, timeouts, and findings all block.
- scripts/gate/hooks/{pre-commit,pre-push}: exec gate.sh commit|push.
- scripts/install/install-hooks.sh: idempotent installer (verifies repo
root, bootstraps pinned gitleaks 8.30.1 if absent, wires both hooks).
- scripts/install/bootstrap-gitleaks.sh: pinned per-user install,
x86_64/arm64, GitHub release download + SHA-less checksum pin.
- .gitleaks.toml: useDefault=true; single allowlist = .env.example
placeholder lines (secret= and change-me values) by path+regex.
Real secrets — even inside .env.example — still trip the gate
(empirically verified: OpenAI/AWS/Slack/GitHub tokens all caught).
- tests/llm-parse.test.ts: pins parse.ts contracts (strict 6-paragraph
body, headline/scalar/or array rejection, stopword rules, tag
fallback) — the choke point for LLM output parsing.
- package.json: 'test' script.
- README: 'Commit gate' section (install, stages, verified fail-closed
modes).
Verified before commit: clean tree PASSes all 3 stages; staged
realistic secret FAILs stage 1 (exit 1); broken type FAILs stage 2;
broken assertion FAILs stage 3; next build exit 0.
This commit is contained in:
Executable
+126
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env bash
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
# maple-brief commit gate
|
||||
# Approved order (hard, fail-closed): 1) secret → 2) build → 3) test
|
||||
#
|
||||
# commit mode (pre-commit, fast):
|
||||
# 1. gitleaks on STAGED changes 2. tsc --noEmit 3. LLM parse unit tests
|
||||
# push mode (pre-push, heavy):
|
||||
# 1. gitleaks on full HEAD history 2. prisma generate && next build
|
||||
#
|
||||
# Fail-closed: any stage error (missing tool, crash, timeout, findings)
|
||||
# fails the gate. Same tree ⇒ same verdict (gates are pure functions of
|
||||
# committed content; no network, no cached state). Only deliberate escape
|
||||
# hatch: `git commit --no-verify` / `git push --no-verify` (explicit, logged
|
||||
# by the remote user).
|
||||
#
|
||||
# Usage: gate.sh commit | push
|
||||
# ────────────────────────────────────────────────────────────────────────────
|
||||
set -u
|
||||
|
||||
MODE="${1:-commit}"
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
cd "$ROOT" || { echo "gate: cannot cd to repo root" >&2; exit 1; }
|
||||
|
||||
TS0=$(date +%s)
|
||||
FAILURES=0
|
||||
|
||||
say() { printf '\n█ gate[%s] %s\n' "$MODE" "$1"; }
|
||||
ok() { printf '\x1b[32m PASS\x1b[0m %s\n' "$1"; }
|
||||
bad() { printf '\x1b[31m FAIL\x1b[0m %s\n' "$1"; FAILURES=$((FAILURES + 1)); }
|
||||
|
||||
# --- setup (fail-closed: never proceed without the gate components) ---------
|
||||
command -v gitleaks >/dev/null 2>&1 || {
|
||||
say "setup"
|
||||
bad "gitleaks not found — run: bash scripts/install/install-hooks.sh"
|
||||
printf '\nGATE VERDICT: FAIL (setup)\n'; exit 1; }
|
||||
[ -d node_modules ] || {
|
||||
say "setup"
|
||||
bad "node_modules missing — run: npm ci"
|
||||
printf '\nGATE VERDICT: FAIL (setup)\n'; exit 1; }
|
||||
[ -f .gitleaks.toml ] || {
|
||||
say "setup"; bad ".gitleaks.toml missing (gate config)";
|
||||
printf '\nGATE VERDICT: FAIL (setup)\n'; exit 1; }
|
||||
[ -x node_modules/.bin/tsx ] || {
|
||||
say "setup"; bad "tsx not in node_modules — run: npm ci";
|
||||
printf '\nGATE VERDICT: FAIL (setup)\n'; exit 1; }
|
||||
|
||||
# verdict key: what content this verdict is a function of
|
||||
case "$MODE" in
|
||||
commit) TREEKEY="$(git write-tree 2>/dev/null || echo no-staged-tree)" ;;
|
||||
push) TREEKEY="$(git rev-parse HEAD 2>/dev/null || echo no-head)" ;;
|
||||
*) say "usage: gate.sh commit|push"; exit 2 ;;
|
||||
esac
|
||||
|
||||
# --- stage 1: secret --------------------------------------------------------
|
||||
# $1 = "staged" | "history"; gitleaks 8.x takes no positional path — use
|
||||
# --staged for staged-only, no flag for full history. Exit 0 = clean,
|
||||
# 1 = findings, anything else = scan error (all fail-closed).
|
||||
stage_secret() {
|
||||
local scope="$1"
|
||||
case "$scope" in
|
||||
staged) say "1/3 secret — gitleaks (staged changes)"; local args=(--staged) ;;
|
||||
history) say "1/3 secret — gitleaks (full HEAD history)"; local args=() ;;
|
||||
*) bad "stage_secret: unknown scope '$scope'"; return 1 ;;
|
||||
esac
|
||||
local out rc
|
||||
out="$(timeout 180 gitleaks protect -c .gitleaks.toml --no-banner --no-color "${args[@]+"${args[@]}"}" 2>&1)"; rc=$?
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
bad "secret scan failed (exit ${rc:-ERR})"
|
||||
printf '%s\n' "$out" | grep -vE '^\s*$' | tail -12 | sed 's/^/ /'
|
||||
else
|
||||
ok "no secret matches in $scope"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- stage 2: build ---------------------------------------------------------
|
||||
stage_build_commit() {
|
||||
say "2/3 build — tsc --noEmit (strict TS)"
|
||||
local out rc
|
||||
out="$(timeout 240 ./node_modules/.bin/tsc --noEmit 2>&1)"; rc=$?
|
||||
if [ "$rc" -ne 0 ]; then bad "typecheck failed (exit $rc)"; printf '%s\n' "$out" | tail -15 | sed 's/^/ /';
|
||||
else ok "typecheck clean"; fi
|
||||
}
|
||||
stage_build_push() {
|
||||
say "2/3 build — prisma generate && next build (pristine, full)"
|
||||
local out rc
|
||||
out="$(timeout 900 npm run build 2>&1)"; rc=$?
|
||||
if [ "$rc" -ne 0 ]; then bad "build failed (exit $rc)"; printf '%s\n' "$out" | tail -20 | sed 's/^/ /';
|
||||
else ok "production build clean"; fi
|
||||
}
|
||||
|
||||
# --- stage 3: test ----------------------------------------------------------
|
||||
stage_test() {
|
||||
say "3/3 test — LLM parse contract (tests/llm-parse.test.ts)"
|
||||
local out rc
|
||||
out="$(timeout 120 ./node_modules/.bin/tsx tests/llm-parse.test.ts 2>&1)"; rc=$?
|
||||
if [ "$rc" -ne 0 ]; then bad "unit test failed (exit $rc)"; printf '%s\n' "$out" | grep -E '^(not ok|ok|fail)' | tail -10 | sed 's/^/ /';
|
||||
else ok "LLM parse contract holds ($(printf '%s\n' "$out" | grep -c '^ok ') checks)"; fi
|
||||
}
|
||||
|
||||
# --- run (all stages always execute; verdict = OR of failures) --------------
|
||||
case "$MODE" in
|
||||
commit)
|
||||
stage_secret staged
|
||||
stage_build_commit
|
||||
stage_test
|
||||
;;
|
||||
push)
|
||||
stage_secret history
|
||||
stage_build_push
|
||||
stage_test
|
||||
;;
|
||||
esac
|
||||
|
||||
ELAPSED=$(( $(date +%s) - TS0 ))
|
||||
case "$MODE" in
|
||||
commit) OBJECT="staged-tree $TREEKEY" ;;
|
||||
push) OBJECT="HEAD $TREEKEY" ;;
|
||||
esac
|
||||
if [ "$FAILURES" -eq 0 ]; then
|
||||
printf '\n════════════════════════════════════════\n GATE PASS [%s] %s in %ss\n════════════════════════════════════════\n' "$MODE" "$OBJECT" "$ELAPSED"
|
||||
exit 0
|
||||
else
|
||||
printf '\n════════════════════════════════════════\n GATE FAIL [%s] %s — %s stage(s) failed, in %ss\n bypass (auditable): git %s --no-verify\n════════════════════════════════════════\n' "$MODE" "$OBJECT" "$FAILURES" "$ELAPSED" "$MODE"
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user