Skip to main content

max / makenotwork

6.5 KB · 149 lines History Blame Raw
1 #!/bin/bash
2 # rustfmt gate: blocks a commit whose staged Rust files are not formatted.
3 #
4 # Activate in a fresh clone (one-time):
5 # git config core.hooksPath scripts/githooks
6 #
7 # Bypass for a work-in-progress commit: git commit --no-verify
8 #
9 # Only crates with staged .rs changes are checked, so the hook stays fast on a
10 # large repo. Each file maps to the nearest enclosing Cargo.toml, and the check
11 # runs as `cargo fmt` there, which picks up that crate's edition and any
12 # rustfmt.toml rather than guessing.
13 set -euo pipefail
14
15 ROOT="$(git rev-parse --show-toplevel)"
16 cd "$ROOT"
17
18 # --- secret scan (gitleaks) -------------------------------------------------
19 # Independent guardrail: blocks a commit whose staged changes contain a secret,
20 # regardless of whether a human judged the value "safe". Shared ruleset lives at
21 # ~/Code/.gitleaks.toml. Degrades gracefully if gitleaks is not installed (the
22 # astra pre-receive hook is the backstop that always runs). Task: infra 97ffeda0.
23 if command -v gitleaks >/dev/null 2>&1; then
24 GL_CFG=""
25 if [ -f "$ROOT/.gitleaks.toml" ]; then
26 GL_CFG="$ROOT/.gitleaks.toml"
27 elif [ -f "$HOME/Code/.gitleaks.toml" ]; then
28 GL_CFG="$HOME/Code/.gitleaks.toml"
29 fi
30 gl_args=(git --staged --no-banner --redact)
31 [ -n "$GL_CFG" ] && gl_args+=(-c "$GL_CFG")
32 if ! gitleaks "${gl_args[@]}"; then
33 echo "pre-commit: gitleaks found a secret in the staged changes."
34 echo " remove it (or allowlist a false positive), then restage."
35 echo " bypass: git commit --no-verify."
36 exit 1
37 fi
38 echo "pre-commit: gitleaks clean."
39 else
40 echo "pre-commit: gitleaks not installed; skipping secret scan (astra gates on push)."
41 fi
42
43 # --- migration immutability -------------------------------------------------
44 # sqlx checksums a migration's whole file when it runs it and refuses one whose
45 # bytes changed since. So editing a migration that has already been applied
46 # anywhere breaks every deploy against that database with "previously applied
47 # but modified" — for a comment edit, and for a line-ending change, exactly as
48 # much as for a schema change.
49 #
50 # The 2026-07-27 exorcise sweep rewrote comments in 29 applied migrations and
51 # converted one from CRLF to LF. Nothing in the test suite checksums a
52 # migration, so it stayed invisible while it blocked every server deploy for
53 # three days. Hence a gate at commit time: a migration that already exists in
54 # HEAD may not be modified, renamed, or deleted. Adding a new one is always fine.
55 touched="$(git diff --cached --name-only --diff-filter=MDR -- '*migrations/*.sql')"
56 if [ -n "$touched" ]; then
57 echo "pre-commit: these already-committed migrations were modified, renamed, or deleted:"
58 while IFS= read -r m; do
59 [ -n "$m" ] && echo " $m"
60 done <<< "$touched"
61 echo " A migration is immutable once applied; sqlx checksums the whole"
62 echo " file, comments included. Write a new migration instead."
63 echo " Bypass ONLY if it has never been applied anywhere, including"
64 echo " prod, staging, and your dev database: git commit --no-verify."
65 exit 1
66 fi
67 echo "pre-commit: migrations unmodified."
68
69 # --- openapi.json staleness -------------------------------------------------
70 # server/openapi.json is a committed artifact and `openapi::tests::
71 # committed_spec_matches_generated` asserts it matches the generated spec. The
72 # spec embeds CARGO_PKG_VERSION, so EVERY version bump invalidates it even when
73 # no route changed.
74 #
75 # Nothing local caught that. The /deploy pre-push guard is a `cargo test
76 # --no-run` compile check, and the spec is read at runtime by path rather than
77 # include_str!, so a stale copy compiles fine. On 2026-08-06 the v0.11.8 bump
78 # left the spec at 0.11.7, pushed clean to all three remotes, and killed Sando
79 # run 38 about fifteen minutes in — two full remote build cycles for a one-line
80 # diff in info.version.
81 #
82 # So: regenerate to stdout and compare against the STAGED copy (not the working
83 # tree one — regenerating without restaging is the same bug wearing a hat).
84 specish="$(git diff --cached --name-only --diff-filter=ACMR \
85 -- 'server/Cargo.toml' 'server/src/*.rs' 'server/src/**/*.rs' 'server/openapi.json')"
86 if [ -n "$specish" ]; then
87 echo "pre-commit: checking openapi.json against the generated spec..."
88 gen="$(mktemp)"
89 trap 'rm -f "$gen"' EXIT
90 if (cd "$ROOT/server" && cargo run --quiet --bin export-openapi -- --stdout) > "$gen" 2>/dev/null; then
91 if ! git show :server/openapi.json 2>/dev/null | diff -q - "$gen" >/dev/null; then
92 echo "pre-commit: server/openapi.json is stale (or regenerated but not staged)."
93 echo " cd server && cargo run --bin export-openapi"
94 echo " git add server/openapi.json"
95 echo " Then vendor the same bytes into the OTHER repo, which this"
96 echo " commit cannot carry and Sando will fail on:"
97 echo " cp server/openapi.json ../synckit/synckit-client/tests/openapi.json"
98 echo " Bypass: git commit --no-verify."
99 exit 1
100 fi
101 echo "pre-commit: openapi.json current."
102 else
103 echo "pre-commit: could not build export-openapi; skipping spec check."
104 echo " cargo_test in Sando is the backstop, 15 minutes into the build."
105 fi
106 fi
107
108 # Paths the gate ignores (extended regex, matched against repo-relative paths).
109 # Empty means check everything.
110 SKIP_PATHS="${SKIP_PATHS:-}"
111
112 staged="$(git diff --cached --name-only --diff-filter=ACMR -- '*.rs')"
113 if [ -n "$SKIP_PATHS" ]; then
114 staged="$(printf '%s\n' "$staged" | grep -Ev "$SKIP_PATHS" || true)"
115 fi
116 [ -n "$staged" ] || exit 0
117
118 # Map each staged file to the directory of its nearest Cargo.toml.
119 crates=""
120 while IFS= read -r f; do
121 [ -n "$f" ] || continue
122 d="$(dirname "$f")"
123 while [ "$d" != "." ] && [ ! -f "$d/Cargo.toml" ]; do
124 d="$(dirname "$d")"
125 done
126 [ -f "$d/Cargo.toml" ] || continue
127 crates="$crates$d"$'\n'
128 done <<< "$staged"
129
130 crates="$(printf '%s' "$crates" | sort -u)"
131 [ -n "$crates" ] || exit 0
132
133 failed=0
134 while IFS= read -r c; do
135 [ -n "$c" ] || continue
136 if ! (cd "$c" && cargo fmt --check >/dev/null 2>&1); then
137 echo "pre-commit: rustfmt gate failed in $c"
138 failed=1
139 fi
140 done <<< "$crates"
141
142 if [ "$failed" -ne 0 ]; then
143 echo "pre-commit: run 'cargo fmt' in the crates above, then restage."
144 echo "pre-commit: commit aborted (use --no-verify to bypass)."
145 exit 1
146 fi
147
148 echo "pre-commit: rustfmt gate clean."
149