Skip to content

ci(deploy): run schema migrations before worker deploy #1950

@cdcore09

Description

@cdcore09

Summary

The deploy workflow ships the worker but never applies pending Drizzle migrations against the Neon database. Today new schema changes land in production only because someone (most recently me) ran npx tsx scripts/apply-migration.ts migrations/<latest>.sql locally against the same connection string the worker uses. That's accidental coupling, not a designed step — a fresh checkout or a forgotten manual apply will let the worker deploy against an outdated schema and silently 500 on any query against the missing tables/columns.

Requirements

  • CI applies any pending migrations against DATABASE_URL before the Deploy worker step runs, so a failing migration blocks the deploy entirely.
  • Idempotent: re-runs are safe even when no new migrations exist.
  • Tracks which migrations have been applied (preferably via Drizzle's _migrations journal in the database, not the local migrations/meta/_journal.json).
  • Surfaces the SQL that ran in the workflow log so a failed migration is debuggable from the deploy run alone.
  • Uses the same DATABASE_URL GitHub secret the worker already has access to — no extra credentials.
  • Local development still works: npm run db:seed and the existing apply-migration.ts ad-hoc path keep functioning for one-off backfills.

Context

Workflow today is .github/workflows/deploy.yml. Order of operations: checkout → install → typecheck → build web → deploy worker → verify worker → deploy web. The migration apply needs to slot in between Install dependencies and Deploy worker so a migration failure aborts before the worker ships.

Recent example: 0007_remarkable_landau.sql (split languages out of skills, with an inline data-move CTE) was generated by drizzle-kit generate and committed to the repo. CI happily deployed the worker on top of it. The new /me/languages endpoints work in production purely because I'd already applied the migration locally against the shared Neon connection ahead of the push.

Future migrations will hit this same trap. If a contributor without access to .dev.vars opens a PR that adds a migration, the merge ships the worker change but leaves the schema unmigrated.

Implementation Notes

Two viable paths:

Option A — drizzle-kit migrate (preferred). Switch to Drizzle's first-party migration runner, which uses a drizzle.__migrations table in the database to track applied migrations. CI runs npx drizzle-kit migrate and skips anything already applied. Requires the runner to know the connection string at build time — already the case via DATABASE_URL secret. Retire scripts/apply-migration.ts for production use, keep it for ad-hoc/local one-shot statements.

Option B — extend apply-migration.ts to detect already-applied migrations (e.g., a migration_history table the script writes to) and apply pending ones in order. Less ecosystem alignment but no new tool to learn.

Option A is the standard path and avoids hand-rolling the journal. Worth converting now while the migration count is small (7 files).

Both options should run against the production DATABASE_URL in CI, fail loudly on migration error, and abort the deploy before the worker ships.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions