#!/usr/bin/env bash # Build candidate app (+events) and pi-agent images once for a commit. # Writes .doc/test-reports/assets/promotion-receipts/.images.json # # Usage: # ./scripts/candidate-images-build.sh [--commit SHA] [--force] set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "${ROOT}" COMMIT="" FORCE=0 while [[ $# -gt 0 ]]; do case "$1" in --commit) COMMIT="${2:-}"; shift 2 ;; --force) FORCE=1; shift ;; -h|--help) sed -n '2,8p' "$0" | sed 's/^# \{0,1\}//' exit 0 ;; *) echo "unknown arg: $1" >&2; exit 1 ;; esac done if [[ -z "${COMMIT}" ]]; then COMMIT="$(git rev-parse HEAD)" fi COMMIT="$(git rev-parse --verify "${COMMIT}^{commit}")" python3 "${ROOT}/scripts/agentctl.py" guard build --commit "${COMMIT}" SHORT="${COMMIT:0:12}" RECEIPT_DIR="${ROOT}/.doc/test-reports/assets/promotion-receipts" OUT="${RECEIPT_DIR}/${COMMIT}.images.json" mkdir -p "${RECEIPT_DIR}" if [[ -f "${OUT}" && "${FORCE}" -ne 1 ]]; then echo "images.json already exists (immutable): ${OUT}" >&2 echo "hint: use --force to rebuild local fixtures" >&2 python3 - "${OUT}" "${COMMIT}" <<'CHECK_MANIFEST' import json, sys with open(sys.argv[1], encoding="utf-8") as source: manifest = json.load(source) if manifest.get("commit") != sys.argv[2]: raise SystemExit("candidate manifest commit mismatch") for field in ("image_ids", "image_digests", "tags"): if not isinstance(manifest.get(field), dict) or not all(manifest[field].get(k) for k in ("app", "events", "pi-agent")): raise SystemExit("incomplete candidate manifest") CHECK_MANIFEST cat "${OUT}" exit 0 fi # Docker reads an immutable export of the requested commit, never the mutable # checkout or the nightly evidence generated after checkout. BUILD_CONTEXT="$(mktemp -d "${TMPDIR:-/tmp}/agent-image-context.XXXXXX")" trap 'rm -rf -- "${BUILD_CONTEXT}"' EXIT trap 'exit 130' INT trap 'exit 143' TERM trap 'exit 129' HUP git archive --format=tar "${COMMIT}" | tar -xf - -C "${BUILD_CONTEXT}" SOURCE_TREE="$(git rev-parse "${COMMIT}^{tree}")" APP_TAG="shuimu-agent-app:candidate-${SHORT}" EVENTS_TAG="shuimu-agent-events:candidate-${SHORT}" PI_TAG="shuimu-pi-agent:candidate-${SHORT}" echo "==> building app/events image ${APP_TAG}..." docker build -f "${BUILD_CONTEXT}/deploy/app/Dockerfile" -t "${APP_TAG}" "${BUILD_CONTEXT}" # events binary lives in the same image docker tag "${APP_TAG}" "${EVENTS_TAG}" echo "==> building pi-agent image ${PI_TAG}..." docker build -f "${BUILD_CONTEXT}/deploy/pi-agent/Dockerfile" -t "${PI_TAG}" "${BUILD_CONTEXT}" image_id() { docker inspect --format '{{.Id}}' "$1" } repo_digest_or_id() { local name="$1" local repo repo="$(docker inspect --format '{{index .RepoDigests 0}}' "${name}" 2>/dev/null || true)" if [[ -n "${repo}" && "${repo}" != "" ]]; then printf '%s' "${repo}" else image_id "${name}" fi } APP_ID="$(image_id "${APP_TAG}")" EVENTS_ID="$(image_id "${EVENTS_TAG}")" PI_ID="$(image_id "${PI_TAG}")" APP_DIGEST="$(repo_digest_or_id "${APP_TAG}")" EVENTS_DIGEST="$(repo_digest_or_id "${EVENTS_TAG}")" PI_DIGEST="$(repo_digest_or_id "${PI_TAG}")" RECORDED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)" export COMMIT SHORT RECORDED_AT APP_TAG EVENTS_TAG PI_TAG SOURCE_TREE export APP_ID EVENTS_ID PI_ID APP_DIGEST EVENTS_DIGEST PI_DIGEST OUT python3 -c ' import json, os, tempfile payload = { "commit": os.environ["COMMIT"], "source_tree": os.environ["SOURCE_TREE"], "source_context": "git-archive", "recorded_at": os.environ["RECORDED_AT"], "tags": { "app": os.environ["APP_TAG"], "events": os.environ["EVENTS_TAG"], "pi-agent": os.environ["PI_TAG"], }, "image_ids": { "app": os.environ["APP_ID"], "events": os.environ["EVENTS_ID"], "pi-agent": os.environ["PI_ID"], }, "image_digests": { "app": os.environ["APP_DIGEST"], "events": os.environ["EVENTS_DIGEST"], "pi-agent": os.environ["PI_DIGEST"], }, } out = os.environ["OUT"] fd, temporary = tempfile.mkstemp(prefix=".candidate-images-", dir=os.path.dirname(out)) try: with os.fdopen(fd, "w", encoding="utf-8") as f: json.dump(payload, f, indent=2, ensure_ascii=False) f.write("\n") f.flush() os.fsync(f.fileno()) os.replace(temporary, out) directory = os.open(os.path.dirname(out), os.O_DIRECTORY) try: os.fsync(directory) finally: os.close(directory) finally: if os.path.exists(temporary): os.unlink(temporary) print(os.environ["OUT"]) ' echo "wrote candidate images manifest: ${OUT}" echo "app=${APP_DIGEST}" echo "events=${EVENTS_DIGEST}" echo "pi-agent=${PI_DIGEST}"