Skip to content

Add extra deployment phase for destructive migrations#5509

Open
backspace wants to merge 10 commits into
mainfrom
deployment-stability-cs-12113
Open

Add extra deployment phase for destructive migrations#5509
backspace wants to merge 10 commits into
mainfrom
deployment-stability-cs-12113

Conversation

@backspace

@backspace backspace commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

We’ve had some production downtime on published sites because of destructive migrations having run while not-yet-updated components are expecting the old schema. This adds a migrations-removal directory for such migrations, which runs after the realm server has reached stability, when no components should still be relying on the old schema.

This includes a new skill and a CI job that tries to prevent destructive migrations being added to migrations directory, here it is failing on an example:

s 2026-07-17 at 14 42 41@2x

Migrations run before the app rolls out, so a destructive change to
boxel_index (a column/table drop or rename) takes effect while the
previous realm-server revision is still serving. During the rolling
deploy window that old code queries the now-missing column and every
published-realm serve 500s until the old tasks drain.

Split migrations into two phases:
  - migrations/          additive, backward-compatible; runs pre-deploy
                         (migrate-db), as before
  - migrations-removal/  destructive; runs post-deploy (migrate-db-remove),
                         gated on deploy-realm-server reaching stability so
                         the previous realm-server and worker task sets have
                         fully drained before any drop executes

A failure in the removal phase is non-fatal to serving: the new code no
longer reads the removed columns, so it is already live and healthy.

Each phase has its own directory and node-pg-migrate tracking table. The
gated one-shot-ECS-task orchestration is extracted to
.github/scripts/run-gated-migration-task.sh so both jobs stay in lockstep.
run-migrations.sh takes an optional (dir, table); the local/CI driver
migrate-local.sh runs both phases together (removal last on up, first on
down). Schema-filename derivation and validation, the migration linter, the
changed-migration detector, and the schema dump's excluded bookkeeping
tables all now account for both directories.

The already-applied drop-boxel-index-html-columns migration moves into the
removal phase; its up is idempotent, so re-running under the new tracking
table is a no-op where the columns are already gone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files      1 suites   3h 3m 34s ⏱️
3 458 tests 3 441 ✅ 15 💤 1 ❌ 1 🔥
3 477 runs  3 459 ✅ 15 💤 2 ❌ 1 🔥

Results for commit d064426.

For more details on these errors, see this check.

Realm Server Test Results

    1 files  ±    0      1 suites  +1   17m 54s ⏱️ + 17m 54s
1 880 tests +1 880  1 880 ✅ +1 880  0 💤 ±0  0 ❌ ±0 
1 959 runs  +1 959  1 959 ✅ +1 959  0 💤 ±0  0 ❌ ±0 

Results for commit d064426. ± Comparison against earlier commit c39a2e5.

@backspace
backspace marked this pull request as ready for review July 16, 2026 18:16

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9f7ec3bcf8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/postgres/scripts/migrate-local.sh Outdated
Comment thread packages/postgres/scripts/migrate-local.sh Outdated
backspace and others added 7 commits July 17, 2026 11:00
The migrate-local.sh dispatch ran every non-`down` action against both the
additive (migrations/) and removal (migrations-removal/) phases. For `create`
that scaffolded two files per invocation — the intended migrations/ file plus a
spurious migrations-removal/ one — so the documented `pnpm migrate create <name>`
no longer had a single unambiguous output.

Handle `create` explicitly: scaffold one file in migrations/. To author a removal
migration, create it this way and `git mv` it into migrations-removal/ (tracked
by filename, so moving a not-yet-applied file is clean). Apply actions (up, redo)
still run against both phases; `down` still rolls the removal phase back first.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The `down` branch always rolled the entire removal phase back, then applied
the caller's count to the additive phase — so a bare `pnpm migrate down`
reverted two migrations (all removal + one additive) instead of the single
latest, and it ignored the combined ordering (removal migrations aren't
necessarily the most recent once an additive migration is added and applied
after them). During local recovery that can silently revert unrelated
destructive changes.

Revert exactly the requested count (default 1) across the combined
newest-first timeline of both phases, using the tracking tables as the source
of truth (--no-check-order lets migrations apply out of filename order).
Consecutive same-phase entries coalesce into one `down K`, so a full-chain
rollback stays ~2 calls rather than one node-pg-migrate invocation per
migration. `determine-changed-migrations.sh` now counts both phases so CI's
full-chain down_count reverts the entire combined chain.

Verified locally: full-chain up/down/up (112+1 → 0 → 112+1, 2 down calls),
bare down reverts only the newest, and an interleave case (fresh additive
applied after the removal) reverts the additive and leaves the removal intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…plit

Make the two-phase migration system discoverable and hard to misuse:

- CI guard (scripts/check-removal-phase.cjs, "Guard removal-phase migrations"
  step): AST-parses newly added migrations/ files and fails if an up() drops or
  renames a column/table, pointing to migrations-removal/. Scoped to changed
  files (historical drops grandfathered) and to up() only, so an additive
  migration's down() reversing itself isn't flagged. Uses the TypeScript parser
  (added as a devDependency) rather than a regex so it can distinguish up() from
  down(). Heuristic — covers the rolling-deploy outage class (drop/rename), not
  NOT-NULL tightening or type narrowing.
- `pnpm migrate:create-removal <name>` scaffolds a destructive migration
  straight into migrations-removal/ (via a create-removal case in
  migrate-local.sh), so authoring one no longer needs a manual git mv.
- packages/postgres/README.md documents which directory a migration belongs in,
  how to create each kind, and the twin-table / already-applied-move gotchas.
- .claude/skills/postgres-migrations surfaces the same guidance to agents
  working on migrations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Describe the failure mode timelessly (a destructive change breaks the previous
code revision during a rolling deploy) rather than citing specific past dates or
counts, which don't help a future reader and rot. Affects the check-removal-phase
guard comment, the postgres README, and the postgres-migrations skill.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Intentionally adds a DROP-in-up() migration to migrations/ (should be in
migrations-removal/) so the "Guard removal-phase migrations" CI check fails.
Remove this commit/file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
determine-changed-migrations.sh emits a newline-separated file list. Inlining it
into the guard step's run script made each newline a command separator, so the
guard only received the first file and the rest ran as bogus shell commands. Feed
the list through the CHANGED_MIGRATIONS env var and have check-removal-phase.cjs
read and split it, so every changed migration is actually checked without relying
on shell word-splitting.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant