Consolidate approved policies and add local lease, document CAS, commit, candidate and environment gates. Preserve conservative recovery and cleanup limits.
148 lines
4.6 KiB
Bash
Executable File
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}"
|