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>
148 lines
4.5 KiB
Bash
Executable File
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
|