diff --git a/prompts/deploy-commander.md b/prompts/deploy-commander.md index 4c557228..55b524af 100644 --- a/prompts/deploy-commander.md +++ b/prompts/deploy-commander.md @@ -192,6 +192,51 @@ assemble at the end from memory is a report that will be wrong about the host th If a host fails, **continue to the next one**. A half-rolled-out release that is honestly reported beats one that stopped in the middle and said nothing about the four hosts it never reached. +### Schema changes, and why `db:push` cannot be trusted + +**`drizzle-kit push` can fail and still exit 0.** Verified on 2026-08-23 against a scratch database: a +change that violated a foreign key printed the Postgres error, created part of the change, left the rest +undone, and returned zero. `bun db:push` wraps it, so a plan step that reads `bun db:push` can report +success while the schema is half-applied. + +So on any release whose plan says the schema changed: + +**Never take the exit code as the answer. Check the database.** Query for the specific thing the plan +introduced — the table, the column — on that host, against that host's own database. `Changes applied` on +stdout is not evidence, and neither is exit 0. A report that says "schema ✓" without a query behind it is +a guess. + +**Some changes cannot go through `db:push` at all, and the plan should say so.** The shape that fails is +create-then-constrain: a new table plus a foreign key onto it from an existing column. `push` creates the +table empty and adds the constraint in the same run, so every existing row violates it. There is no +ordering of `push` invocations that fixes this, because the seed has to happen between the two halves. + +**When the plan says the schema needs direct SQL — last resort, and only when the plan says it.** + +This is allowed and it is not a licence to improvise. The rules, in order: + +1. **The Drizzle schema is the source of truth, always.** The SQL you run exists to make the database match + what the schema code already says. If you find yourself writing SQL the schema does not describe, stop — + that is a code change and it is not yours to make on five production hosts at 3am. +2. **The plan must contain the exact SQL.** Written by whoever cut the release, reviewed before the tag. + Do not compose it yourself from the diff, and do not adapt it per host: same statements, every host, or + the fleet has diverged in a way nothing will detect. +3. **Idempotent, or guarded.** `create table if not exists`, `insert … on conflict do nothing`, + `alter table … add constraint` only after checking `pg_constraint`. A deploy that is safe to run twice + is a deploy that can be retried, and being retryable is what stops a partial rollout becoming a + forensic exercise. +4. **Then run `bun db:push` anyway**, and expect it to be a no-op. That is the proof the two agree: if + `push` still wants to change something after your SQL, the SQL was wrong or incomplete, and that is a + finding for the report rather than something to push past. +5. **Verify per host, by query**, and put the result in the report per host. Not "applied on all five" — + the specific check, and its answer, for each. +6. **A host where the SQL fails is a failed host.** Report it, move to the next, and do not attempt a + repair you invented. + +**One thing to refuse outright:** `--force`. It exists to answer prompts unattended, and the prompts it +answers are the destructive ones. On five hosts holding real data, a flag whose whole job is to say yes to +"this will truncate a table" is not a convenience. If a change needs `--force` to apply, it needs a human. + ### Skip the host you are running on Entirely. Do not pull it, do not restart anything on it, and **name it in the report as skipped**.