From 358a9574732c2fc6f39c4cff72ad03e6d3f9a9b3 Mon Sep 17 00:00:00 2001 From: waleed Date: Fri, 26 Jun 2026 11:45:23 -0700 Subject: [PATCH] ci(migrations): skip db:migrate on merges that change no migration files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every push to main/staging ran db:migrate against the production/staging database even when the merge changed no schema, so a no-op migration would dial the DB and fail whenever it was at its connection limit (53300, slots reserved for SUPERUSER) — red-X'ing UI-only merges. Add a detect-migrations job (dorny/paths-filter on packages/db/migrations/**) and pass the result into the reusable migrations workflow, which now skips the apply step when no migration files changed. The migrate job still runs so downstream build/deploy jobs that need it are never skipped, and the flag defaults to 'true' so manual dispatch and any unknown value always apply migrations — the gate only ever skips a provably-empty change. --- .github/workflows/ci.yml | 29 ++++++++++++++++++++++++++++- .github/workflows/migrations.yml | 11 +++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c62cd2f2d1e..2dea372c8c9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,15 +49,42 @@ jobs: # Run database migrations before images are pushed: the ECR push triggers # CodePipeline, so migrating first guarantees the schema is in place before # the new app version deploys (replaces the removed ECS migration sidecar) + # Detect whether this push actually changed any migration files, so the + # migrate job below can skip connecting to the production/staging database on + # merges that touch no schema (a no-op db:migrate otherwise dials the DB and + # fails any time it is at its connection limit). + detect-migrations: + name: Detect Migration Changes + runs-on: blacksmith-4vcpu-ubuntu-2404 + if: >- + github.event_name == 'push' && + (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') + outputs: + migrations_changed: ${{ steps.filter.outputs.migrations }} + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6 + with: + fetch-depth: 2 # Need the previous commit to diff migration paths + - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4 + id: filter + with: + filters: | + migrations: + - 'packages/db/migrations/**' + + # The job always runs on push to main/staging so downstream build/deploy jobs + # that `need` it are never skipped; the actual db:migrate step is gated on + # detect-migrations inside the reusable workflow. migrate: name: Migrate DB - needs: [test-build] + needs: [test-build, detect-migrations] if: >- github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') uses: ./.github/workflows/migrations.yml with: environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }} + migrations_changed: ${{ needs.detect-migrations.outputs.migrations_changed }} secrets: inherit # Same ordering for dev (schema push before the dev image lands in ECR) diff --git a/.github/workflows/migrations.yml b/.github/workflows/migrations.yml index f789ec32627..cfd09ad9128 100644 --- a/.github/workflows/migrations.yml +++ b/.github/workflows/migrations.yml @@ -7,6 +7,14 @@ on: description: Target environment (production, staging, or dev) required: true type: string + migrations_changed: + description: >- + Whether this push changed migration files. When 'false' the versioned + apply step is skipped. Defaults to 'true' so manual dispatch and any + omitted/unknown value always applies migrations (never silently skip). + required: false + type: string + default: 'true' workflow_dispatch: inputs: environment: @@ -61,6 +69,7 @@ jobs: DATABASE_URL: ${{ inputs.environment == 'production' && secrets.DATABASE_URL || inputs.environment == 'staging' && secrets.STAGING_DATABASE_URL || inputs.environment == 'dev' && secrets.DEV_DATABASE_URL || '' }} MIGRATION_DATABASE_URL: ${{ inputs.environment == 'production' && secrets.MIGRATION_DATABASE_URL || inputs.environment == 'staging' && secrets.STAGING_MIGRATION_DATABASE_URL || '' }} ENVIRONMENT: ${{ inputs.environment }} + MIGRATIONS_CHANGED: ${{ inputs.migrations_changed }} run: | if [ -z "$DATABASE_URL" ]; then echo "ERROR: no database URL secret resolved for environment '${ENVIRONMENT}'" >&2 @@ -70,6 +79,8 @@ jobs: if [ "${ENVIRONMENT}" = "dev" ]; then echo "Dev environment — pushing schema directly (db:push)" bun run db:push --force + elif [ "${MIGRATIONS_CHANGED}" = "false" ]; then + echo "No migration files changed in this push — skipping db:migrate (nothing to apply)." else echo "Applying versioned migrations (db:migrate)" bun run ./scripts/migrate.ts