Repository navigation
fix(db): adopt prisma migrate with baseline + drift CI guard (#233) - #577
Conversation
…y-Labs#233) Replace `prisma db push` with a committed migration history so schema changes are reviewable, deploys are reproducible, and a bad change is recoverable. - Baseline migration 0_init reproduces the current production schema exactly (generated via `migrate diff --from-empty --to-schema-datamodel`). - CI job `db-migrations` replays every migration against a real Postgres and fails on drift (`migrate diff ... --exit-code`), and proves a new migration can be generated, applied and rolled back on a clean DB. - DB-free vitest test locks schema.prisma to the baseline; it fails while the repo has no migration history and on un-migrated schema edits. - App startup and the load workflow now run `migrate deploy`; `db push` is removed as a script and called out as unsafe in docs/database-migrations.md.
|
@Neziahtech Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
tsc --noEmit clean vitest 102 files, 1157 passing #563 extend the env drift guard to examples and scripts #568 let callers read their own usage #571 zero-downtime admin key rotation #578 pin the connect-time address to defeat DNS rebinding #563 earned itself immediately. Extending the guard beyond src/ caught two variables read by the live-spec validator that were never documented: LIVE_API_URL (scripts/validate-live-spec.ts:15) LENS_URL (scripts/validate-live-spec.ts:15) That script came in with #318, which I merged earlier today, so this is a gap I let through two batches ago. Both are now in .env.example with a note on the precedence between them. Its own test case was flagged by the dropped-test guard and turned out to be split rather than deleted -- "fails when a new un-documented process.env read is encountered" became one case for src/ and one for examples/. Third time today that guard surfaced a rename; it has yet to find a real deletion, which is the outcome you want from it. #578 closes the SSRF rebinding hole: resolving a host, checking it, then letting the HTTP client resolve it again leaves a window where the second answer differs from the one that passed the check. Pinning the connect-time address removes the window rather than narrowing it. Two from this group were NOT merged, both failing on their own branch: #572 (capability scopes per route) 1 type error alone: src/api/auth.ts(165): Property 'scope' does not exist on type '{ public?: boolean | undefined; }' #577 (prisma migrate with a baseline) 1 failing test alone -- its own: "has an init/baseline migration that reproduces schema.prisma exactly" #573 #574 #575 #576 conflict. Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
Miracle656
left a comment
There was a problem hiding this comment.
Fails one test on its own branch — and it is this PR's own test:
× #233 Prisma migration history replaces db push > has an init/baseline migration that reproduces schema.prisma exactly
tsc --noEmit is clean and the other 1149 tests pass, so the baseline in prisma/migrations/0_init/migration.sql does not reproduce schema.prisma exactly. Most likely something landed in the schema after you generated it — several PRs added models today, including a WebhookDeliveryFailure in #388.
Worth getting exactly right rather than close. This repo has been bitten by prisma db push ignoring committed migrations and half-applying a schema, and a baseline that does not match is how that recurs: the migration history claims to describe the database and quietly does not.
Regenerate the baseline against current schema.prisma, confirm your own test passes, and re-request. The change itself — replacing db push with a real migration history — is one I want.
…loses #233) Replaces `prisma db push` with a committed migration history: a generated baseline in `prisma/migrations/0_init`, a `db-migrations` CI job that replays every migration against a real Postgres and fails on drift from `schema.prisma`, a DB-free test that locks the baseline to the schema, and `migrate deploy` at startup and in the load workflow. Four changes on top of the contribution. 1. **Startup would have crash-looped on every existing database.** Every Lens database was created by `db push`, so it has the full schema and no `_prisma_migrations` table. Prisma's baselining guidance is explicit about what `migrate deploy` then does: it runs the baseline's `CREATE TABLE`s against tables that are already there, and errors. `main()` ends in `process.exit(1)`, so that is a crash loop, not a failed migration — and the PR's own doc put the fix (`migrate resolve --applied 0_init`) in a runbook, which means the deploy survives or not depending on whether a human ran a command first. `applyMigrations()` in the new `src/db/migrate.ts` does the adoption in code: one `to_regclass` query, and when the schema is populated without a history it records the baseline as applied without executing it, then deploys. The three states — empty, pre-migrate, already adopted — are pinned by `tests/db-migrate-baseline.test.ts`, which also pins that `resolve` runs before `deploy` and exactly once. 2. **The baseline had drifted.** `main` has gained `webhook_delivery_failures` and `oracle_publications` since it was generated, and the PR's own drift test caught it. Regenerated. 3. **Dropping `db push` means drift is no longer self-healed, and that is not written down anywhere.** `api_keys.expires_at` is in `schema.prisma` and in the baseline; if it is missing from a live database, adoption records the baseline as applied and `migrate deploy` has nothing to apply, so the column stays missing — and `assertSchemaValid()` only covers the five core tables in `EXPECTED_SCHEMA`, so nothing fails at boot. It surfaces as 500s on the routes that read it. `docs/database-migrations.md` now carries the one-time `migrate diff --from-schema-datasource` check to run against each live database before the first deploy on this commit, and says why that check is deliberately not wired into startup (Lens shares a Postgres instance with wraith; a false positive there would be a refusal to boot). 4. **The operational docs still described the old boot.** `docs/RUNBOOK.md` listed `db push --accept-data-loss` as boot step 1 and told an operator to roll a schema back by running `prisma db push` from the old checkout — a command that no longer exists and that would now leave the database and the migration history disagreeing. `docs/database-schema.md` still opened with "Current State: `prisma db push`", and `docs/sep40-oracle-publisher.md` pointed at the deleted `npm run db:push`. All updated. Also pinned `*.sql` to LF in `.gitattributes`, alongside the four generated artifacts that already needed it. 123 test files / 1,472 tests pass; tsc and build clean. Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
|
Merged as Your drift test also earned its keep immediately: it failed on merge because Four things I changed on the way in. 1. Startup would have crash-looped on every database we have (blocking, fixed)This is the one that mattered. Every Lens database that exists was created by From main().catch(err => {
log.error({ err }, 'fatal startup error')
process.exit(1)
})So: Your own doc has the fix ( So I moved the adoption into code, in a new export async function applyMigrations(options: ApplyMigrationsOptions): Promise<void> {
const baseline = options.baseline === undefined ? baselineMigration() : options.baseline
if (baseline) {
const state = await readMigrationState(db)
if (needsBaselineAdoption(state)) {
warn?.({ baseline }, 'database predates the migration history; recording the baseline as applied without running it')
run(`${PRISMA_CLI} migrate resolve --applied ${baseline}`)
}
}
run(`${PRISMA_CLI} migrate deploy`)
}One query decides it: select to_regclass('_prisma_migrations') is not null as has_history,
to_regclass('price_points') is not null as has_schemaThree states, all three now pinned by
A few details that are deliberate rather than incidental:
2. Dropping
|
…500) Stamps `api_keys.last_used_at` on each successful authentication, throttled to one write per key per `KEY_LAST_USED_THROTTLE_SECONDS` (default 60) so a busy key does not turn a read-only auth check into a write per request, and surfaces it on `GET /admin/keys`. The in-memory window is claimed before the await, so concurrent requests for one key do not each decide to write. Merged locally. The branch predated the `buildApp()`/admin-auth work and four files conflicted, including the whole `/admin/keys` handler, so I took `main` for those and re-applied only the #500 parts. In particular I did **not** take the PR's pagination of `/admin/keys` — that is issue #505, assigned to someone else — only `lastUsedAt` on the existing listing. Three things on top of the contribution. 1. **There was no migration, so the column would never have reached a live database.** This is the first schema change since #577 landed this morning: startup runs `prisma migrate deploy`, not `db push`, so a bare `schema.prisma` edit no longer converges anything. The baseline is already recorded as applied, nothing would have been pending, and every `lastUsedAt` write would have failed against a table with no such column — the feature silently dead, exactly the hazard `docs/database-migrations.md` warns about. Added `prisma/migrations/20261023120000_api_key_last_used_at/migration.sql`. 2. **Regenerating the baseline instead would have broken a fresh database.** My first attempt regenerated `0_init` from the new schema *and* added the ALTER, which collide: `0_init` creates `last_used_at`, then the ALTER adds it again. The baseline stays behind `schema.prisma` on purpose — the later migration carries the difference. That means #577's DB-free drift test was wrong the moment a second migration existed: it asserted the baseline reproduces `schema.prisma` exactly, which would have demanded the very regeneration that causes the collision. It now makes that comparison only while the baseline is the whole history, defers to the DB-backed replay in the `db-migrations` CI job once there is more, and gained a case asserting no migration sorts before the baseline (Prisma replays lexicographically, so one that did would run against an empty database). 3. **The failed write was swallowed by a bare `catch {}`.** Not refusing a valid authenticated request because an audit timestamp could not be stored is right; saying nothing is not, and after (1) a missing column is a real possibility. It is now logged through the house logger — which redacts secrets — once per key, so one bad environment is one line, not one per throttle window. Also moved the `recordApiKeyLastUsed` call to sit immediately after the key is accepted, before the scope and daily-quota checks: "last used" should mean "last authenticated", not "last allowed through", or a key sitting at its quota looks abandoned. And `resetApiKeyLastUsedThrottle()` now runs in `beforeEach` — the throttle map is module state, and a window claimed by one test silently throttled the next one's write. 126 test files / 1,499 tests pass; tsc clean. Claude-Session: https://claude.ai/code/session_01USgemLt4Rnz4SGB1Srf3GB
Closes #233
What & why (#233)
prisma/shipped onlyschema.prisma— nomigrations/, so every schema change went throughprisma db push, which diffs the live DB and applies whatever it decides. Nothing was reviewable, reproducible, or roll-backable. This adoptsprisma migrate.Acceptance criteria
prisma/migrations/0_init/migration.sql, generated viamigrate diff --from-empty --to-schema-datamodel --script. Verified byte-for-byte against a regenerated diff.db-migrationsjob boots Postgres and runsscripts/db-drift-check.sh(migrate diff … --exit-code), plusscripts/db-migration-cycle.shwhich also proves the guard fires on an un-migrated schema edit.migrate deploy;db pushcalled out as unsafe — newdocs/database-migrations.md; README / CONTRIBUTING / backup-restore / load.yml updated; startupsrc/index.tsnow runsmigrate deploy;db:pushremoved.scripts/db-migration-cycle.sh(reset → generate canary migration → deploy → assert table → reset → assert gone).Proven with a test that fails first
tests/prisma-migrations.test.tsis DB-free and locksschema.prismato the baseline. Ran both ways: withprisma/migrationshidden it fails (4/4); with the baseline present it passes (4/4).Scope notes
Baseline generated from
main(production shape); it does not include the un-merged #237 unique index — that PR should ship its own migration. No unrelated files touched.Not verified locally
The DB-backed drift + generate/apply/rollback jobs need a live Postgres; the docker daemon was unreachable on my box. Their logic and
bash -nare validated and they run in CI.