Skip to content

ci(release): apply pending migrations before publishing the image - #230

Merged
aquie00t merged 1 commit into
mainfrom
ci/apply-migrations-on-release
Aug 27, 2026
Merged

ci(release): apply pending migrations before publishing the image#230
aquie00t merged 1 commit into
mainfrom
ci/apply-migrations-on-release

Conversation

@aquie00t

Copy link
Copy Markdown
Collaborator

⚠️ Before merging: add a DATABASE_URL repository secret

The new job fails loudly without it, and it gates the image push. That is deliberate — an image whose schema has not been applied is the bug this PR exists to prevent — but it means the next release will not publish until the secret is there.

And this PR does not fix production. It stops the next occurrence. The current outage still needs pnpm prisma migrate deploy run against the production database once, by hand.

Why

Nothing in the pipeline ever ran migrations:

  • release.yml cuts a version, then pushes an image to GHCR
  • the Dockerfile starts with CMD ["node", "dist/index.js"]
  • db:deploy sits in package.json with no caller anywhere

So applying a migration was a manual step somebody had to remember, and the only thing that told you it had been forgotten was production breaking.

Which is what happened. 20260826233221_notification_deep_link_targets shipped with v1.8.0 (#221) but was never applied, so the deployed code selects notifications.postId, articleId and commentId from a database that does not have them:

{ "type": "about:blank", "title": "PrismaClientKnownRequestError",
  "status": 500, "detail": "The server could not complete the request.",
  "instance": "/api/v1/notifications?page=1&limit=20" }

GET /notifications is just the loudest symptom — every write that creates a notification touches the same columns, so follows, likes and comments were all in the same position.

What the job does

Runs after the release and before the image push, so the schema is in place before the image that expects it can be pulled:

release ──▶ migrate ──▶ docker-push
  • prisma migrate deploy — only applies what has not run yet, never rewrites or drops anything, and takes an advisory lock so overlapping releases queue rather than race
  • prisma migrate status runs first with continue-on-error, so the run log records what each release applied. It exits non-zero exactly when something is pending, which is the normal case here, so its status is not the job's
  • a missing DATABASE_URL fails with ::error::The DATABASE_URL secret is not set… rather than a Prisma error that does not name the cause
  • environment: production gives the run a deployment record, and somewhere to add required reviewers if this should ever need approval

One trap worth knowing about

The job deliberately does not set NODE_ENV=production. pnpm skips devDependencies when it sees that, and the Prisma CLI is a devDependency — the job would have failed at pnpm prisma. The datasource URL does not need it either: prisma.config.ts reads process.env.DATABASE_URL, and the .env file it looks for does not exist on a runner, so dotenv is a no-op and the step env wins.

Verification

The workflow parses and the job graph is what it should be:

jobs: ['release', 'migrate', 'docker-push']
migrate needs: release
docker-push needs: ['release', 'migrate']

The job itself can only really be exercised by a release, so it will prove itself on the next merge to main — with the secret in place.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PwzkQ5YGFXSB9jWCZKzX4H

Nothing in the pipeline ran migrations. release.yml cut a version and
pushed an image; the Dockerfile starts the server directly; db:deploy
existed in package.json with no caller. Applying migrations was a manual
step somebody had to remember.

It was missed for the notification deep-link columns, so production ran
code that selects notifications.postId, articleId and commentId against
a database without them. GET /notifications answered 500 with a
PrismaClientKnownRequestError, and every write that creates a
notification was in the same position.

A migrate job now runs between the release and the image push, so the
schema is in place before the image that expects it can be pulled. It
uses deploy, which only applies what has not run yet and takes an
advisory lock, and it prints migrate status first so the run log records
what each release applied. A missing DATABASE_URL secret fails the job
with a message that names the secret rather than a Prisma error.

The job deliberately does not set NODE_ENV=production: pnpm would skip
devDependencies, and the Prisma CLI it runs is one of them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PwzkQ5YGFXSB9jWCZKzX4H
@aquie00t
aquie00t merged commit 6f0a6ca into main Aug 27, 2026
10 checks passed
@aquie00t
aquie00t deleted the ci/apply-migrations-on-release branch August 27, 2026 06:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant