Files
Agent-System/scripts/candidate-images-build.sh
weichen fed87ad14b feat(governance): enforce managed worktrees and scoped agent operations
Consolidate approved policies and add local lease, document CAS, commit, candidate and environment gates. Preserve conservative recovery and cleanup limits.
2026-09-21 14:41:40 +00:00

148 lines
4.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build candidate app (+events) and pi-agent images once for a commit.
# Writes .doc/test-reports/assets/promotion-receipts/<sha>.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}" != "<no value>" ]]; 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}"