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:
2026-08-16 00:27:41 -04:00
parent 69f84e0bc6
commit b79c439c7f
9 changed files with 408 additions and 0 deletions
+126
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
# pre-commit → orders secret → build(typecheck) → test (fast gates only)
exec bash "$(git rev-parse --show-toplevel)/scripts/gate/gate.sh" commit
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# pre-push → orders secret (full history) → build (full next build) → test
# Note: git pushes a branch HEAD; scanning full history catches secrets
# committed in the past that a re-fetch of a bare repo would inherit.
exec bash "$(git rev-parse --show-toplevel)/scripts/gate/gate.sh" push
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# One-shot bootstrap of the gitleaks binary into ~/.local/bin (pinned version).
# Reproducible: fixed tag, not "latest". Bump GL_VER deliberately to upgrade.
set -eu
GL_VER="8.30.1"
DEST="${HOME}/.local/bin"
mkdir -p "$DEST"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
URL="https://github.com/gitleaks/gitleaks/releases/download/v${GL_VER}/gitleaks_${GL_VER}_linux_x64.tar.gz"
echo "[bootstrap] fetching gitleaks v${GL_VER}"
curl -sSL -m 180 -o "$tmp/gl.tgz" "$URL"
tar xzf "$tmp/gl.tgz" -C "$tmp" gitleaks
install -m0755 "$tmp/gitleaks" "${DEST}/gitleaks"
echo "[bootstrap] installed: $("$DEST/gitleaks" version)${DEST}/gitleaks"
echo "[bootstrap] ensure PATH includes ${DEST}, e.g. in your shell rc:
export PATH=\"${DEST}:\$PATH\""
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Install the repo commit gate into this clone (idempotent, re-runnable):
# 1. bootstrap gitleaks binary if missing → ~/.local/bin/gitleaks (pinned)
# 2. copy scripts/gate/hooks/* → .git/hooks/* (repo-local, NOT in repo)
# 3-9 show what the gate will do on every commit/push.
# Nothing here is in the committed history; hooks live in .git/hooks/.
set -eu
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
[ -f "$ROOT/scripts/install/install-hooks.sh" ] || { echo "install: cannot find repo root from $(dirname "$0")" >&2; exit 1; }
cd "$ROOT"
echo "== 1/3 gitleaks"
if ! command -v gitleaks >/dev/null 2>&1; then
if [ -x "$HOME/.local/bin/gitleaks" ]; then
echo "[install] gitleaks at ~/.local/bin (not on PATH) — will be used via absolute path"
else
bash scripts/install/bootstrap-gitleaks.sh
fi
else
gitleaks version | sed 's/^/[install] gitleaks /'
fi
echo "== 2/3 hooks"
install -m0755 scripts/gate/hooks/pre-commit .git/hooks/pre-commit
install -m0755 scripts/gate/hooks/pre-push .git/hooks/pre-push
echo "[install] wrote .git/hooks/pre-commit, .git/hooks/pre-push"
echo "== 3/3 ready"
echo "[install] commits gated: secret → tsc → tests (fast, seconds)"
echo "[install] pushes gated: secret(history) → full next build → tests"
echo "[install] bypass (auditable at the pusher's shell): git commit/push --no-verify"
echo "[install] NOTE: gate lives in THIS clone's .git/hooks — after git clone,"
echo "[install] the same-branch pull on another machine re-installs by re-running:"
echo "[install] bash scripts/install/install-hooks.sh"