From 1ad0f28f48446335ca34067cc59ab71bb76b8fa9 Mon Sep 17 00:00:00 2001 From: Samraaj Bath Date: Mon, 6 Jul 2026 23:20:59 -0700 Subject: [PATCH] Run db:migrate automatically before every Railway deploy The 2026-07-06 prod outage was a missed manual migration: the 0002 job_events migration never ran against prod, so the worker crash-looped on every job. Make the deploy pipeline own it: - railway.json sets `npm run db:migrate` as the pre-deploy command. Railway runs it with the service's env before starting the new deployment; a failed migration fails the deploy and the old version keeps serving. - runMigrations() now serializes on a Postgres advisory lock so api and worker pre-deploys firing off the same push can't race Drizzle's journal writes. - DEPLOY.md: document the automatic path; keep the manual command for first-time setup. Co-Authored-By: Claude Fable 5 --- docs/DEPLOY.md | 16 +++++++++++----- packages/db/src/migrate.ts | 18 ++++++++++++++++-- railway.json | 6 ++++++ 3 files changed, 33 insertions(+), 7 deletions(-) create mode 100644 railway.json diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 4cae894..b861622 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -12,14 +12,20 @@ Postgres and S3-compatible storage. The recommended OSS-friendly stack: ## 1. Database (Neon) 1. Create a Neon project; copy the pooled connection string. -2. Apply migrations (from CI/CD or locally): +2. Migrations run **automatically before every Railway deploy**: the repo's + `railway.json` sets `npm run db:migrate` as the pre-deploy command, which + Railway executes (with the service's env, so `DATABASE_URL` is present) + before starting the new deployment. Re-running is a no-op for anything + already applied, and concurrent runs (api + worker deploying off the same + push) serialize on a Postgres advisory lock. A failed migration fails the + deploy — the old version keeps serving. + + To apply migrations manually (first-time setup, or outside Railway): ```bash DATABASE_URL='postgres://...neon.../ditto_site?sslmode=require' npm run db:migrate ``` - Run this on every deploy that changes `packages/db/migrations`. Drizzle tracks - applied migrations in the database, so re-running is a no-op for anything - already applied. (Alternatively, apply a single migration by piping its SQL - through `psql`/the Neon CLI — but prefer `db:migrate`, which also records the + (Alternatively, apply a single migration by piping its SQL through + `psql`/the Neon CLI — but prefer `db:migrate`, which also records the journal entry so future runs stay consistent.) Current migrations: `0000` core tables, `0001` signup tokens, diff --git a/packages/db/src/migrate.ts b/packages/db/src/migrate.ts index f01846f..6e9fcad 100644 --- a/packages/db/src/migrate.ts +++ b/packages/db/src/migrate.ts @@ -6,12 +6,26 @@ import { createDb } from "./client.js"; const HERE = dirname(fileURLToPath(import.meta.url)); export const MIGRATIONS_DIR = join(HERE, "..", "migrations"); -/** Apply all pending Drizzle migrations to the target database. */ +/** App-wide advisory lock id for migrations. Concurrent `db:migrate` runs (e.g. + * api + worker pre-deploy firing off the same push) serialize on it instead of + * racing Drizzle's journal writes. */ +const MIGRATE_LOCK_ID = 0xd1770; + +/** Apply all pending Drizzle migrations to the target database. Safe to run + * concurrently: callers serialize on a Postgres advisory lock, and re-running + * applied migrations is a journal no-op. */ export async function runMigrations(connectionString: string, migrationsFolder = MIGRATIONS_DIR): Promise { const { db, pool } = createDb(connectionString); + const client = await pool.connect(); try { - await migrate(db, { migrationsFolder }); + await client.query("SELECT pg_advisory_lock($1)", [MIGRATE_LOCK_ID]); + try { + await migrate(db, { migrationsFolder }); + } finally { + await client.query("SELECT pg_advisory_unlock($1)", [MIGRATE_LOCK_ID]); + } } finally { + client.release(); await pool.end(); } } diff --git a/railway.json b/railway.json new file mode 100644 index 0000000..7ef5b94 --- /dev/null +++ b/railway.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://railway.com/railway.schema.json", + "deploy": { + "preDeployCommand": "npm run db:migrate" + } +}