veza/scripts/check-migration-backward-compat.sh

113 lines
4.2 KiB
Bash
Raw Normal View History

#!/usr/bin/env bash
# check-migration-backward-compat.sh — pre-deploy gate for canary releases.
#
# Refuses to deploy when the latest migration is NOT backward-compatible
# with the running schema. Backward-compat = the OLD code can still
# read/write against the NEW schema for at least one canary window
# (otherwise canary mode is meaningless ; the old node would crash on
# the first request that touches a removed column).
#
# Heuristic : reject migrations that contain any of these patterns :
# - DROP COLUMN
# - DROP TABLE
# - ALTER COLUMN ... TYPE (type change is rarely backward-compat)
# - ADD COLUMN ... NOT NULL (without DEFAULT — old code can't INSERT)
# - DROP CONSTRAINT
# - DROP INDEX UNIQUE (existing data may already violate)
#
# This is a STATIC check ; some patterns are false-positives (e.g.
# DROP COLUMN of a column that no code reads). When a real migration
# is flagged, the operator either :
# 1. Splits the migration : ship the additive part now, drop in v+1
# after old-version backends are decommissioned.
# 2. Bypasses with FORCE_MIGRATE=1 + a justification in the commit
# message of the migration file.
#
# v1.0.9 W5 Day 23.
#
# Usage :
# bash scripts/check-migration-backward-compat.sh
#
# Required env :
# MIGRATIONS_DIR default veza-backend-api/migrations
# GIT_RANGE default origin/main..HEAD ; the range to inspect for
# newly-added migration files
# Optional env :
# FORCE_MIGRATE=1 bypass with a logged warning. Use sparingly.
#
# Exit codes :
# 0 — all new migrations are backward-compat (or FORCE_MIGRATE=1)
# 1 — at least one migration carries a forbidden pattern
# 3 — required tool missing / config error
set -euo pipefail
MIGRATIONS_DIR=${MIGRATIONS_DIR:-veza-backend-api/migrations}
GIT_RANGE=${GIT_RANGE:-origin/main..HEAD}
FORCE_MIGRATE=${FORCE_MIGRATE:-0}
log() { printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" >&2; }
fail() { log "FAIL: $*"; exit "${2:-1}"; }
require() {
command -v "$1" >/dev/null 2>&1 || fail "required tool missing: $1" 3
}
require git
require grep
require date
# Patterns that indicate non-backward-compat schema change.
# Heredoc preserves the pipe characters as alternations.
FORBIDDEN_PATTERNS='DROP COLUMN|DROP TABLE|ALTER COLUMN [A-Za-z_]+ TYPE|ADD COLUMN [A-Za-z_]+ [^,;]* NOT NULL[^,;]*(;|$)|DROP CONSTRAINT|DROP INDEX [A-Za-z_]*UNIQUE'
# Identify newly-added migration files in the current range.
new_migrations=$(git diff --name-only --diff-filter=A "$GIT_RANGE" -- "$MIGRATIONS_DIR" 2>/dev/null \
| grep -E "^${MIGRATIONS_DIR}/[0-9]+_.*\.sql$" || true)
if [ -z "$new_migrations" ]; then
log "no new migrations in $GIT_RANGE — nothing to check"
exit 0
fi
log "checking $(echo "$new_migrations" | wc -l) new migration(s) in $GIT_RANGE"
findings=0
for f in $new_migrations; do
log " scanning $f"
# -i case-insensitive ; -E extended regex ; -n line numbers
matches=$(grep -inE "$FORBIDDEN_PATTERNS" "$f" || true)
if [ -n "$matches" ]; then
findings=$((findings + 1))
log ""
log " ⚠ NON-BACKWARD-COMPAT pattern in $f :"
echo "$matches" | sed 's/^/ /' >&2
# Special case : ADD COLUMN ... NOT NULL ... DEFAULT <x> is fine.
# The regex above tries to exclude that but the match-then-filter
# approach is more reliable than a single regex. Suppress matches
# that include `DEFAULT` on the same line.
real=$(echo "$matches" | grep -ivE "DEFAULT" || true)
if [ -z "$real" ]; then
log " ↳ all matches include DEFAULT clause — actually backward-compat"
findings=$((findings - 1))
fi
fi
done
if [ "$findings" -gt 0 ]; then
log ""
log "$findings migration(s) flagged as potentially non-backward-compat."
if [ "$FORCE_MIGRATE" = "1" ]; then
log "FORCE_MIGRATE=1 set — proceeding anyway."
exit 0
fi
log ""
log "Options to proceed :"
log " 1. Split the migration : ship the additive part now, drop the"
log " non-compat part in v+1 after old backends are off."
log " 2. Set FORCE_MIGRATE=1 if you accept the risk + document the"
log " justification in the migration's commit message."
exit 1
fi
log "PASS : all new migrations are backward-compat"
exit 0