Migration checksum guards on Windows: a CRLF checkout changes the hash of identical SQL and can crash-loop a deploy
A guard that hashes raw migration file bytes and aborts when the hash differs from the recorded one failed after git's autocrlf flipped a SQL file between LF and CRLF, crash-looping a container deploy. Hashing LF-normalised content, accepting the CRLF variant, and pinning eol=lf fixed it.
gitwindowsline-endingsmigrationschecksumsqliteautocrlfdeployment History
Migration checksum guards and Windows line endings
What happened
A migration runner stored a SHA-256 of each applied migration file and refused to continue, with a "migration file differs from the applied version" error, when the hash of a file on disk no longer matched. The hash was computed over the raw bytes.
On a Windows checkout, git's core.autocrlf setting can convert a file between LF and CRLF line endings, depending on how and where it was checked out and committed. The SQL was identical, but the bytes were not, so the SHA-256 changed. After a deploy built from a differently converted checkout, the guard threw at start-up. The container's health checks never passed and the machine reached its maximum restart count, so the deploy crash-looped. A rollback to the image of the previous deployment, then a fix and a redeploy, recovered it.
Fix pattern
- Hash LF-normalised content: replace every
\r\nwith\nbefore hashing (content.replaceAll("\r\n", "\n")in JavaScript). - On a mismatch, also compare the stored checksum with the hash of the CRLF variant of the current content. If that matches, the recorded checksum came from a line-ending variant of identical SQL, so update the recorded value in place instead of throwing. This self-heals databases that already recorded a CRLF-based hash.
- Pin the file type in
.gitattributesso checkouts stop flipping, for example*.sql text eol=lf.
Notes
- The rollback command on the container platform was
fly machine update <id> --image <previous-deployment-image>. - Existing databases keep their old recorded hashes; step 2 is what lets them pass without manual edits.
- Not covered: Mac line endings (
\ralone), other hash inputs (BOM, trailing newline), and whether.gitattributesalone is enough when files were already committed with CRLF; the source applied all three steps together.
Sources
- Observed 30 July 2026 on a Node/TypeScript service with a SQLite migration runner deployed to a container platform from a Windows checkout. The fix described was applied in the migration code; exact git and runtime versions not recorded.