From 454553d3ca916b85386fafaa59d19540edb8783d Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Mon, 29 Jun 2026 16:03:14 -0700 Subject: [PATCH 1/5] ci: enforce prisma migration ordering and schema drift Add a GitHub Actions workflow that runs on PRs touching packages/db/prisma/**. It verifies migrations apply cleanly in sequence and reproduce schema.prisma (drift check via prisma migrate diff), and that no newly added migration predates the latest migration on main (out-of-order timestamp check). Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/prisma-migrations.yml | 81 +++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 .github/workflows/prisma-migrations.yml diff --git a/.github/workflows/prisma-migrations.yml b/.github/workflows/prisma-migrations.yml new file mode 100644 index 000000000..c6a985131 --- /dev/null +++ b/.github/workflows/prisma-migrations.yml @@ -0,0 +1,81 @@ +name: Prisma Migrations + +on: + pull_request: + branches: ["main"] + paths: + - "packages/db/prisma/**" + +jobs: + check-migrations: + runs-on: ubuntu-latest + permissions: + contents: read + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: sourcebot + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + DATABASE_URL: postgresql://postgres:postgres@localhost:5432/sourcebot + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: "true" + # full history so we can diff migrations against the merge base + fetch-depth: 0 + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: "20.x" + - name: Install + run: yarn install --frozen-lockfile + + # Check 1: migrations apply cleanly in order AND reproduce schema.prisma. + # `migrate diff` exits 2 when the applied migration history drifts from the schema. + - name: Apply migrations + working-directory: packages/db + run: yarn prisma migrate deploy + + - name: Check for schema drift + working-directory: packages/db + 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: no new migration has a timestamp earlier than the latest on main. + - name: Check migration ordering + run: | + MIG_DIR=packages/db/prisma/migrations + # latest timestamp already on main + git fetch origin main --depth=1 + LATEST_ON_MAIN=$(git ls-tree -r --name-only origin/main -- "$MIG_DIR" \ + | sed -n "s#$MIG_DIR/\([0-9]\{14\}\)_.*#\1#p" | sort | tail -1) + echo "Latest migration on main: ${LATEST_ON_MAIN:-}" + # migrations added in this PR (present locally, not on main) + NEW=$(comm -23 \ + <(ls "$MIG_DIR" | sed -n 's/^\([0-9]\{14\}\)_.*/\1/p' | sort -u) \ + <(git ls-tree -r --name-only origin/main -- "$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_MAIN" ] && [ "$ts" -lt "$LATEST_ON_MAIN" ]; then + echo "❌ New migration $ts predates latest migration on main ($LATEST_ON_MAIN). Rename it with a current timestamp." + FAIL=1 + fi + done + [ "$FAIL" -eq 0 ] && echo "✅ Migration ordering OK" + exit $FAIL From b653ef42576fccd44b635c72290d970d5057035e Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Mon, 29 Jun 2026 16:15:36 -0700 Subject: [PATCH 2/5] ci: gate PRs and release builds on prisma migration order Replace the standalone migration workflow with a composite action (.github/actions/check-prisma-migrations) and embed it in: - pr-gate.yml's build job, so migration drift or out-of-order timestamps fail the required PR check before merge. - _build.yml's build job (amd64 only) as a release-time backstop for schema drift. The action self-contains Postgres via docker run, detects whether Prisma files changed (skipping fast on PRs that don't), verifies migrations apply in sequence and reproduce schema.prisma, and checks no new migration predates the latest on the base branch. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../check-prisma-migrations/action.yml | 114 ++++++++++++++++++ .github/workflows/_build.yml | 8 ++ .github/workflows/pr-gate.yml | 12 +- .github/workflows/prisma-migrations.yml | 81 ------------- CHANGELOG.md | 1 + 5 files changed, 134 insertions(+), 82 deletions(-) create mode 100644 .github/actions/check-prisma-migrations/action.yml delete mode 100644 .github/workflows/prisma-migrations.yml 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 diff --git a/.github/workflows/prisma-migrations.yml b/.github/workflows/prisma-migrations.yml deleted file mode 100644 index c6a985131..000000000 --- a/.github/workflows/prisma-migrations.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: Prisma Migrations - -on: - pull_request: - branches: ["main"] - paths: - - "packages/db/prisma/**" - -jobs: - check-migrations: - runs-on: ubuntu-latest - permissions: - contents: read - services: - postgres: - image: postgres:16 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: sourcebot - ports: - - 5432:5432 - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - env: - DATABASE_URL: postgresql://postgres:postgres@localhost:5432/sourcebot - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - submodules: "true" - # full history so we can diff migrations against the merge base - fetch-depth: 0 - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: "20.x" - - name: Install - run: yarn install --frozen-lockfile - - # Check 1: migrations apply cleanly in order AND reproduce schema.prisma. - # `migrate diff` exits 2 when the applied migration history drifts from the schema. - - name: Apply migrations - working-directory: packages/db - run: yarn prisma migrate deploy - - - name: Check for schema drift - working-directory: packages/db - 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: no new migration has a timestamp earlier than the latest on main. - - name: Check migration ordering - run: | - MIG_DIR=packages/db/prisma/migrations - # latest timestamp already on main - git fetch origin main --depth=1 - LATEST_ON_MAIN=$(git ls-tree -r --name-only origin/main -- "$MIG_DIR" \ - | sed -n "s#$MIG_DIR/\([0-9]\{14\}\)_.*#\1#p" | sort | tail -1) - echo "Latest migration on main: ${LATEST_ON_MAIN:-}" - # migrations added in this PR (present locally, not on main) - NEW=$(comm -23 \ - <(ls "$MIG_DIR" | sed -n 's/^\([0-9]\{14\}\)_.*/\1/p' | sort -u) \ - <(git ls-tree -r --name-only origin/main -- "$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_MAIN" ] && [ "$ts" -lt "$LATEST_ON_MAIN" ]; then - echo "❌ New migration $ts predates latest migration on main ($LATEST_ON_MAIN). Rename it with a current timestamp." - FAIL=1 - fi - done - [ "$FAIL" -eq 0 ] && echo "✅ Migration ordering OK" - exit $FAIL diff --git a/CHANGELOG.md b/CHANGELOG.md index c82dfee67..73d592935 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [EE] Added a context-window usage gauge to the Ask Sourcebot chat details, showing how much of the selected model's context window each turn occupies. Window sizes are resolved from the models.dev catalog. [#1370](https://github.com/sourcebot-dev/sourcebot/pull/1370) - Added language model input-modality and document capability resolution, automatically resolved from the models.dev catalog (falls back to text-only for uncatalogued/self-hosted models). [#1372](https://github.com/sourcebot-dev/sourcebot/pull/1372) - [EE] Added DPoP sender-constrained OAuth tokens for MCP clients. [#1395](https://github.com/sourcebot-dev/sourcebot/pull/1395) +- Added CI enforcement that Prisma migrations apply in order and reproduce `schema.prisma`, gating PRs (PR Gate) and release builds. [#1400](https://github.com/sourcebot-dev/sourcebot/pull/1400) ### Fixed - Send anonymous server-side PostHog events as personless so unauthenticated requests don't inflate person counts. [#1367](https://github.com/sourcebot-dev/sourcebot/pull/1367) From 43bb910e23c6566dcd646dd58efc34207be31a41 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Mon, 29 Jun 2026 16:16:39 -0700 Subject: [PATCH 3/5] chore: remove changelog entry Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 73d592935..c82dfee67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [EE] Added a context-window usage gauge to the Ask Sourcebot chat details, showing how much of the selected model's context window each turn occupies. Window sizes are resolved from the models.dev catalog. [#1370](https://github.com/sourcebot-dev/sourcebot/pull/1370) - Added language model input-modality and document capability resolution, automatically resolved from the models.dev catalog (falls back to text-only for uncatalogued/self-hosted models). [#1372](https://github.com/sourcebot-dev/sourcebot/pull/1372) - [EE] Added DPoP sender-constrained OAuth tokens for MCP clients. [#1395](https://github.com/sourcebot-dev/sourcebot/pull/1395) -- Added CI enforcement that Prisma migrations apply in order and reproduce `schema.prisma`, gating PRs (PR Gate) and release builds. [#1400](https://github.com/sourcebot-dev/sourcebot/pull/1400) ### Fixed - Send anonymous server-side PostHog events as personless so unauthenticated requests don't inflate person counts. [#1367](https://github.com/sourcebot-dev/sourcebot/pull/1367) From e0ec67542134601b7e38fdec60a54fc8435d0924 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Mon, 29 Jun 2026 16:21:12 -0700 Subject: [PATCH 4/5] test: add out-of-order migration to exercise PR gate check DO NOT MERGE. No-op migration timestamped 2020-01-01, earlier than the latest migration on main, to verify the ordering check fails. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../migrations/20200101000000_dummy_out_of_order/migration.sql | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 packages/db/prisma/migrations/20200101000000_dummy_out_of_order/migration.sql diff --git a/packages/db/prisma/migrations/20200101000000_dummy_out_of_order/migration.sql b/packages/db/prisma/migrations/20200101000000_dummy_out_of_order/migration.sql new file mode 100644 index 000000000..40c09e795 --- /dev/null +++ b/packages/db/prisma/migrations/20200101000000_dummy_out_of_order/migration.sql @@ -0,0 +1,3 @@ +-- Dummy no-op migration to test the out-of-order CI check. +-- Timestamp (2020-01-01) is intentionally earlier than the latest migration on main. +SELECT 1; From f074eb117369c6e0d48be85693fd016461ab2cb0 Mon Sep 17 00:00:00 2001 From: Brendan Kellam Date: Mon, 29 Jun 2026 16:32:20 -0700 Subject: [PATCH 5/5] fix migration --- .../migration.sql | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/db/prisma/migrations/{20200101000000_dummy_out_of_order => 20260629193001_dummy_out_of_order}/migration.sql (100%) diff --git a/packages/db/prisma/migrations/20200101000000_dummy_out_of_order/migration.sql b/packages/db/prisma/migrations/20260629193001_dummy_out_of_order/migration.sql similarity index 100% rename from packages/db/prisma/migrations/20200101000000_dummy_out_of_order/migration.sql rename to packages/db/prisma/migrations/20260629193001_dummy_out_of_order/migration.sql