feat: gate upgrades with compatibility matrix
This commit is contained in:
@@ -138,6 +138,7 @@ run "phase-sec24-supplychain" bash "$TESTS/phase-sec24-tests.sh"
|
||||
run "phase-sec25-attestation" bash "$TESTS/phase-sec25-tests.sh"
|
||||
run "phase-production-handoff" bash "$TESTS/phase-production-preflight-tests.sh"
|
||||
run "phase-state-backup" bash "$TESTS/phase-state-backup-tests.sh"
|
||||
run "phase-upgrade-compatibility" bash "$TESTS/phase-upgrade-compatibility-tests.sh"
|
||||
run "phase-release-provenance" bash "$TESTS/phase-release-provenance-tests.sh"
|
||||
run "phase-service-desk-onboard" bash "$TESTS/phase-service-desk-onboard-tests.sh"
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
#!/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
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/../scripts/bash/casan-paths.sh"
|
||||
S="$CASAN_HARNESS_ROOT/scripts/bash/upgrade-compatibility.sh"
|
||||
M="$CASAN_APP_ROOT/infra/production/compatibility-matrix.json"
|
||||
PASS=0; FAIL=0
|
||||
pass(){ echo "PASS: $1"; PASS=$((PASS+1)); }; fail(){ echo "FAIL: $1"; FAIL=$((FAIL+1)); }
|
||||
if bash "$S" check --from 1.0.0 --to 1.0.1 --matrix "$M" | grep -q '"decision": "allow"'; then pass "patch upgrade is explicitly allowed"; else fail "patch upgrade denied"; fi
|
||||
if bash "$S" check --from 1.0.1 --to 1.0.0 --rollback --matrix "$M" | grep -q '"rollback": true'; then pass "approved rollback is explicitly allowed"; else fail "approved rollback denied"; fi
|
||||
if bash "$S" check --from 1.0.1 --to 2.0.0 --matrix "$M" >/dev/null 2>&1; then fail "unreviewed major upgrade accepted"; else pass "unreviewed major upgrade denied"; fi
|
||||
if bash "$S" check --from invalid --to 1.0.0 --matrix "$M" >/dev/null 2>&1; then fail "invalid version accepted"; else pass "invalid version denied"; fi
|
||||
echo "===== UPGRADE COMPATIBILITY SUMMARY: PASS=$PASS FAIL=$FAIL ====="; [[ "$FAIL" -eq 0 ]]
|
||||
Reference in New Issue
Block a user