From 6bf96d77c3e71a41952a8ec4a8179681187dec93 Mon Sep 17 00:00:00 2001 From: Lewis Carhart Date: Fri, 7 Aug 2026 15:36:20 -0400 Subject: [PATCH] fix(api): warn when the deployed schema does not match schema.prisma --- apps/api/scripts/build-func.mjs | 47 ++++++++++++++++++++++++++++++--- docs/setup.md | 22 +++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/apps/api/scripts/build-func.mjs b/apps/api/scripts/build-func.mjs index 25740c26..ea6e1583 100644 --- a/apps/api/scripts/build-func.mjs +++ b/apps/api/scripts/build-func.mjs @@ -1,4 +1,4 @@ -import { execSync } from "node:child_process"; +import { execSync, spawnSync } from "node:child_process"; import { cpSync, existsSync, @@ -182,11 +182,52 @@ if (!process.env.VERCEL) { } else if (!directDatabaseUrl) { console.log("• no database URL at build time — skipping migrations"); } else { + const dbDir = join(repoRoot, "packages/db"); + const dbEnv = { ...process.env, DATABASE_URL: directDatabaseUrl }; + console.log("• applying migrations (prisma migrate deploy)..."); execSync(`${bun} x prisma migrate deploy`, { - cwd: join(repoRoot, "packages/db"), + cwd: dbDir, stdio: "inherit", - env: { ...process.env, DATABASE_URL: directDatabaseUrl }, + env: dbEnv, }); console.log("✓ migrations applied"); + + console.log("• checking the deployed schema against schema.prisma..."); + const drift = spawnSync( + bun, + [ + "x", + "prisma", + "migrate", + "diff", + "--from-config-datasource", + "--to-schema", + join("prisma", "schema.prisma"), + "--exit-code", + ], + { cwd: dbDir, encoding: "utf8", env: dbEnv }, + ); + + if (drift.status === 0) { + console.log("✓ schema matches"); + } else if (drift.status === 2) { + console.log(""); + console.log("!! THE PRODUCTION SCHEMA DOES NOT MATCH schema.prisma !!"); + console.log( + " Every migration is recorded as applied, so `migrate deploy` will keep reporting", + ); + console.log( + " nothing pending while queries fail on columns that are not there. Reconcile with", + ); + console.log( + " `prisma migrate diff --from-config-datasource --to-schema prisma/schema.prisma --script`.", + ); + console.log(""); + console.log(drift.stdout || ""); + } else { + console.log( + `• could not compare the schema (${drift.stderr?.trim() || "unknown error"})`, + ); + } } diff --git a/docs/setup.md b/docs/setup.md index a3e398b8..e953fcc0 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -113,6 +113,28 @@ builds, and the pages that touch them fail. Test schema changes locally, where worse: every preview applied its own migrations to the production database, so on 2026-08-07 the live schema ran six migrations ahead of the live code all day. +### `migrate deploy` is not proof the schema is right + +The build follows the deploy with `prisma migrate diff --exit-code` against +`schema.prisma` and shouts in the build log when they disagree. **`No pending +migrations to apply` only means `_prisma_migrations` has a row for every file** — +it says nothing about what the tables actually look like. + +They came apart once. A `prisma db push` shaped production from a laptop, the +migration rows were recorded as applied without their SQL ever running, and +`agentConversationAttachment` went live without its `position` column. Every deploy +reported nothing pending, for days, while `conversations.builderById` returned 500. +The tell is an object in the database that no migration defines — there was an +`agentConversationAttachment_submissionId_createdAt_idx` that appears in no +migration file, only in a `db push` of an older schema. + +Reconciling is one command, and it is worth reading before running: + +```sh +DATABASE_URL="…" bunx prisma migrate diff \ + --from-config-datasource --to-schema prisma/schema.prisma --script +``` + ## Secrets hygiene `.gitignore` ignores `.env` and `.env.*` with one negation for `.env.example`, so