#!/usr/bin/env bash # Validate that a commit has a promotion-eligible receipt (PASS) # or a non-expired audited waiver binding a successful receipt. # # Usage: # ./scripts/promotion-receipt-check.sh [--commit SHA] [--require-digests] # # Exit 0: eligible to promote # Exit 1: blocked (missing / product_fail / infra_fail / stale waiver / mismatch / missing digests) set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" RECEIPT_DIR="${ROOT}/.doc/test-reports/assets/promotion-receipts" COMMIT="" REQUIRE_DIGESTS=0 while [[ $# -gt 0 ]]; do case "$1" in --commit) COMMIT="${2:-}"; shift 2 ;; --require-digests) REQUIRE_DIGESTS=1; shift ;; -h|--help) sed -n '2,12p' "$0" | sed 's/^# \{0,1\}//' exit 0 ;; *) echo "unknown arg: $1" >&2; exit 1 ;; esac done if [[ -z "${COMMIT}" ]]; then COMMIT="$(git -C "${ROOT}" rev-parse HEAD)" fi COMMIT="$(echo "${COMMIT}" | tr -d '[:space:]' | tr 'A-F' 'a-f')" # Resolve full sha when possible; receipts may use full or abbreviated form. FULL_COMMIT="$(git -C "${ROOT}" rev-parse --verify "${COMMIT}^{commit}" 2>/dev/null || true)" if [[ -n "${FULL_COMMIT}" ]]; then COMMIT="${FULL_COMMIT}" fi find_receipt() { local sha="$1" local candidate if [[ -f "${RECEIPT_DIR}/${sha}.json" ]]; then echo "${RECEIPT_DIR}/${sha}.json" return 0 fi # Allow abbreviated filename match against full sha prefix. shopt -s nullglob for candidate in "${RECEIPT_DIR}"/*.json; do local base base="$(basename "${candidate}" .json)" [[ "${base}" == *.waiver ]] && continue [[ "${base}" == *.images ]] && continue if [[ "${sha}" == "${base}"* || "${base}" == "${sha}"* ]]; then echo "${candidate}" return 0 fi done return 1 } find_waiver() { local sha="$1" if [[ -f "${RECEIPT_DIR}/${sha}.waiver.json" ]]; then echo "${RECEIPT_DIR}/${sha}.waiver.json" return 0 fi shopt -s nullglob local candidate base for candidate in "${RECEIPT_DIR}"/*.waiver.json; do base="$(basename "${candidate}" .waiver.json)" if [[ "${sha}" == "${base}"* || "${base}" == "${sha}"* ]]; then echo "${candidate}" return 0 fi done return 1 } RECEIPT_PATH="" if RECEIPT_PATH="$(find_receipt "${COMMIT}")"; then : else RECEIPT_PATH="" fi WAIVER_PATH="" if WAIVER_PATH="$(find_waiver "${COMMIT}")"; then : else WAIVER_PATH="" fi export RECEIPT_PATH WAIVER_PATH COMMIT RECEIPT_DIR ROOT REQUIRE_DIGESTS python3 <<'PY' import json, os, sys from datetime import datetime, timezone commit = os.environ["COMMIT"] receipt_path = os.environ.get("RECEIPT_PATH") or "" waiver_path = os.environ.get("WAIVER_PATH") or "" receipt_dir = os.environ["RECEIPT_DIR"] root = os.environ.get("ROOT") or "." require_digests = os.environ.get("REQUIRE_DIGESTS") == "1" def load(path): with open(path, encoding="utf-8") as f: return json.load(f) def fail(msg, code=1): print(f"promotion-receipt-check: BLOCKED — {msg}", file=sys.stderr) sys.exit(code) def ok(msg): print(f"promotion-receipt-check: OK — {msg}") sys.exit(0) def require_image_digests(r, label): if not require_digests: return digests = r.get("image_digests") or {} missing = [k for k in ("app", "events", "pi-agent") if not digests.get(k)] if missing: fail(f"{label} missing image_digests for same-image promote: {', '.join(missing)}") def resolve_bound_receipt(binds): if not binds: return None candidates = [] if os.path.isabs(binds): candidates.append(binds) elif binds.startswith(".doc/"): candidates.append(os.path.join(root, binds)) candidates.append(os.path.join(receipt_dir, binds)) candidates.append(os.path.join(receipt_dir, f"{binds}.json")) for c in candidates: if os.path.isfile(c) and not c.endswith(".images.json") and not c.endswith(".waiver.json"): return c for name in os.listdir(receipt_dir): if name.endswith(".waiver.json") or name.endswith(".images.json") or not name.endswith(".json"): continue stem = name[:-5] if binds.startswith(stem) or stem.startswith(binds): return os.path.join(receipt_dir, name) return None now = datetime.now(timezone.utc) if receipt_path: r = load(receipt_path) verdict = r.get("verdict") r_commit = (r.get("commit") or "").lower() if r_commit and not (commit.startswith(r_commit) or r_commit.startswith(commit)): fail(f"receipt commit mismatch: receipt={r_commit} target={commit}") if verdict == "pass": require_image_digests(r, "PASS receipt") ok(f"PASS receipt for {commit[:12]} ({receipt_path})") elif verdict == "product_fail": fail(f"product_fail receipt rejects promotion ({receipt_path}); failure_class={r.get('failure_class')}") elif verdict == "infra_fail": if not waiver_path: fail(f"infra_fail receipt freezes promotion ({receipt_path}); provide timed waiver or re-run nightly") else: fail(f"unknown verdict {verdict!r} in {receipt_path}") if not waiver_path: fail(f"missing promotion receipt for {commit[:12]} under {receipt_dir}") w = load(waiver_path) w_commit = (w.get("commit") or "").lower() if w_commit and not (commit.startswith(w_commit) or w_commit.startswith(commit)): fail(f"waiver commit mismatch: waiver={w_commit} target={commit}") expires = w.get("expires_at") if not expires: fail(f"waiver missing expires_at ({waiver_path})") try: exp = datetime.fromisoformat(expires.replace("Z", "+00:00")) except Exception as e: fail(f"waiver expires_at unparseable: {expires} ({e})") if exp.tzinfo is None: exp = exp.replace(tzinfo=timezone.utc) if now > exp: fail(f"waiver expired at {expires} ({waiver_path})") owner = w.get("owner") or "" reason = w.get("reason") or "" if not owner or not reason: fail(f"waiver requires owner and reason ({waiver_path})") binds = w.get("binds_receipt") or w.get("binds_recent_pass_receipt") or "" bound_path = resolve_bound_receipt(binds) if not bound_path: fail(f"waiver binds_receipt not found: {binds!r}") br = load(bound_path) if br.get("verdict") != "pass": fail(f"waiver bound receipt is not PASS: {bound_path}") require_image_digests(br, "waiver-bound PASS receipt") ok( f"audited waiver for {commit[:12]} by {owner} until {expires}; " f"bound PASS={bound_path}" ) PY