Skip to content

fix(api): make the closed 2026-08-06 migration block fail at authoring time (#3016) - #3193

Merged
ToddHebebrand merged 2 commits into
mainfrom
fix/3016-reserved-migration-block
Aug 7, 2026
Merged

fix(api): make the closed 2026-08-06 migration block fail at authoring time (#3016)#3193
ToddHebebrand merged 2 commits into
mainfrom
fix/3016-reserved-migration-block

Conversation

@ToddHebebrand

Copy link
Copy Markdown
Collaborator

Closes #3016

Three separate authors reached for 2026-08-06-g- in one day — #2995 (merged, reddened main ~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

Added What it does
scripts/check-migration-naming.sh Rejects additions to the closed block. Also rejects filenames that miss the runner's ^\d{4}-.*\.sql$ discovery pattern — today those aren't rejected, they're silently never applied.
.githooks/pre-commit Runs the guard on staged-added files, alongside the existing confidential scan. This is where an author should hit it.
.github/workflows/ci.yml (check-migrations) Whole-directory run as the non-bypassable backstop for --no-verify.
apps/api/migrations/README.md Naming, closed-block table, idempotency, no inner BEGIN;/COMMIT;, immutability + the rename/reference-sweep trap, and a new-migration checklist — in the directory you're already in when authoring.
CLAUDE.md Documents the closed block right next to the same-day infix guidance that implied the letters were free-running, and states that infixes are per-date, not a global sequence.

The 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:

FAIL  src/db/autoMigrate.test.ts > migration filename conventions >
      adds no new migration to the closed 2026-08-06 reserved block
AssertionError: Migration(s) added to the CLOSED reserved 2026-08-06- block:
  - 2026-08-06-g-drop-custom-alert-conditions.sql

That date is not a free namespace and its letters do NOT run past the
shipped set. Rename the file to a date AFTER the block ... and update every
reference to the old path — integration tests replay migrations BY PATH, so
a stale name is an ENOENT in Integration Tests, not a compile error.
See apps/api/migrations/README.md.

Regex → frozen manifest (issue suggestion 4, cheap version). RESERVED_SLOT_PATTERN = /^2026-08-06-[a-f]-/ was wrong in both directions: it silently blessed 2026-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 an ENOENT in Integration shard 3/4, minutes in, well after Test API had gone green. Verified by renaming 2026-08-08-drop-custom-alert-conditions.sql locally:

AssertionError: Migration path(s) referenced from apps/api/src that do not exist:
  - 2026-08-08-drop-custom-alert-conditions.sql (referenced from
    apps/api/src/__tests__/integration/customAlertConditionCleanup.integration.test.ts)

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.md told its executor to name migrations 2026-08-06-g-/-h-/-i-. Renamed to 2026-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.sql has a comment naming the now-removed RESERVED_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.ts59 passed (56 before + 3 new).
  • Negative-tested both new guards: squatter file → 1 named failure; simulated rename → dangling-reference failure naming both files. Reverted after.
  • tsc --noEmit on @breeze/api — clean.
  • check-migration-naming.sh — OK in both modes on the current tree; fires correctly on a planted 2026-08-06-g-*.sql and a planted nodigits-bad.sql.
  • check-migration-immutability.sh — OK (no shipped migration touched).
  • Pre-commit hook exercised live by this commit.

No migrations added or edited; no runtime code touched.

🤖 Generated with Claude Code

…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>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 6, 2026

Copy link
Copy Markdown

Deploying breeze with  Cloudflare Pages  Cloudflare Pages

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

View logs

…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>
@ToddHebebrand

Copy link
Copy Markdown
Collaborator Author

Review run: /pr-review-toolkit:review-pr — code-reviewer, pr-test-analyzer, comment-analyzer, silent-failure-hunter (4 agents, parallel).

Findings: 6 raised → all 6 addressed in f429b70; 0 outstanding.

  1. Manifest drift guard was near-vacuous (flagged independently by all three code reviewers). keeps the commit-time naming guard pinned to the same reserved date only asserted the .sh contained the date substring — code-reviewer proved a bogus 9th entry injected into the shell manifest still passed. Now parses the bash array and asserts element-for-element equality with RESERVED_BLOCK_MIGRATIONS, plus an anchored regex on RESERVED_DATE. Re-ran that exact repro: it now fails as it should.
  2. Whole-directory mode passed vacuously on an empty directory — without nullglob the unmatched glob expands to the literal pattern, [ -f ] skips it, and the guard printed OK having inspected nothing. That is precisely the failure class this PR exists to prevent. Now nullglob + hard-fail on zero files.
  3. git diff --cached failure was invisible to set -e inside the process substitution, yielding an empty file list and a silent pass. Captured into a variable with an explicit failure branch.
  4. The guard's failure path was never exercised — CI only ever ran it against a clean tree, proving the pass branch alone; the negative testing was manual. Added a test driving the script via spawnSync against a fixture dir (BREEZE_MIGRATIONS_DIR, an override nothing in CI or the hook sets), asserting exit 1 + the offending filename in stderr for a squatter and 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 8 — and contradicted by this same test file's own Wave 6 comments: -e-action-intents-origin-principal and -f-m365-comms-delegated are unrelated same-day work. Corrected in all four places. The block is closed for its inter-file ordering dependencies, which is both accurate and sufficient. 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, now stated prospectively.
  6. Reference scan over-excluded apps/api/src/extensions/, skipping a real core-migration citation in extensions/stateStore.ts. Narrowed to extension *.test.ts only (the files using synthetic fixture names).

Tests: vitest run src/db/autoMigrate.test.ts60 passed (56 on main + 4 new). tsc --noEmit on @breeze/api — clean. check-migration-naming.sh OK in both modes; check-migration-immutability.sh OK (no shipped migration touched).

CI: the pull_request trigger dropped for this branch — gh pr checks showed only a Cloudflare Pages entry, which reads deceptively green. Dispatched CI by hand: run 31127084687. Check Migrations — the job carrying this PR's new guard step — passed, along with Lint, Type Check, Check Migrations (non-superuser), and Lint Agent (Go). Test API (which carries autoMigrate.test.ts) was still queued behind the repo-wide runner backlog at the time of writing; it needs a look before merge, though that suite is green locally.

Status: review-clean, awaiting Test API on the dispatched run + maintainer merge.

@ToddHebebrand
ToddHebebrand merged commit 35e2775 into main Aug 7, 2026
100 checks passed
@ToddHebebrand
ToddHebebrand deleted the fix/3016-reserved-migration-block branch August 7, 2026 15:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant