diff --git a/docs/plans/CASAN_BACKLOG_STATUS.md b/docs/plans/CASAN_BACKLOG_STATUS.md index 10e7031..cca2680 100644 --- a/docs/plans/CASAN_BACKLOG_STATUS.md +++ b/docs/plans/CASAN_BACKLOG_STATUS.md @@ -43,6 +43,7 @@ | T2.5 | Billing-API telemetry thật | 🟡 Docker lab ✅ (billing API mock) | `billing-api` @ `:19093/usage`; `provider-usage-fetch.sh` import được provider telemetry. Prod: OpenAI/Anthropic usage API thật + key. | | T2.6 | Sandbox: rootless/nsjail + base image CI | 🟡 (container isolation live via Docker) | Thêm profile nsjail/bubblewrap cho Linux CI (không cần Docker daemon); hardened base image tối thiểu. `sandbox-container.sh` đã có bản Docker. | | T2.7 | Backup/restore + restore drill | 🟡 automated + test | `state-backup.sh` creates manifest/hash-bound state snapshots, requires encryption key in prod, restores only to an empty explicit directory; `phase-state-backup` is in CI. Remaining: customer object-store replication, retention/RPO/RTO, and a retained production restore-drill record. | +| T2.8 | Upgrade/rollback compatibility | 🟡 automated + test | `upgrade-compatibility.sh` is fail-closed and permits only explicit matrix rules; the 1.0.x patch/rollback rule requires provenance, backup, artifact smoke and post-deploy Evidence Pack verification. Remaining: a reviewed migration rule for every future minor/major release and a retained live rollout/rollback record. | ## TIER 3 — Platform plans (INDEX 01–12, việc lớn nhiều phiên) 📋 diff --git a/infra/production/UPGRADE_ROLLBACK.md b/infra/production/UPGRADE_ROLLBACK.md new file mode 100644 index 0000000..9aad9db --- /dev/null +++ b/infra/production/UPGRADE_ROLLBACK.md @@ -0,0 +1,18 @@ +# Upgrade and Rollback Runbook + +The compatibility gate is mandatory before every deployment or rollback: + +```bash +bash packages/casan-harness/scripts/bash/upgrade-compatibility.sh check \ + --from 1.0.0 --to 1.0.1 +``` + +Only an explicit rule in `compatibility-matrix.json` can permit a version pair. +Before rollout, verify provenance, create and verify a state backup, smoke the +extracted artifact, deploy immutable image digests, and verify an Evidence Pack +after deployment. A rollback requires the same gate with `--rollback`, plus an +approved change record and a verified backup. + +Do not infer compatibility from matching image tags, passing unit tests, or +successful container startup. Add and review a new matrix entry before any +cross-minor or cross-major migration. diff --git a/infra/production/compatibility-matrix.json b/infra/production/compatibility-matrix.json new file mode 100644 index 0000000..75ecb10 --- /dev/null +++ b/infra/production/compatibility-matrix.json @@ -0,0 +1,18 @@ +{ + "schema_version": "1.0", + "product": "CASAN", + "rules": [ + { + "from": "1.0.x", + "to": "1.0.x", + "policy_schema": "1", + "evidence_pack_schema": "1.0-mvp", + "rollback": "supported", + "notes": "Patch-level upgrades only. Back up runtime state and verify the candidate artifact before rollout." + } + ], + "unsupported": [ + "Cross-major upgrades require a reviewed migration entry before deployment.", + "Downgrades that change policy or Evidence Pack schema are denied until an explicit rollback entry is added." + ] +} diff --git a/packages/casan-harness/scripts/bash/ci-harness-gate.sh b/packages/casan-harness/scripts/bash/ci-harness-gate.sh index 3e300e5..93afb4b 100755 --- a/packages/casan-harness/scripts/bash/ci-harness-gate.sh +++ b/packages/casan-harness/scripts/bash/ci-harness-gate.sh @@ -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" diff --git a/packages/casan-harness/scripts/bash/upgrade-compatibility.sh b/packages/casan-harness/scripts/bash/upgrade-compatibility.sh new file mode 100755 index 0000000..8a2b92b --- /dev/null +++ b/packages/casan-harness/scripts/bash/upgrade-compatibility.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 diff --git a/packages/casan-harness/tests/phase-upgrade-compatibility-tests.sh b/packages/casan-harness/tests/phase-upgrade-compatibility-tests.sh new file mode 100755 index 0000000..e885995 --- /dev/null +++ b/packages/casan-harness/tests/phase-upgrade-compatibility-tests.sh @@ -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 ]]