Files
Agent-System/scripts/candidate-images-apply.sh
weichenandCursor da15eb91b6 feat(infra): Wave 2C same-image digest pin for test/main promote
Build/apply candidate images once, require digests on promote, and keep
pinned env-service from rebuilding divergent images.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 14:44:26 +00:00

148 lines
4.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# Apply candidate image digests from a promotion receipt to test|main.
# Retags local images and writes configs/.env.<env>.images for pinned compose.
#
# Usage:
# ./scripts/candidate-images-apply.sh <test|main> [--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 <test|main> [--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