#!/usr/bin/env bash # Build an ISO from the signed pending OTA byte domain, before feed promotion. # Invoke under flock /tmp/opencode/release-heavy.lock for the entire operation. set -euo pipefail REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$REPO" UNBUNDLED=1 RC_OVERRIDE='' TARGET_PLAN='' while [ "$#" -gt 0 ]; do case "$1" in --bundled) UNBUNDLED=0; shift ;; --rc) RC_OVERRIDE="${2:?--rc requires a number}"; shift 2 ;; --os-plan-sha256) TARGET_PLAN="${2:?--os-plan-sha256 requires a digest}"; shift 2 ;; *) echo "Unsupported flag: $1 (release gates cannot be skipped)" >&2; exit 2 ;; esac done [[ "$RC_OVERRIDE" = '' || "$RC_OVERRIDE" =~ ^[0-9]+$ ]] || exit 2 [[ "$TARGET_PLAN" = '' || "$TARGET_PLAN" =~ ^[0-9a-f]{64}$ ]] || exit 2 VERSION=$(python3 -c 'import tomllib; print(tomllib.load(open("core/archipelago/Cargo.toml", "rb"))["package"]["version"])') SOURCE_COMMIT=$(python3 scripts/release_payload.py source --version "$VERSION") MANIFEST="$REPO/releases/pending/v$VERSION/manifest.json" if [ -n "$TARGET_PLAN" ]; then MANIFEST="$REPO/releases/pending/v$VERSION/targets/$TARGET_PLAN/manifest.json" test "$TARGET_PLAN" = "$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["os_update"]["plan_sha256"])' "$MANIFEST")" fi ARTIFACTS="$REPO/releases/v$VERSION" BACKEND="$ARTIFACTS/archipelago" FRONTEND="$ARTIFACTS/archipelago-frontend-$VERSION.tar.gz" bash scripts/check-release-manifest.sh "$MANIFEST" test "$SOURCE_COMMIT" = "$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["source_commit"])' "$MANIFEST")" "$BACKEND" ceremony verify "$MANIFEST" python3 scripts/release_payload.py companion bash tests/release/run.sh python3 scripts/check-app-catalog-drift.py --release --strict command -v qemu-system-x86_64 >/dev/null # The ISO receives the exact flat archive checked above, not a mutable dist that # merely contains the version string. Keep it for post-build byte comparison. WORK=$(mktemp -d -t archipelago-iso-release.XXXXXX) trap 'rm -rf "$WORK"' EXIT mkdir "$WORK/web-ui" tar -xzf "$FRONTEND" -C "$WORK/web-ui" --no-same-owner touch "$WORK/build-started" env_args=(UNBUNDLED="$UNBUNDLED" BUILD_FROM_SOURCE=0 DEV_SERVER=localhost ARCHIPELAGO_BIN="$BACKEND" ARCHIPELAGO_WEB_UI="$WORK/web-ui") [ -z "$RC_OVERRIDE" ] || env_args+=(RC="$RC_OVERRIDE") sudo -E env "${env_args[@]}" nice -n 5 bash image-recipe/build-debian-iso.sh test "$SOURCE_COMMIT" = "$(python3 scripts/release_payload.py source --version "$VERSION")" ISO=$(python3 - "$VERSION" "$WORK/build-started" <<'PY' from pathlib import Path import sys start = Path(sys.argv[2]).stat().st_mtime_ns paths = [p for p in Path('image-recipe/results').glob(f'archipelago-installer-{sys.argv[1]}*-x86_64_RC*.iso') if p.stat().st_mtime_ns > start] if len(paths) != 1: raise SystemExit('expected exactly one newly built ISO; refusing stale/ambiguous result') print(paths[0].resolve()) PY ) bash scripts/iso-smoke-test.sh "$ISO" "$VERSION" # Inspect actual ISO bytes without mounting or modifying a node. mkdir "$WORK/extracted" xorriso -osirrox on -indev "$ISO" -extract /archipelago/web-ui "$WORK/extracted/web-ui" diff -qr "$WORK/web-ui" "$WORK/extracted/web-ui" mkdir "$WORK/qemu" read -r SSH_PORT HTTP_PORT < <(python3 - <<'PY' import socket with socket.socket() as a, socket.socket() as b: a.bind(('127.0.0.1', 0)); b.bind(('127.0.0.1', 0)) print(a.getsockname()[1], b.getsockname()[1]) PY ) TMPDIR="$WORK/qemu" QEMU_SSH_PORT="$SSH_PORT" QEMU_HTTP_PORT="$HTTP_PORT" \ bash image-recipe/_archived/test-iso-qemu.sh "$ISO" 180 (cd "$(dirname "$ISO")" && sha256sum "$(basename "$ISO")" > "$ISO.sha256") echo "ISO artifact gates passed: $ISO (source $SOURCE_COMMIT)" echo "Operator signing command (input now exists): bash scripts/sign-iso-checksums.sh '$ISO'" echo 'Installed/running kernel, physical install, canary and operator acceptance remain separate gates.'