From 1cad57f6b505833c555815d8e83e6ee06501b7e4 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Sat, 14 Feb 2026 19:09:09 +0000 Subject: [PATCH 1/3] ci(policy): enforce one migration per PR --- .github/workflows/ci.yml | 7 +++++ docs/policy/one-migration-per-pr.md | 23 ++++++++++++++ scripts/ci/one_migration_per_pr.sh | 49 +++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 docs/policy/one-migration-per-pr.md create mode 100755 scripts/ci/one_migration_per_pr.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61edc03..d545ce7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,13 @@ jobs: + - name: Enforce one migration per PR + if: ${{ github.event_name == 'pull_request' }} + env: + GITHUB_BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: | + ./scripts/ci/one_migration_per_pr.sh + - name: Run migration integrity gate if: ${{ github.event_name == 'pull_request' }} run: | diff --git a/docs/policy/one-migration-per-pr.md b/docs/policy/one-migration-per-pr.md new file mode 100644 index 0000000..d118829 --- /dev/null +++ b/docs/policy/one-migration-per-pr.md @@ -0,0 +1,23 @@ +# Policy: one DB migration per PR + +## Rule +If a pull request adds migration files under: + +- `backend/migrations/versions/*.py` + +…then it must add **no more than one** migration file. + +## Why +- Makes review and rollback simpler. +- Reduces surprise Alembic multiple-head situations. +- Keeps CI/installer failures easier to debug. + +## Common exceptions / guidance +- If you have multiple Alembic heads, prefer creating **one** merge migration. +- If changes are unrelated, split into multiple PRs. + +## CI enforcement +CI runs `scripts/ci/one_migration_per_pr.sh` on PRs and fails if >1 migration file is added. + +## Notes +This policy does not replace the existing migration integrity gate (`make backend-migration-check`). It is a lightweight guardrail to prevent multi-migration PRs. diff --git a/scripts/ci/one_migration_per_pr.sh b/scripts/ci/one_migration_per_pr.sh new file mode 100755 index 0000000..83ab900 --- /dev/null +++ b/scripts/ci/one_migration_per_pr.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Enforce: if a PR adds migration files under backend/migrations/versions/, it must add <= 1. +# Rationale: keeps review/rollback straightforward and avoids surprise multiple-head merges. + +if [ "${GITHUB_EVENT_NAME:-}" != "pull_request" ]; then + echo "Not a pull_request event; skipping one-migration-per-PR gate." + exit 0 +fi + +BASE_SHA="${GITHUB_BASE_SHA:-${GITHUB_EVENT_PULL_REQUEST_BASE_SHA:-}}" +HEAD_SHA="${GITHUB_SHA:-}" + +if [ -z "$BASE_SHA" ] || [ -z "$HEAD_SHA" ]; then + echo "Missing BASE_SHA/HEAD_SHA (BASE_SHA='$BASE_SHA', HEAD_SHA='$HEAD_SHA')" + exit 2 +fi + +# Ensure base is present in shallow clones. +git fetch --no-tags --depth=1 origin "$BASE_SHA" || true + +CHANGED=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" || true) +if [ -z "$CHANGED" ]; then + echo "No changed files detected." + exit 0 +fi + +MIGRATIONS=$(echo "$CHANGED" | grep -E '^backend/migrations/versions/.*\.py$' || true) +COUNT=$(echo "$MIGRATIONS" | sed '/^$/d' | wc -l | tr -d ' ') + +if [ "$COUNT" -le 1 ]; then + echo "Migration gate OK (migrations_added=$COUNT)." + exit 0 +fi + +echo "Migration gate FAILED: this PR adds $COUNT migration files; policy allows at most 1." +echo + +echo "Migrations detected:" +echo "$MIGRATIONS" +echo + +echo "How to fix:" +echo "- Consolidate schema changes into a single migration file (squash)." +echo "- If you have multiple Alembic heads, create ONE merge migration instead of several." +echo "- If you truly need multiple migrations, split into multiple PRs." + +exit 1 From c89db7677dc52c089764fed5e2b58a2c940560d5 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Sat, 14 Feb 2026 19:48:19 +0000 Subject: [PATCH 2/3] fix(ci): only count added migrations in one_migration_per_pr gate --- scripts/ci/one_migration_per_pr.sh | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/scripts/ci/one_migration_per_pr.sh b/scripts/ci/one_migration_per_pr.sh index 83ab900..a40ce8c 100755 --- a/scripts/ci/one_migration_per_pr.sh +++ b/scripts/ci/one_migration_per_pr.sh @@ -20,13 +20,15 @@ fi # Ensure base is present in shallow clones. git fetch --no-tags --depth=1 origin "$BASE_SHA" || true -CHANGED=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" || true) -if [ -z "$CHANGED" ]; then - echo "No changed files detected." +# Only count *newly added* migration files. Modified/deleted migrations should not trip this gate. +# See review thread: https://github.com/abhi1693/openclaw-mission-control/pull/136#discussion_r2807812935 +ADDED_FILES=$(git diff --name-only --diff-filter=A "$BASE_SHA" "$HEAD_SHA" || true) +if [ -z "$ADDED_FILES" ]; then + echo "No added files detected." exit 0 fi -MIGRATIONS=$(echo "$CHANGED" | grep -E '^backend/migrations/versions/.*\.py$' || true) +MIGRATIONS=$(echo "$ADDED_FILES" | grep -E '^backend/migrations/versions/.*\.py$' || true) COUNT=$(echo "$MIGRATIONS" | sed '/^$/d' | wc -l | tr -d ' ') if [ "$COUNT" -le 1 ]; then From e919987351c9e570aa34f880daa017223c00f610 Mon Sep 17 00:00:00 2001 From: Abhimanyu Saharan Date: Sat, 14 Feb 2026 19:49:56 +0000 Subject: [PATCH 3/3] chore(ci): tighten migration pattern and fail if base SHA missing --- scripts/ci/one_migration_per_pr.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/scripts/ci/one_migration_per_pr.sh b/scripts/ci/one_migration_per_pr.sh index a40ce8c..c56e2fd 100755 --- a/scripts/ci/one_migration_per_pr.sh +++ b/scripts/ci/one_migration_per_pr.sh @@ -18,7 +18,12 @@ if [ -z "$BASE_SHA" ] || [ -z "$HEAD_SHA" ]; then fi # Ensure base is present in shallow clones. +# NOTE: fetch may fail due to fetch-depth/ref settings; validate the commit exists after fetch. git fetch --no-tags --depth=1 origin "$BASE_SHA" || true +if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then + echo "Base commit '$BASE_SHA' not found locally even after fetch. Check checkout fetch-depth / ref configuration." + exit 2 +fi # Only count *newly added* migration files. Modified/deleted migrations should not trip this gate. # See review thread: https://github.com/abhi1693/openclaw-mission-control/pull/136#discussion_r2807812935 @@ -28,7 +33,9 @@ if [ -z "$ADDED_FILES" ]; then exit 0 fi -MIGRATIONS=$(echo "$ADDED_FILES" | grep -E '^backend/migrations/versions/.*\.py$' || true) +# Be slightly strict: avoid counting non-migration python files (e.g. __init__.py). +# Alembic versions typically look like: _.py +MIGRATIONS=$(echo "$ADDED_FILES" | grep -E '^backend/migrations/versions/[0-9a-f]+_.*\.py$' || true) COUNT=$(echo "$MIGRATIONS" | sed '/^$/d' | wc -l | tr -d ' ') if [ "$COUNT" -le 1 ]; then