ci(migrations): guardrail against table-locking DDL in new migrations (JEF-592) - #149
Merged
thejefflarson merged 1 commit intoJul 28, 2026
Conversation
… (JEF-592) Migration 0017 shipped a plain (non-CONCURRENTLY) CREATE INDEX on a 10.4M-row hot table with nothing in CI flagging it as risky. Add scripts/lint-migrations.sh, run as a new CI job, that fails on: non-CONCURRENTLY CREATE/DROP INDEX, ADD COLUMN ... NOT NULL DEFAULT (table rewrite), and a multi-statement file that also carries a `-- no-transaction` comment (sqlx only supports one statement per no-transaction migration). A `-- lock-ok:<reason>` comment escapes a genuinely safe case. Migrations at or below server/migrations/.locklint-baseline (17) are grandfathered without editing them -- sqlx checksums applied migrations, so touching 0017 to add an escape comment would crashloop prod. Only migrations added after the baseline are linted. Documented the rules and escape hatch in a new server/migrations/README.md (with a CLAUDE.md pointer), including where heavy/online DDL against spans/logs/metric_series_rollups should go instead (the JEF-580 online-DDL lane, ADR 0021). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
thejefflarson
deleted the
thejefflarson/jef-592-ci-guardrail-block-table-locking-ddl-in-migrations
branch
July 28, 2026 03:07
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Migration 0017 shipped a plain (non-
CONCURRENTLY)CREATE INDEXonmetric_series_rollups— at the time a 10.4M-row hot table — with nothing inCI flagging it as risky. This adds a CI lint that fails on that pattern (and a
couple of other table-locking-DDL shapes) in any newly-added migration.
scripts/lint-migrations.sh(POSIXsh+awk/grep/sed, nonew dependency) checks every
server/migrations/*.sqlfile for:CREATE INDEX/DROP INDEXwithoutCONCURRENTLY.ALTER TABLE ... ADD COLUMN ... NOT NULL DEFAULT ...(table rewrite).-- no-transactioncomment(sqlx only supports one statement per no-transaction migration).
Escape hatch: a
-- lock-ok:<reason>comment anywhere in the file allows it(e.g. an index on a table created earlier in the same migration, still
empty).
.github/workflows/ci.ymlgets amigrations-lintjob that runsthe script on every push/PR.
server/migrations/.locklint-baselinerecords17. Migrations at or belowthat version are skipped entirely — sqlx checksums applied migrations, so
editing 0017 to add a
-- lock-okcomment would crashloop prod on nextboot (the whole reason for this ticket). Only migrations added after the
baseline get linted; new migrations get no free pass.
server/migrations/README.mddocuments the rules, the escapehatch, the grandfather mechanism, the one-statement-per-
-- no-transaction-file rule, and points heavy/online DDL against
spans/logs/metric_series_rollupsat the JEF-580 online-DDL lane (ADR 0021) instead ofa boot migration.
CLAUDE.mdgets a one-line pointer to it.How the lint runs / grandfather mechanism
scripts/lint-migrations.sh [migrations-dir]— defaults toserver/migrations.For each
*.sqlfile with a numeric prefix strictly above.locklint-baseline's value, it strips--comments, splits on;, andchecks each statement against the three rules above (a per-file
-- lock-ok:comment short-circuits all three).Test plan
scripts/lint-migrations.sh --selftest— a self-contained fixture suite(no real bad migration ever committed) proving: a new plain
CREATE INDEXis flagged,
CONCURRENTLYis accepted,-- lock-ok:escapes a flaggedstatement,
ADD COLUMN ... NOT NULL DEFAULTis flagged, a nullableADD COLUMNis not, a 2-statement-- no-transactionfile is flagged, a1-statement one is not, and a migration at/below the baseline (which would
otherwise trip rule 1) is skipped. All 9 assertions pass.
scripts/lint-migrations.sh server/migrations— passes clean on thecurrent tree (0017 grandfathered, unedited).
0018_*.sqlwith a plain
CREATE INDEXor anADD COLUMN ... NOT NULL DEFAULTagainstthe current tree's baseline correctly fails with exit 1.
shellcheck -s sh scripts/lint-migrations.sh— clean.<<EOF $(...) EOFheredoc patternused to avoid a subshell-scoped loop variable) under both
bash's/bin/shand
dashdirectly.actionlint .github/workflows/ci.yml— same pre-existing findings as onmain(unknown custom runner label, shellcheck notes in the unrelatedimagejob); no new findings from this change.cargo/npmgates apply to this change.Closes JEF-592.
Co-Authored-By: Claude Sonnet 5 noreply@anthropic.com