diff --git a/.github/actions/check-prisma-migrations/action.yml b/.github/actions/check-prisma-migrations/action.yml new file mode 100644 index 000000000..41d349c7a --- /dev/null +++ b/.github/actions/check-prisma-migrations/action.yml @@ -0,0 +1,114 @@ +name: Check Prisma Migrations +description: >- + Verify Prisma migrations apply cleanly in order and reproduce schema.prisma + (drift check), and that no new migration predates the latest on the base + branch (ordering check). Designed to be embedded in an existing job so its + failure turns that job's status red. + +inputs: + base-ref: + description: >- + Base git ref to diff migrations against (e.g. a PR's base branch). When + set, the action skips work on PRs that don't touch migrations and runs the + ordering check. When empty (release builds), the drift check always runs + and the ordering check is skipped. + required: false + default: "" + +runs: + using: composite + steps: + - name: Detect Prisma changes + id: detect + shell: bash + run: | + if [ -z "${{ inputs.base-ref }}" ]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "No base-ref provided — running drift check unconditionally." + exit 0 + fi + git fetch --no-tags --depth=1 origin "+refs/heads/${{ inputs.base-ref }}:refs/remotes/origin/${{ inputs.base-ref }}" + if git diff --name-only "origin/${{ inputs.base-ref }}" HEAD | grep -q '^packages/db/prisma/'; then + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "Prisma changes detected — running migration checks." + else + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "No Prisma changes — skipping migration checks." + fi + + - name: Start Postgres + if: steps.detect.outputs.changed == 'true' + shell: bash + run: | + docker run -d --name prisma-check-pg \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_PASSWORD=postgres \ + -e POSTGRES_DB=sourcebot \ + -p 5432:5432 postgres:16 + for i in $(seq 1 30); do + if docker exec prisma-check-pg pg_isready -U postgres -q; then + echo "Postgres ready." + exit 0 + fi + sleep 2 + done + echo "Postgres failed to become ready." && exit 1 + + - name: Use Node.js + if: steps.detect.outputs.changed == 'true' + uses: actions/setup-node@v4 + with: + node-version: "20.x" + + - name: Install + if: steps.detect.outputs.changed == 'true' + shell: bash + run: yarn install --frozen-lockfile + + # Check 1: migrations apply cleanly in order AND reproduce schema.prisma. + # `migrate deploy` fails if a migration is broken or applies out of sequence; + # `migrate diff` exits 2 when the applied history drifts from the schema. + - name: Apply migrations + if: steps.detect.outputs.changed == 'true' + shell: bash + working-directory: packages/db + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/sourcebot + run: yarn prisma migrate deploy + + - name: Check for schema drift + if: steps.detect.outputs.changed == 'true' + shell: bash + working-directory: packages/db + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/sourcebot + run: | + yarn prisma migrate diff \ + --from-database \ + --to-schema-datamodel prisma/schema.prisma \ + --exit-code \ + && echo "✅ No drift: migrations reproduce schema.prisma" \ + || (echo "❌ schema.prisma has changes not captured in a migration. Run: yarn dev:prisma:migrate:dev --name " && exit 1) + + # Check 2 (PRs only): no new migration predates the latest on the base branch. + - name: Check migration ordering + if: steps.detect.outputs.changed == 'true' && inputs.base-ref != '' + shell: bash + run: | + MIG_DIR=packages/db/prisma/migrations + BASE="origin/${{ inputs.base-ref }}" + LATEST_ON_BASE=$(git ls-tree -r --name-only "$BASE" -- "$MIG_DIR" \ + | sed -n "s#$MIG_DIR/\([0-9]\{14\}\)_.*#\1#p" | sort | tail -1) + echo "Latest migration on ${{ inputs.base-ref }}: ${LATEST_ON_BASE:-}" + NEW=$(comm -23 \ + <(ls "$MIG_DIR" | sed -n 's/^\([0-9]\{14\}\)_.*/\1/p' | sort -u) \ + <(git ls-tree -r --name-only "$BASE" -- "$MIG_DIR" | sed -n "s#$MIG_DIR/\([0-9]\{14\}\)_.*#\1#p" | sort -u)) + FAIL=0 + for ts in $NEW; do + if [ -n "$LATEST_ON_BASE" ] && [ "$ts" -lt "$LATEST_ON_BASE" ]; then + echo "❌ New migration $ts predates latest migration on ${{ inputs.base-ref }} ($LATEST_ON_BASE). Rename it with a current timestamp." + FAIL=1 + fi + done + [ "$FAIL" -eq 0 ] && echo "✅ Migration ordering OK" + exit $FAIL diff --git a/.github/workflows/_build.yml b/.github/workflows/_build.yml index d87c28472..32656ffc8 100644 --- a/.github/workflows/_build.yml +++ b/.github/workflows/_build.yml @@ -76,6 +76,14 @@ jobs: fetch-depth: 0 token: ${{ inputs.use_app_token && steps.generate_token.outputs.token || github.token }} + # Release backstop: fail the build if migrations drift from schema.prisma. + # Runs once (amd64 only) since the check is platform-independent. base-ref + # is omitted, so the drift check always runs and the (PR-only) ordering + # check is skipped. + - name: Check Prisma migrations + if: matrix.platform == 'linux/amd64' + uses: ./.github/actions/check-prisma-migrations + # Extract metadata (tags, labels) for Docker # https://github.com/docker/metadata-action - name: Extract Docker metadata diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml index b344195ed..a18fef278 100644 --- a/.github/workflows/pr-gate.yml +++ b/.github/workflows/pr-gate.yml @@ -1,6 +1,7 @@ name: PR Gate -# This gate simply validates that we can build the docker container. +# This gate validates that Prisma migrations are in order and that we can build +# the docker container. on: pull_request: @@ -16,6 +17,15 @@ jobs: uses: actions/checkout@v4 with: submodules: "true" + # full history so migration checks can diff against the base branch + fetch-depth: 0 + + # Fails fast (before the docker build) when migrations drift from + # schema.prisma or a new migration is added out of timestamp order. + - name: Check Prisma migrations + uses: ./.github/actions/check-prisma-migrations + with: + base-ref: ${{ github.base_ref }} - name: Build Docker image id: build