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