Ships Brief 7 (documentation + verification runbook) of the
reconcile-historical-add-scripts convoy, ~2 months post-hoc. The
6 implementer briefs (B1-B6) landed 2026-06-14 to 2026-07-06 via
PRs #148, #149, #150, #151, #152, #153. This PR closes the loop:
- Brings the architect's parent convoy file + 6 brief files onto
main (they only existed on the stale convoy/reconcile-historical-
add-scripts branch, never merged)
- Adds § As-shipped to the parent convoy file documenting all 6
squash SHAs + PR numbers + merge dates + the reservation-timestamp
rename (1781000000001-006 → 1781442330001-006 in ec9bb2b, except
B3 which kept its original) + the B6 shipped-as-tiny-migration
deviation from the collapse-to-docs plan
- Fixes docs/SCHEMA_MAP.md § user_favorites (was stale
(user_id, card_id); actual polymorphic (item_type, item_id) per
B4's migration)
- Adds docs/MIGRATION_VERIFICATION_RUNBOOK.md — manual
fresh-Neon-branch vs prod pg_dump diff runbook per architect D5
- Flips .convoys/ship-readiness.md entries:
- reconcile-historical-add-scripts → RESOLVED
- retire-graveyard-scripts-after-audit → UNBLOCKED
- Adds two new queued follow-ups surfaced by the architect:
- unify-user-avatar-column (P3 — dual avatar column smell)
- drop-dead-cards-columns (P3 — cards.quantity + cards.favorited)
No source-code changes. Docs only.
Co-authored-by: Cursor <cursoragent@cursor.com>
149 lines
4.8 KiB
Markdown
149 lines
4.8 KiB
Markdown
# Migration verification runbook
|
|
|
|
How to confirm that a fresh Neon branch's migrated schema matches a
|
|
long-lived environment (prod, preview, or similar). The migration history
|
|
under `migrations/` is authoritative; this runbook is the manual check until
|
|
the queued `wire-migrate-into-ci` convoy automates it on every PR.
|
|
|
|
## When to run
|
|
|
|
- After any reconcile-adjacent convoy lands on `main`.
|
|
- Before releasing schema changes to prod when migration correctness is
|
|
uncertain.
|
|
- When suspecting drift between an env's actual schema and the migration
|
|
history (rare).
|
|
|
|
## Prerequisites
|
|
|
|
- A Neon account with permission to create and delete branches (or any
|
|
other way to spin up a fresh Postgres DB on the same major version as
|
|
prod).
|
|
- `POSTGRES_URL` for the branch under test — typically stored in a local
|
|
`.env.local.branch` or passed inline (never commit branch URLs).
|
|
- `ADMIN_INITIAL_PASSWORD` set (required by `npm run setup-db`; see
|
|
`AGENTS.md` § 5).
|
|
- `pg_dump` installed locally (PostgreSQL 16+), or available via a Docker
|
|
container with network access to Neon.
|
|
|
|
## Procedure
|
|
|
|
### 1. Create a throwaway Neon branch
|
|
|
|
```bash
|
|
neon branches create --name migrate-verify-YYYYMMDD --parent main
|
|
```
|
|
|
|
Or create an equivalent branch via the Neon console. Copy the branch
|
|
connection string into `POSTGRES_URL`.
|
|
|
|
### 2. Onboard the fresh branch via setup-db
|
|
|
|
```bash
|
|
POSTGRES_URL=<branch-url> \
|
|
ADMIN_INITIAL_PASSWORD=$(openssl rand -base64 24) \
|
|
npm run setup-db
|
|
```
|
|
|
|
`setup-db` runs `npm run migrate up` (every file in `migrations/` in
|
|
timestamp order) and seeds the admin user.
|
|
|
|
### 3. Snapshot the fresh branch schema
|
|
|
|
```bash
|
|
POSTGRES_URL=<branch-url> pg_dump --schema-only --no-owner --no-acl \
|
|
--schema=public > fresh-schema.sql
|
|
```
|
|
|
|
### 4. Snapshot prod schema (read-only)
|
|
|
|
```bash
|
|
POSTGRES_URL=<prod-url> pg_dump --schema-only --no-owner --no-acl \
|
|
--schema=public > prod-schema.sql
|
|
```
|
|
|
|
Use a read-only role or connection if your operator policy requires it.
|
|
Do not run DDL against prod during verification.
|
|
|
|
### 5. Diff
|
|
|
|
```bash
|
|
diff -u prod-schema.sql fresh-schema.sql
|
|
```
|
|
|
|
- **Empty output** — structural parity confirmed for tables, columns,
|
|
constraints, and indexes in `public`.
|
|
- **Non-empty output** — investigate before releasing; see below.
|
|
|
|
### 6. Delete the throwaway branch
|
|
|
|
```bash
|
|
neon branches delete migrate-verify-YYYYMMDD
|
|
```
|
|
|
|
## Known-safe diffs
|
|
|
|
These differences from `pg_dump` are **not** real schema drift:
|
|
|
|
- Schema owner / comment lines that differ between Neon projects.
|
|
- Extension version pins (`CREATE EXTENSION` version strings).
|
|
- `pgmigrations` row content — the fresh env accumulates rows as
|
|
migrations apply; prod may have the same rows with different apply
|
|
timestamps.
|
|
- Constraint or index **name** differences when prod objects were created
|
|
by historical `scripts/add-*` jobs with autogenerated names and fresh
|
|
envs use migration-defined names (material shape must still match).
|
|
- Column **order** within a table (historical ALTER order vs migration
|
|
order).
|
|
|
|
## When drift is detected
|
|
|
|
1. **Halt** — do not release until the delta is understood.
|
|
2. **Identify which env is canonical.** Usually prod. If prod is missing
|
|
a column that exists in the migration history, run
|
|
`POSTGRES_URL=<prod-url> npm run migrate up` to catch up.
|
|
3. **If prod has something not in the migration history**, file a new
|
|
convoy or brief describing the delta and ship a reconciliation
|
|
migration following the pattern in
|
|
`.convoys/reconcile-historical-add-scripts/`.
|
|
4. **Never edit existing migrations** — `pgmigrations` pins applied
|
|
files. Always add a new dated migration to correct schema.
|
|
|
|
## Supplementary spot-checks
|
|
|
|
For high-risk tables, compare column / constraint / index counts between
|
|
envs:
|
|
|
|
```sql
|
|
SELECT table_name, column_name, data_type, is_nullable, column_default
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'public'
|
|
ORDER BY table_name, ordinal_position;
|
|
|
|
SELECT table_name, constraint_name, constraint_type
|
|
FROM information_schema.table_constraints
|
|
WHERE table_schema = 'public'
|
|
ORDER BY table_name, constraint_name;
|
|
|
|
SELECT tablename, indexname, indexdef
|
|
FROM pg_indexes
|
|
WHERE schemaname = 'public'
|
|
ORDER BY tablename, indexname;
|
|
```
|
|
|
|
A mismatch in column count, constraint count, or index count is material
|
|
drift.
|
|
|
|
## Automation path
|
|
|
|
The queued `wire-migrate-into-ci` convoy (see `.convoys/ship-readiness.md`
|
|
§ Queued convoys) will run `npm run migrate up` against a test DB on every
|
|
PR. Until that ships, this operator runbook is the only automated-parity
|
|
signal.
|
|
|
|
## Cross-references
|
|
|
|
- `AGENTS.md` § 4 Gotcha #6 — migration tool adoption history.
|
|
- `.convoys/migration-tool.md` — `node-pg-migrate` adoption convoy.
|
|
- `.convoys/reconcile-historical-add-scripts.md` § As-shipped — the convoy
|
|
that folded historical add-script DDL into `migrations/`.
|
|
- `.cursor/rules/db-and-schema.mdc` — schema-change conventions.
|