fix(api): make the closed 2026-08-06 migration block fail at authoring time (#3016) - #3193
Conversation
…g time (#3016) Three separate authors reached for `2026-08-06-g-` in one day (#2995 merged and reddened main for ~1h, #3008 was caught on the PR, and the m365-comms plan doc instructed its executor to do the same). None was being careless: the documented same-day-ordering convention is literally "add an `-a-`/`-b-` infix", so taking the next free letter is the natural reading. Nothing at authoring time said the letters stopped at `-f-`. Discoverability: - New `scripts/check-migration-naming.sh`, wired into `.githooks/pre-commit` (staged-added files) and the `check-migrations` CI job (whole directory) as the non-bypassable backstop. Rejects additions to the closed block, and separately rejects filenames that miss the runner's `^\d{4}-.*\.sql$` discovery pattern — those are not rejected today, they are silently never applied. - New `apps/api/migrations/README.md`: naming, the closed-block table, idempotency, no inner BEGIN/COMMIT, immutability, and a new-migration checklist — sitting in the directory you are already in when authoring. - CLAUDE.md documents the closed block next to the same-day infix guidance that implied the letters were free-running, and notes that the infixes are per-date rather than a global sequence. Diagnosability: - The reserved-block property moves out of three wave-ordering tests (whose names pointed at the wrong wave and whose only output was `expected [ Array(1) ] to deeply equal []`) into one `migration filename conventions` suite whose test name states the rule and whose message names the offending file and the fix. - The `-a..-f` slot regex becomes an explicit frozen manifest. The regex was wrong in both directions: it blessed `2026-08-06-a-anything-unrelated.sql`, and it read as an open range. A manifest also catches deletion/rename of a shipped block member. - New assertion that every `migrations/<file>.sql` path referenced from `apps/api/src` resolves. Integration suites replay migrations BY PATH, so the #3015 rename surfaced as an ENOENT several minutes into Integration shard 3/4, well after Test API had gone green; this moves it into the unit job. Also renames the m365-comms plan doc's three planned migrations off the closed block (`2026-08-06-g/h/i-` -> `2026-08-14-a/b/c-`) and corrects its rationale to "sort last via a later DATE, not a later letter", which is the property that plan actually needs. Deliberately NOT changed: the comment in `apps/api/migrations/2026-08-09-backup-jobs-snapshot-id-index.sql` still names the removed `RESERVED_SLOT_PATTERN`. Editing a migration to fix a comment is exactly the #2708 incident class; its substance is still correct. Closes #3016 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Deploying breeze with
|
| Latest commit: |
f429b70
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://764241dd.breeze-9te.pages.dev |
| Branch Preview URL: | https://fix-3016-reserved-migration.breeze-9te.pages.dev |
…framing Findings from /pr-review-toolkit:review-pr (code-reviewer, pr-test-analyzer, comment-analyzer, silent-failure-hunter): 1. The .sh/.ts manifest drift guard was near-vacuous (all three code reviewers). It asserted only that the script contained the date substring; code-reviewer proved a bogus 9th entry injected into the shell manifest still passed. It now parses the bash array and asserts element-for-element equality with RESERVED_BLOCK_MIGRATIONS, and pins RESERVED_DATE by anchored regex. Re-ran that exact repro: now fails. 2. Whole-directory mode passed vacuously on an empty/missing migrations directory — the unmatched glob expanded to the literal pattern and `[ -f ]` skipped it, so the guard reported OK having checked nothing. That is the precise failure class this PR exists to prevent. Now uses nullglob and hard-fails on a zero-file directory. 3. `git diff --cached` failure inside a process substitution was invisible to `set -e`, so a git error yielded an empty file list and a silent pass. Captured into a variable with an explicit failure branch. 4. The guard script's failure path was never exercised — CI only ever ran it against a clean tree, proving the pass branch alone, and the negative testing was manual. Added a test driving the script via spawnSync against a fixture directory (BREEZE_MIGRATIONS_DIR, an override nothing in CI or the hook sets): asserts exit 1 + the offending filename in stderr for a squatter and for an undiscoverable filename, and exit 1 for the empty directory. 5. "One migration per wave" / "the eight security-remediation wave migrations" was false for 2 of the 8, and directly contradicted by this same test file's Wave 6 comments — `-e-action-intents-origin- principal` and `-f-m365-comms-delegated` are unrelated same-day work. Corrected in all four places (test, script, README, CLAUDE.md); the block is closed for its inter-file ordering dependencies, which is the accurate reason and does not depend on the false framing. Also dropped "later migrations ALREADY re-create objects it defines" — no shipped later migration does; the known case is the (still unwritten) m365 plan-doc migration, so it is now stated prospectively. 6. The reference scan excluded all of `apps/api/src/extensions/`, which skipped a real core-migration citation in extensions/stateStore.ts. Narrowed to extension *.test.ts files only, which are the ones using synthetic fixture names. Documented that the scan covers comments too — a comment citing a deleted migration is rot as well. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Review run: Findings: 6 raised → all 6 addressed in f429b70; 0 outstanding.
Tests: CI: the Status: review-clean, awaiting |
Closes #3016
Three separate authors reached for
2026-08-06-g-in one day — #2995 (merged, reddenedmain~1h, fixed by #3015), #3008 (caught on the PR), and the m365-comms plan doc, which instructs its executor to do the same. None was being careless: the documented same-day-ordering convention is literally "insert an explicit-a-/-b-infix", so reaching for the next free letter is the natural reading. Nothing at authoring time said the letters stopped at-f-.Two problems, addressed separately.
1. Undiscoverable at authoring time
scripts/check-migration-naming.sh^\d{4}-.*\.sql$discovery pattern — today those aren't rejected, they're silently never applied..githooks/pre-commit.github/workflows/ci.yml(check-migrations)--no-verify.apps/api/migrations/README.mdBEGIN;/COMMIT;, immutability + the rename/reference-sweep trap, and a new-migration checklist — in the directory you're already in when authoring.CLAUDE.mdThe guidance everywhere is the same: if you need to sort after everything shipped, use a later DATE, not a later letter. Today's date already sorts last — that's the property authors actually want, and it collides with nothing.
2. The guard didn't name the offending file
Before, a squatter failed three wave-ordering tests, each named for a wave rather than the problem, all emitting
AssertionError: expected [ Array(1) ] to deeply equal []. Both incidents needed a manual bisect (delete each new migration, re-run) to identify the culprit.Now it fails exactly one test, named for the rule:
Regex → frozen manifest (issue suggestion 4, cheap version).
RESERVED_SLOT_PATTERN = /^2026-08-06-[a-f]-/was wrong in both directions: it silently blessed2026-08-06-a-something-unrelated.sql, and it read as an open range with-g-free for the taking. The block is now closed and fully shipped, so the honest expression is an explicit list of its eight filenames. It also catches the reverse — a shipped block member deleted or renamed.3. Companion assertion (from the issue thread)
resolves every core migration path referenced from apps/api/src— integration suites replay migrations by path, so a filename is executable code, not prose. In #3015 the rename was easy but the reference sweep wasn't: it surfaced as anENOENTin Integration shard 3/4, minutes in, well afterTest APIhad gone green. Verified by renaming2026-08-08-drop-custom-alert-conditions.sqllocally:apps/api/src/extensions/is excluded — extensions ship their own per-bundle migrations dirs and their tests use synthetic in-memory filenames.4. The queued third instance
docs/superpowers/plans/integrations/2026-07-30-m365-comms-3-api-integration.mdtold its executor to name migrations2026-08-06-g-/-h-/-i-. Renamed to2026-08-14-a-/-b-/-c-and the rationale corrected: the plan needs sort-last, which a later date gives it. Its "re-check at execution time" instruction is kept, but now says move the date rather than bump the letters.Deliberately not changed
apps/api/migrations/2026-08-09-backup-jobs-snapshot-id-index.sqlhas a comment naming the now-removedRESERVED_SLOT_PATTERN. Editing a migration to fix a comment is precisely the #2708 incident class, and the comment's substance ("don't put it in the 2026-08-06 block") is still correct.Issue suggestion 4's larger form — replacing date-based reservation with a dedicated prefix — isn't needed: the block is closed and no future block is planned. If one ever is, the manifest shape generalizes.
Verification
vitest run src/db/autoMigrate.test.ts— 59 passed (56 before + 3 new).tsc --noEmiton@breeze/api— clean.check-migration-naming.sh— OK in both modes on the current tree; fires correctly on a planted2026-08-06-g-*.sqland a plantednodigits-bad.sql.check-migration-immutability.sh— OK (no shipped migration touched).No migrations added or edited; no runtime code touched.
🤖 Generated with Claude Code