113 lines
4.2 KiB
Bash
113 lines
4.2 KiB
Bash
|
|
#!/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
|