From 67dc8e20507959af6546d199a5f6e3896c4d1f19 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 10 Jun 2026 15:22:05 -0700 Subject: [PATCH 1/7] feat(ci): run db migrations from github ci with environment-scoped secrets --- .github/workflows/ci.yml | 35 +++++++++++++++++++++--------- .github/workflows/migrations.yml | 37 ++++++++++++++++++++++++++------ 2 files changed, 55 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1dc73e648f..a019331463f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,10 +46,33 @@ jobs: echo "ℹ️ Not a release commit" fi + # 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) + migrate: + name: Migrate DB + needs: [test-build] + 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' }} + secrets: inherit + + # Same ordering for dev (schema push before the dev image lands in ECR) + migrate-dev: + name: Migrate Dev DB + if: github.event_name == 'push' && github.ref == 'refs/heads/dev' + uses: ./.github/workflows/migrations.yml + with: + environment: dev + secrets: inherit + # Dev: build all 3 images for ECR only (no GHCR, no ARM64) build-dev: name: Build Dev ECR - needs: [detect-version] + needs: [detect-version, migrate-dev] if: github.event_name == 'push' && github.ref == 'refs/heads/dev' runs-on: blacksmith-8vcpu-ubuntu-2404 permissions: @@ -108,7 +131,7 @@ jobs: # Main/staging: build AMD64 images and push to ECR + GHCR build-amd64: name: Build AMD64 - needs: [test-build, detect-version] + needs: [test-build, detect-version, migrate] if: >- github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') @@ -318,14 +341,6 @@ jobs: docker manifest push "${IMAGE_BASE}:${VERSION}" fi - # Run database migrations for dev - migrate-dev: - name: Migrate Dev DB - needs: [build-dev] - if: github.event_name == 'push' && github.ref == 'refs/heads/dev' - uses: ./.github/workflows/migrations.yml - secrets: inherit - # Check if docs changed check-docs-changes: name: Check Docs Changes diff --git a/.github/workflows/migrations.yml b/.github/workflows/migrations.yml index 03f85553aa6..8084043e875 100644 --- a/.github/workflows/migrations.yml +++ b/.github/workflows/migrations.yml @@ -2,7 +2,21 @@ name: Database Migrations on: workflow_call: + inputs: + environment: + description: Target GitHub environment (production, staging, or dev) + required: true + type: string workflow_dispatch: + inputs: + environment: + description: Target GitHub environment + required: true + type: choice + options: + - production + - staging + - dev permissions: contents: read @@ -11,6 +25,7 @@ jobs: migrate: name: Apply Database Migrations runs-on: blacksmith-4vcpu-ubuntu-2404 + environment: ${{ inputs.environment }} steps: - name: Checkout code @@ -35,15 +50,23 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile + # MIGRATIONS_DATABASE_URL is an environment-scoped secret with no repo-level + # fallback: if it's missing on the target environment it resolves to empty and + # the guard below fails the job, instead of silently inheriting another + # environment's database - name: Apply database schema changes working-directory: ./packages/db env: - DATABASE_URL: ${{ github.ref == 'refs/heads/main' && secrets.DATABASE_URL || github.ref == 'refs/heads/dev' && secrets.DEV_DATABASE_URL || secrets.STAGING_DATABASE_URL }} + DATABASE_URL: ${{ secrets.MIGRATIONS_DATABASE_URL }} run: | - if [ "${{ github.ref }}" = "refs/heads/dev" ]; then - echo "Dev environment detected — pushing schema with drizzle-kit (db:push)" + if [ -z "$DATABASE_URL" ]; then + echo "ERROR: MIGRATIONS_DATABASE_URL is not set on environment '${{ inputs.environment }}'" >&2 + exit 1 + fi + + echo "Applying versioned migrations (db:migrate)" + bun run ./scripts/migrate.ts + if [ "${{ inputs.environment }}" = "dev" ]; then + echo "Dev environment — also pushing unversioned schema drift (db:push)" bun run db:push --force - else - echo "Applying versioned migrations (db:migrate)" - bun run ./scripts/migrate.ts - fi \ No newline at end of file + fi From 9a499b9dcdd400a6099c570ff9757f884dc4565d Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 10 Jun 2026 15:27:07 -0700 Subject: [PATCH 2/7] fix(ci): pass environment input via env var instead of shell interpolation --- .github/workflows/migrations.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/migrations.yml b/.github/workflows/migrations.yml index 8084043e875..94f8bf22ab4 100644 --- a/.github/workflows/migrations.yml +++ b/.github/workflows/migrations.yml @@ -58,15 +58,16 @@ jobs: working-directory: ./packages/db env: DATABASE_URL: ${{ secrets.MIGRATIONS_DATABASE_URL }} + ENVIRONMENT: ${{ inputs.environment }} run: | if [ -z "$DATABASE_URL" ]; then - echo "ERROR: MIGRATIONS_DATABASE_URL is not set on environment '${{ inputs.environment }}'" >&2 + echo "ERROR: MIGRATIONS_DATABASE_URL is not set on environment '${ENVIRONMENT}'" >&2 exit 1 fi echo "Applying versioned migrations (db:migrate)" bun run ./scripts/migrate.ts - if [ "${{ inputs.environment }}" = "dev" ]; then + if [ "${ENVIRONMENT}" = "dev" ]; then echo "Dev environment — also pushing unversioned schema drift (db:push)" bun run db:push --force fi From 4eeb81bf386e31674ce9b93187c4a6d4a71f0e75 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 10 Jun 2026 15:29:45 -0700 Subject: [PATCH 3/7] fix(ci): rename migration environments to db-* to avoid collision with Vercel's Production env --- .github/workflows/ci.yml | 4 ++-- .github/workflows/migrations.yml | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a019331463f..2d25f605f2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') uses: ./.github/workflows/migrations.yml with: - environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }} + environment: ${{ github.ref == 'refs/heads/main' && 'db-production' || 'db-staging' }} secrets: inherit # Same ordering for dev (schema push before the dev image lands in ECR) @@ -66,7 +66,7 @@ jobs: if: github.event_name == 'push' && github.ref == 'refs/heads/dev' uses: ./.github/workflows/migrations.yml with: - environment: dev + environment: db-dev secrets: inherit # Dev: build all 3 images for ECR only (no GHCR, no ARM64) diff --git a/.github/workflows/migrations.yml b/.github/workflows/migrations.yml index 94f8bf22ab4..d8ef6221f69 100644 --- a/.github/workflows/migrations.yml +++ b/.github/workflows/migrations.yml @@ -4,7 +4,7 @@ on: workflow_call: inputs: environment: - description: Target GitHub environment (production, staging, or dev) + description: Target GitHub environment (db-production, db-staging, or db-dev) required: true type: string workflow_dispatch: @@ -14,9 +14,9 @@ on: required: true type: choice options: - - production - - staging - - dev + - db-production + - db-staging + - db-dev permissions: contents: read @@ -67,7 +67,7 @@ jobs: echo "Applying versioned migrations (db:migrate)" bun run ./scripts/migrate.ts - if [ "${ENVIRONMENT}" = "dev" ]; then + if [ "${ENVIRONMENT}" = "db-dev" ]; then echo "Dev environment — also pushing unversioned schema drift (db:push)" bun run db:push --force fi From 11d285deb1b513577aec40867b6907946a94c8d2 Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 10 Jun 2026 16:44:55 -0700 Subject: [PATCH 4/7] improvement(ci): resolve migration db url from prefixed repo secrets, drop github environments --- .github/workflows/ci.yml | 4 ++-- .github/workflows/migrations.yml | 24 +++++++++++------------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d25f605f2b..a019331463f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/staging') uses: ./.github/workflows/migrations.yml with: - environment: ${{ github.ref == 'refs/heads/main' && 'db-production' || 'db-staging' }} + environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }} secrets: inherit # Same ordering for dev (schema push before the dev image lands in ECR) @@ -66,7 +66,7 @@ jobs: if: github.event_name == 'push' && github.ref == 'refs/heads/dev' uses: ./.github/workflows/migrations.yml with: - environment: db-dev + environment: dev secrets: inherit # Dev: build all 3 images for ECR only (no GHCR, no ARM64) diff --git a/.github/workflows/migrations.yml b/.github/workflows/migrations.yml index d8ef6221f69..1c340d48b39 100644 --- a/.github/workflows/migrations.yml +++ b/.github/workflows/migrations.yml @@ -4,19 +4,19 @@ on: workflow_call: inputs: environment: - description: Target GitHub environment (db-production, db-staging, or db-dev) + description: Target environment (production, staging, or dev) required: true type: string workflow_dispatch: inputs: environment: - description: Target GitHub environment + description: Target environment required: true type: choice options: - - db-production - - db-staging - - db-dev + - production + - staging + - dev permissions: contents: read @@ -25,7 +25,6 @@ jobs: migrate: name: Apply Database Migrations runs-on: blacksmith-4vcpu-ubuntu-2404 - environment: ${{ inputs.environment }} steps: - name: Checkout code @@ -50,24 +49,23 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile - # MIGRATIONS_DATABASE_URL is an environment-scoped secret with no repo-level - # fallback: if it's missing on the target environment it resolves to empty and - # the guard below fails the job, instead of silently inheriting another - # environment's database + # The expression maps the explicit environment input to exactly one repo + # secret, so the job never holds another environment's database URL. An + # unknown environment resolves to empty and the guard below fails the job. - name: Apply database schema changes working-directory: ./packages/db env: - DATABASE_URL: ${{ secrets.MIGRATIONS_DATABASE_URL }} + DATABASE_URL: ${{ inputs.environment == 'production' && secrets.DATABASE_URL || inputs.environment == 'staging' && secrets.STAGING_DATABASE_URL || inputs.environment == 'dev' && secrets.DEV_DATABASE_URL || '' }} ENVIRONMENT: ${{ inputs.environment }} run: | if [ -z "$DATABASE_URL" ]; then - echo "ERROR: MIGRATIONS_DATABASE_URL is not set on environment '${ENVIRONMENT}'" >&2 + echo "ERROR: no database URL secret resolved for environment '${ENVIRONMENT}'" >&2 exit 1 fi echo "Applying versioned migrations (db:migrate)" bun run ./scripts/migrate.ts - if [ "${ENVIRONMENT}" = "db-dev" ]; then + if [ "${ENVIRONMENT}" = "dev" ]; then echo "Dev environment — also pushing unversioned schema drift (db:push)" bun run db:push --force fi From bbe722b440b1ea3f3f5cab013c15096d6959466c Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 10 Jun 2026 16:48:31 -0700 Subject: [PATCH 5/7] fix(ci): dev migrations use db:push only, matching previous behavior --- .github/workflows/migrations.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/migrations.yml b/.github/workflows/migrations.yml index 1c340d48b39..240972f9a03 100644 --- a/.github/workflows/migrations.yml +++ b/.github/workflows/migrations.yml @@ -63,9 +63,10 @@ jobs: exit 1 fi - echo "Applying versioned migrations (db:migrate)" - bun run ./scripts/migrate.ts if [ "${ENVIRONMENT}" = "dev" ]; then - echo "Dev environment — also pushing unversioned schema drift (db:push)" + echo "Dev environment — pushing schema directly (db:push)" bun run db:push --force + else + echo "Applying versioned migrations (db:migrate)" + bun run ./scripts/migrate.ts fi From 3b80d8387be5ff01e9dfe35621b1dbfec8f72fcb Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 10 Jun 2026 16:52:33 -0700 Subject: [PATCH 6/7] improvement(ci): reject pooled (pgbouncer) database urls for migrations --- .github/workflows/migrations.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/migrations.yml b/.github/workflows/migrations.yml index 240972f9a03..2d8e8256193 100644 --- a/.github/workflows/migrations.yml +++ b/.github/workflows/migrations.yml @@ -62,6 +62,12 @@ jobs: echo "ERROR: no database URL secret resolved for environment '${ENVIRONMENT}'" >&2 exit 1 fi + case "$DATABASE_URL" in + *:6432*) + echo "ERROR: '${ENVIRONMENT}' database URL targets PgBouncer (port 6432); migrations need a direct connection (port 5432) — session advisory locks and SET don't survive transaction pooling" >&2 + exit 1 + ;; + esac if [ "${ENVIRONMENT}" = "dev" ]; then echo "Dev environment — pushing schema directly (db:push)" From e141ff1588472528c8d744c508514fceccb88c2e Mon Sep 17 00:00:00 2001 From: Theodore Li Date: Wed, 10 Jun 2026 16:55:08 -0700 Subject: [PATCH 7/7] Revert "improvement(ci): reject pooled (pgbouncer) database urls for migrations" This reverts commit 3b80d8387be5ff01e9dfe35621b1dbfec8f72fcb. --- .github/workflows/migrations.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/migrations.yml b/.github/workflows/migrations.yml index 2d8e8256193..240972f9a03 100644 --- a/.github/workflows/migrations.yml +++ b/.github/workflows/migrations.yml @@ -62,12 +62,6 @@ jobs: echo "ERROR: no database URL secret resolved for environment '${ENVIRONMENT}'" >&2 exit 1 fi - case "$DATABASE_URL" in - *:6432*) - echo "ERROR: '${ENVIRONMENT}' database URL targets PgBouncer (port 6432); migrations need a direct connection (port 5432) — session advisory locks and SET don't survive transaction pooling" >&2 - exit 1 - ;; - esac if [ "${ENVIRONMENT}" = "dev" ]; then echo "Dev environment — pushing schema directly (db:push)"