#!/usr/bin/env bash # Apply candidate image digests from a promotion receipt to test|main. # Retags local images and writes configs/.env..images for pinned compose. # # Usage: # ./scripts/candidate-images-apply.sh [--commit SHA] set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "${ROOT}" ENV_NAME="${1:-}" shift || true COMMIT="" while [[ $# -gt 0 ]]; do case "$1" in --commit) COMMIT="${2:-}"; shift 2 ;; -h|--help) sed -n '2,8p' "$0" | sed 's/^# \{0,1\}//' exit 0 ;; *) echo "unknown arg: $1" >&2; exit 1 ;; esac done case "${ENV_NAME}" in test|main) ;; *) echo "usage: $0 [--commit SHA]" >&2; exit 1 ;; esac if [[ -z "${COMMIT}" ]]; then if [[ "${ENV_NAME}" == "test" ]]; then COMMIT="$(git rev-parse dev)" else COMMIT="$(git rev-parse test)" fi fi COMMIT="$(git rev-parse --verify "${COMMIT}^{commit}" 2>/dev/null || echo "${COMMIT}")" COMMIT="$(echo "${COMMIT}" | tr 'A-F' 'a-f')" RECEIPT_DIR="${ROOT}/.doc/test-reports/assets/promotion-receipts" RECEIPT="" if [[ -f "${RECEIPT_DIR}/${COMMIT}.json" ]]; then RECEIPT="${RECEIPT_DIR}/${COMMIT}.json" else shopt -s nullglob for f in "${RECEIPT_DIR}"/*.json; do base="$(basename "${f}" .json)" [[ "${base}" == *.waiver ]] && continue [[ "${base}" == *.images ]] && continue if [[ "${COMMIT}" == "${base}"* || "${base}" == "${COMMIT}"* ]]; then RECEIPT="${f}" break fi done fi if [[ -z "${RECEIPT}" || ! -f "${RECEIPT}" ]]; then echo "error: no promotion receipt for ${COMMIT}" >&2 exit 1 fi export RECEIPT ENV_NAME ROOT python3 <<'PY' import json, os, subprocess, sys receipt_path = os.environ["RECEIPT"] env_name = os.environ["ENV_NAME"] root = os.environ["ROOT"] with open(receipt_path, encoding="utf-8") as f: receipt = json.load(f) verdict = receipt.get("verdict") if verdict != "pass": # allow apply when digests present on a pass-only path; still warn print(f"warning: receipt verdict={verdict!r} (expected pass)", file=sys.stderr) digests = receipt.get("image_digests") or {} app_d = digests.get("app") or "" events_d = digests.get("events") or "" pi_d = digests.get("pi-agent") or "" missing = [k for k, v in (("app", app_d), ("events", events_d), ("pi-agent", pi_d)) if not v] if missing: print(f"error: receipt missing image_digests: {', '.join(missing)}", file=sys.stderr) sys.exit(1) def resolve_local_id(ref: str) -> str: # ref may be sha256:ID, name@sha256:..., or full image id candidates = [ref] if ref.startswith("sha256:"): candidates.append(ref) elif "@sha256:" in ref: candidates.append(ref.split("@", 1)[1]) for c in candidates: try: out = subprocess.check_output( ["docker", "image", "inspect", "--format", "{{.Id}}", c], stderr=subprocess.DEVNULL, text=True, ).strip() if out: return out except subprocess.CalledProcessError: continue print(f"error: image not found locally for digest/ref: {ref}", file=sys.stderr) print("hint: run scripts/candidate-images-build.sh for this commit first", file=sys.stderr) sys.exit(1) app_id = resolve_local_id(app_d) events_id = resolve_local_id(events_d) pi_id = resolve_local_id(pi_d) tags = { "app": f"shuimu-agent-app:{env_name}", "events": f"shuimu-agent-events:{env_name}", "pi-agent": f"shuimu-pi-agent:{env_name}", } for img_id, tag in ((app_id, tags["app"]), (events_id, tags["events"]), (pi_id, tags["pi-agent"])): subprocess.check_call(["docker", "tag", img_id, tag]) got = subprocess.check_output( ["docker", "image", "inspect", "--format", "{{.Id}}", tag], text=True ).strip() if got != img_id: print(f"error: retag mismatch for {tag}: want {img_id} got {got}", file=sys.stderr) sys.exit(1) print(f"tagged {tag} -> {got}") # Pin compose to retagged tags (same IDs as receipt). env_path = os.path.join(root, f"configs/.env.{env_name}.images") lines = [ f"# generated by candidate-images-apply.sh from {os.path.basename(receipt_path)}", f"APP_IMAGE={tags['app']}", f"EVENTS_IMAGE={tags['events']}", f"PI_AGENT_IMAGE={tags['pi-agent']}", f"PINNED_IMAGE_MODE=1", f"PINNED_COMMIT={receipt.get('commit') or ''}", f"PINNED_APP_DIGEST={app_d}", f"PINNED_EVENTS_DIGEST={events_d}", f"PINNED_PI_AGENT_DIGEST={pi_d}", "", ] with open(env_path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) print(f"wrote {env_path}") PY