#!/usr/bin/env bash set -euo pipefail # Fail-closed compatibility gate for an upgrade or rollback. This command does # not deploy. It produces a machine-readable plan that a release workflow must # verify before it changes runtime state. # # upgrade-compatibility.sh check --from 1.0.0 --to 1.0.1 [--matrix FILE] # upgrade-compatibility.sh check --from 1.0.1 --to 1.0.0 --rollback [--matrix FILE] SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/casan-paths.sh" CMD="${1:-}"; shift || true FROM=""; TO=""; ROLLBACK=false MATRIX="${CASAN_COMPATIBILITY_MATRIX:-$CASAN_APP_ROOT/infra/production/compatibility-matrix.json}" while [[ "$#" -gt 0 ]]; do case "$1" in --from) FROM="${2:-}"; shift 2 ;; --to) TO="${2:-}"; shift 2 ;; --matrix) MATRIX="${2:-}"; shift 2 ;; --rollback) ROLLBACK=true; shift ;; *) echo "UPGRADE_COMPATIBILITY_FAIL reason=unknown_option option=$1" >&2; exit 64 ;; esac done [[ "$CMD" == "check" && -n "$FROM" && -n "$TO" && -f "$MATRIX" ]] || { echo "Usage: upgrade-compatibility.sh check --from X.Y.Z --to X.Y.Z [--rollback] [--matrix FILE]" >&2; exit 64; } python3 - "$MATRIX" "$FROM" "$TO" "$ROLLBACK" <<'PY' import json, re, sys matrix, source, target, rollback = sys.argv[1:] def parse(v): m = re.fullmatch(r'(\d+)\.(\d+)\.(\d+)', v) if not m: raise ValueError('invalid_semver') return tuple(map(int, m.groups())) try: src, dst = parse(source), parse(target) data = json.load(open(matrix, encoding='utf8')) except Exception as e: print(f'UPGRADE_COMPATIBILITY_FAIL reason={str(e)[:80]}', file=sys.stderr); raise SystemExit(1) def matches(pattern, version): m = re.fullmatch(r'(\d+)\.(\d+)\.(x|\d+)', pattern) if not m: return False a,b,c=m.groups() return version[0] == int(a) and version[1] == int(b) and (c == 'x' or version[2] == int(c)) for rule in data.get('rules', []): if matches(str(rule.get('from','')), src) and matches(str(rule.get('to','')), dst): if rollback == 'true' and rule.get('rollback') != 'supported': continue print(json.dumps({'decision':'allow','from':source,'to':target,'rollback':rollback == 'true', 'policy_schema':rule.get('policy_schema'),'evidence_pack_schema':rule.get('evidence_pack_schema'), 'required_steps':['verify_release_provenance','state_backup','artifact_smoke','post_deploy_evidence_verify']}, sort_keys=True)) raise SystemExit(0) print(json.dumps({'decision':'deny','from':source,'to':target,'rollback':rollback == 'true', 'reason':'no_approved_compatibility_rule'}, sort_keys=True), file=sys.stderr) raise SystemExit(1) PY