Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Fix migrate.yml auto-applies production DB migrations on push with no concurrency guard or deploy ordering - #2703

Open
polylane[bot] wants to merge 1 commit into
mainfrom
polylane/autofix/x9j5wy8my9tk
Open

Fix migrate.yml auto-applies production DB migrations on push with no concurrency guard or deploy ordering#2703
polylane[bot] wants to merge 1 commit into
mainfrom
polylane/autofix/x9j5wy8my9tk

Conversation

@polylane

@polylane polylane Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Fixes: migrate.yml auto-applies production DB migrations on push with no concurrency guard or deploy ordering

Every push to main that touches a database schema file triggers an automatic migration against the production Turso database — the single shared data store behind the API, background jobs, and every customer status page. Nothing stopped two of those migrations from running at the same time, which can corrupt or deadlock a database that all tenants share, and nothing bounded a stalled migration run. This adds the same concurrency guard and 15-minute job timeout the repository already uses on its other release workflows, so production migrations queue one at a time instead of racing.

What caused this

Affected: int_01b0344f3001ufkixqp40d4t · severity medium

Before / After

Before: Two pushes to main touching schema files within a few minutes ran two production migrations at the same time against the one shared Turso database, with nothing queuing them and nothing bounding a hung run. A migration that stalled held the runner open indefinitely.

After: A second push that touches packages/db/drizzle/** waits in a queue until the in-flight migration finishes, then applies the newest migration set; a manually dispatched run waits behind a push-triggered one the same way. A migration that stalls fails after 15 minutes instead of running forever.

What changed

  • .github/workflows/migrate.yml: added a workflow-level concurrency group (migrate-db, cancel-in-progress: false) so overlapping drizzle-triggered pushes, or a push plus a manual dispatch, queue instead of running two migrators against the production database at once.
  • .github/workflows/migrate.yml: added timeout-minutes: 15 to the migrate job, matching deploy.yml, deploy-checker.yml, deploy-private-location.yml and restart-workflows.yml, so a hung migration fails fast and cannot hold a runner or block queued runs indefinitely.

Why it's safe

  • The concurrency group cannot starve the queue: cancel-in-progress: false means every queued run still executes, only later; nothing is dropped.
  • Push-triggered runs check out the triggering SHA, so a queued run applies a newer migration set after an older one — exactly the ordering drizzle's journal expects; queueing preserves correctness rather than risking it.
  • The static group name means dispatches serialize with pushes intentionally: every run targets the same production DATABASE_URL secret, so there is no legitimate case for two concurrent runs.
  • The 15-minute timeout matches the value already in production on the sibling deploy workflows; recent migrations completed well under 5 minutes, so the bound only affects the pathological case.
  • No application code, secrets, or deploy steps are touched — the change is inert until the next drizzle-path push, and single-run behavior is identical to today's.

Validation

  • pnpm verify (oxfmt + oxlint + doc refs + deno check, all 40 workspace packages): 40/40 tasks successful, run in this thread after pnpm install --frozen-lockfile.
  • actionlint .github/workflows/migrate.yml (v1.7.7): clean, no findings.
  • YAML structure parse of the edited workflow: concurrency: {group: migrate-db, cancel-in-progress: false} and timeout-minutes: 15 verified present, on: triggers unchanged.
  • pnpm verify:test: not run — the change touches only a CI workflow file, no package code or tests; the repo's test suites need a live libSQL database, which this change does not affect.
Root cause and scoping notes

Root cause

  • Symptom: Migrate DB runs deno run -A src/migrate.mts (drizzle's libsql migrator) against the production Turso DATABASE_URL on every push to main touching packages/db/drizzle/**, plus manual dispatch — with no concurrency: block, so GitHub never serialized runs.
  • Mechanism: the migrator reads __drizzle_migrations and applies pending SQL with no database-level lock; two concurrent runs on back-to-back schema pushes can both attempt the same migration set and interleave, leaving the DB in a partial state. The window is real: 2026-09-10 saw pushes at 12:27Z, 12:39Z, 12:40Z.
  • Root cause: migrate.yml was the one release-triggering workflow without the guard — deploy.yml and siblings already carry concurrency groups (PR Fix Fly Deploy has no concurrency guard: overlapping deploys race machine leases — the recurring "machine not found" failures dismissed as transients are structural #2686) — and the only CI path applying migrations to prod (test.yml's identical step targets its own ephemeral sqld at 127.0.0.1:8080).
  • Why this fixes it: a workflow-level group with cancel-in-progress: false makes GitHub queue every run, so the race cannot open. The static name is deliberate: every run targets the same prod DB via the same secrets. The 15-minute timeout matches the sibling workflows and only affects a hung run.
  • Not closed here: migrate-vs-deploy interleaving and unvalidated migrations reaching unprotected main — both recorded as follow-ups on the issue timeline.

Out of scope / follow-ups

Causal chain
  • Signal (alert): migrate.yml auto-applies production DB migrations on push with no concurrency guard or deploy ordering (confirmed issue iss_08ecdd893001h972tpc1fo8x, severity medium)
  • Surfacing site: Migrate DB workflow runs on openstatusHQ/openstatus at GitHub Actions workflow run queue for openstatusHQ/openstatus
  • Mechanism: A qualifying push starts the migrate job immediately. With no concurrency group, GitHub never serializes runs, so two drizzle-touching pushes (or a push plus a manual dispatch) execute deno run -A src/migrate.mts — drizzle's libsql migrator against production DATABASE_URL — concurrently. The migrator reads __drizzle_migrations and applies pending SQL files with no database-level lock, so concurrent runs can both attempt the same pending set and interleave mid-set. Established from reading packages/db/src/migrate.mts and .github/workflows/migrate.yml this run.
  • Producer: Migrate DB workflow (.github/workflows/migrate.yml) — the only CI path that applies drizzle schema migrations to the production Turso DB, at openstatusHQ/openstatus:.github/workflows/migrate.yml
  • Trigger: Every push to main touching packages/db/drizzle/** (~monthly historically: 09-07, 09-03, 08-27, 07-29, 07-28, 07-23, 07-15, 07-08, 07-06) plus bursts (09-10: three pushes in 13 minutes) and any manual workflow_dispatch — all targeting the same production DB secrets.
  • What happens to the failed unit today: Today a failed migration run just goes red: deploy.yml (triggers on every push, no paths filter) and deploy-workflows.yml (triggers on packages/db/**) run unconditionally on the same push, main has no required status checks, and nothing blocks or rolls back the deploy. A hung run held the job open indefinitely (no timeout-minutes). Established from reading migrate.yml, deploy.yml, and the branch-protection verification in the issue evidence.
  • Cadence check: Matches. A guard-absence defect predicts zero failures until the first overlap, and that is exactly the observed pattern: all ~10 recorded push-triggered runs succeeded because no two overlapped yet. The 09-10 burst (pushes at 12:27Z, 12:39Z, 12:40Z) shows the overlap window is real at current push behavior.
  • Blast radius: 6 other resource(s), 0 other tenant(s); data at risk: The production Turso DB is the single shared data store for openstatus-api, openstatus-workflows, checker ingest, and all Vercel data paths — all tenants. A partial or destructive migration applied by a racing or hung run means API 5xx for every tenant and manual Turso recovery, with fly-log-shipper stopped so runtime logs are not queryable to diagnose.
  • Producer evidence:

Detection outcome

The fingerprinted signal is the confirmed issue "migrate.yml auto-applies production DB migrations on push with no concurrency guard or deploy ordering" (iss_08ecdd893001h972tpc1fo8x). Once this deploys, the unguarded-migration aspect it names stops being true: every Migrate DB run — push-triggered or dispatched — joins one migrate-db concurrency group, so a second run queues instead of racing the first against the production Turso DB, and a stalled run fails at 15 minutes instead of holding open indefinitely. The signal's remaining aspects (migrate-vs-deploy interleaving and unvalidated migrations reaching main) are unchanged by design and are recorded as follow-ups on the issue timeline, not silenced here.

Fix chosen

Chosen: cause (removes the mechanism that produces the failure): Add a workflow-level concurrency group (migrate-db, cancel-in-progress: false) and a 15-minute job timeout to migrate.yml, serializing all production migration runs against each other.

Considered and not chosen:

  • cause (removes the mechanism that produces the failure): Gate the deploy on migration success: a migrate step or needs relationship inside deploy.yml, or a workflow_run chain that deploys only after Migrate DB succeeds. Not chosen: Changes the release model and deploy latency for every push to main — a maintainer decision, recorded as a follow-up on the issue timeline rather than made unilaterally in this PR.
  • cause (removes the mechanism that produces the failure): Apply migrations to a staging Turso first and auto-run prod migrate only on staging success. Not chosen: Requires provisioning a staging Turso instance and new repo secrets that do not exist; an infrastructure decision for the team, not a workflow-file edit.
  • disable (turns a feature, guard, check, test, or telemetry off): Drop the push trigger so migrations are only ever run manually via workflow_dispatch. Not chosen: Removes the automation entirely and changes the release process unilaterally; the concurrency guard keeps the automation and removes only the race.

Outcome after fix

Replay the 09-10 burst: pushes land on main at 12:27Z, 12:39Z and 12:40Z, the first two touching packages/db/drizzle/**. Today, the 12:27Z push starts a migrate run immediately and the 12:39Z push starts a second one while the first may still hold the lock window — two drizzle migrators reading the same __drizzle_migrations table and racing to apply the same pending SQL against the production Turso DB. With this change: the 12:27Z run joins migrate-db and runs; the 12:39Z run enters the same group, is marked queued, and GitHub starts it only after the first completes, at which point it applies the newer migration set against a DB already at the older state. If instead a migration hangs (e.g. a network partition to Turso), the job now fails at 15 minutes and the queued run proceeds, where before it held the runner indefinitely and nothing behind it could advance. The deployment itself is untouched: deploy.yml still rolls the API on the same push, as before — that interleaving is explicitly left open on the issue timeline.

1 file changed (+4/-0)
  • .github/workflows/migrate.yml: modified, +4/-0

Repository conventions present in the repository: AGENTS.md, CLAUDE.md, apps/checker/AGENTS.md, apps/dashboard/AGENTS.md, apps/server/AGENTS.md, apps/status-page/AGENTS.md, apps/web/AGENTS.md, apps/workflows/AGENTS.md, packages/services/AGENTS.md, packages/ui/AGENTS.md.

Repository lint: pnpm run lint (declared in package.json) passed over the whole repository (the declared target takes no file arguments).

view-autofix view-investigation view-issue


Generated by Polylane.

Review in cubic

@vercel

vercel Bot commented Sep 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
openstatus-dashboard Ready Ready Preview Sep 11, 2026 5:51am UTC
openstatus-status-page Ready Ready Preview Sep 11, 2026 5:51am UTC
openstatus-web Ready Ready Preview Sep 11, 2026 5:51am UTC

Request Review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

polylane severity:medium Polylane autofix severity: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants