Skip to main content

max / makenotwork

Block edits to already-committed migrations in the pre-commit hook The exorcise sweep rewrote comments in 29 applied migrations and nothing caught it: cargo check and cargo test both pass, because nothing in the suite checksums a migration. It surfaced only as a failed migration_dry_run three days later, having blocked every deploy in between. Refuse a staged modify/rename/delete of any migration already in HEAD. Adding one is untouched. --no-verify is the escape hatch for a migration that has genuinely never been applied anywhere.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-07-30 21:57 UTC
Signed with PGP, not checked
Commit: 11f7db32bef623ea17240706b8b29b2a36aa648b
Parent: 1c6b695
1 file changed, +26 insertions, -0 deletions
@@ -40,6 +40,32 @@
40 40 echo "pre-commit: gitleaks not installed; skipping secret scan (astra gates on push)."
41 41 fi
42 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 +
43 69 # Paths the gate ignores (extended regex, matched against repo-relative paths).
44 70 # Empty means check everything.
45 71 SKIP_PATHS="${SKIP_PATHS:-}"