diff --git a/.github/workflows/migrate.yml b/.github/workflows/migrate.yml index ff2be697a12..4975ca60334 100644 --- a/.github/workflows/migrate.yml +++ b/.github/workflows/migrate.yml @@ -48,6 +48,31 @@ jobs: cancel-in-progress: false steps: + # An environment only migrates from its own branch, so a manual dispatch + # from a feature branch cannot apply that branch's migrations to a shared + # database. Under workflow_call github.ref is the caller's ref (ci.yml's + # push to main/staging/dev). This fails rather than skips: ci.yml's + # promote gates on migrate succeeding, and a skipped run must never read + # as a migration that happened. It runs before any secret is read. + - name: Require the environment's own branch + env: + ENVIRONMENT: ${{ inputs.environment }} + REF: ${{ github.ref }} + run: | + case "${ENVIRONMENT}" in + production) expected=refs/heads/main ;; + staging) expected=refs/heads/staging ;; + dev) expected=refs/heads/dev ;; + *) + echo "::error::Unknown migrate environment '${ENVIRONMENT}'" + exit 1 + ;; + esac + if [ "${REF}" != "${expected}" ]; then + echo "::error::Refusing to migrate ${ENVIRONMENT} from ${REF}; ${ENVIRONMENT} migrates only from ${expected}" + exit 1 + fi + - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 with: diff --git a/scripts/migrations-workflow.test.ts b/scripts/migrations-workflow.test.ts index ce25ddd7058..d3e1494e37c 100644 --- a/scripts/migrations-workflow.test.ts +++ b/scripts/migrations-workflow.test.ts @@ -3,7 +3,9 @@ import { readFileSync } from 'node:fs' import { describe, expect, it } from 'vitest' const workflow = readFileSync(new URL('../.github/workflows/migrate.yml', import.meta.url), 'utf8') -const step = workflow.match(/^ {8}run: \|\r?\n((?: {10}.*(?:\r?\n|$)|\r?\n)+)/m)?.[1] +const step = workflow.match( + /^ {6}- name: Apply database schema changes\r?\n(?: {8}.*\r?\n)*? {8}run: \|\r?\n((?: {10}.*(?:\r?\n|$)|\r?\n)+)/m +)?.[1] if (!step) throw new Error('Migration workflow must contain its schema application shell step') const script = step.replace(/^ {10}/gm, '')