#!/usr/bin/env bash # Clean-room install rehearsal (plan: per-release clean-VM install.sh # rehearsal): assemble a REAL signed bundle from the built images, run # the customer's install path end to end — offline verify, docker load, # compose up — and assert the platform answers before tearing down. # # Requires: clawmates/server:latest + clawmates/frontend:latest built # (POSTGRES_PASSWORD=x docker compose -f deploy/compose/docker-compose.yml build). set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" # Compose v2 is REQUIRED here, and an explicit $COMPOSE wins. # # deploy/compose/docker-compose.yml uses v2-only syntax — a top-level `name:` # and the long-form `env_file: {path, required}` — so legacy docker-compose # 1.29 cannot parse it at all ("'name' does not match any of the regexes"). A # v1 fallback would not degrade, it would fail, so there is no point pretending # otherwise: say so up front instead of dying twenty lines later. # # The override exists because the release runner (gw-04) deliberately has NO # `docker compose` plugin: installing one system-wide would silently switch the # PRODUCTION rolling deploy off docker-compose v1, which is a change nobody # asked for. It gets a standalone v2 binary and passes it in instead. if [ -z "${COMPOSE:-}" ]; then if docker compose version >/dev/null 2>&1; then COMPOSE="docker compose" else echo "compose v2 not found. Install it, or set COMPOSE=/path/to/docker-compose (v2)." >&2 echo "docker-compose v1 cannot read this compose file — it is not a supported fallback." >&2 exit 1 fi fi WORK="$(mktemp -d)" # A UNIQUE compose project, and never the production one. # # deploy/compose/docker-compose.yml declares `name: clawmates` at the top level, # and that wins over --project-directory. On a shared host this rehearsal # therefore ADOPTS any running stack called `clawmates` — and its cleanup trap # runs `down -v`, which deletes that stack's volumes. That is exactly what # happened on gw-04: the release rehearsal recreated the live containers and # destroyed the production pgdata volume. -p is the fix; the assertion below is # the seatbelt. PROJECT="rehearse-$$" case "$PROJECT" in clawmates|clawmates-*) echo "refusing to use production project name" >&2; exit 1 ;; esac export CLAWMATES_HOME="$WORK/opt" cleanup() { $COMPOSE -p "$PROJECT" --project-directory "$CLAWMATES_HOME" down -v >/dev/null 2>&1 || true rm -rf "$WORK" } trap cleanup EXIT echo "==> Building bundler + signing key" # Honour a pre-built binary via CLAWMATES_BUNDLER. The release runner has no # cargo — it builds Rust inside a container — and this script's job is to # rehearse an INSTALL, not to be a second place that needs a toolchain. if [ -n "${CLAWMATES_BUNDLER:-}" ] && [ -x "${CLAWMATES_BUNDLER}" ]; then BUNDLER="$CLAWMATES_BUNDLER" echo " using pre-built $BUNDLER" else cargo build -q -p clawmates-bundler BUNDLER="$ROOT/target/debug/clawmates-bundler" fi "$BUNDLER" keygen "$WORK/release.key" "$WORK/release.pub" echo "==> Saving runtime images" mkdir -p "$WORK/images" docker pull -q postgres:16-alpine >/dev/null docker pull -q tecnativa/docker-socket-proxy:0.3 >/dev/null docker save clawmates/server:latest -o "$WORK/images/server.tar" docker save clawmates/frontend:latest -o "$WORK/images/frontend.tar" docker save clawmates/broker:latest -o "$WORK/images/broker.tar" docker save postgres:16-alpine -o "$WORK/images/postgres.tar" docker save tecnativa/docker-socket-proxy:0.3 -o "$WORK/images/socket-proxy.tar" echo "==> Assembling the signed bundle" ARTIFACTS="" for tar in "$WORK"/images/*.tar; do ARTIFACTS="$ARTIFACTS $tar=images/$(basename "$tar")" done # shellcheck disable=SC2086 "$BUNDLER" assemble "$WORK/bundle" "rehearsal" "$WORK/release.key" \ "$ROOT/deploy/compose/docker-compose.yml=compose/docker-compose.yml" \ "$ROOT/deploy/compose/clawmates.toml=compose/clawmates.toml" \ "$ROOT/deploy/compose/.env.example=compose/.env.example" \ "$ROOT/deploy/airgapped/install.sh=install.sh" \ "$BUNDLER=bin/clawmates-bundler" \ $ARTIFACTS chmod +x "$WORK/bundle/bin/clawmates-bundler" "$WORK/bundle/install.sh" echo "==> Customer install: verify -> load -> stage" "$WORK/bundle/install.sh" "$WORK/bundle" "$WORK/release.pub" echo "==> First boot" echo "POSTGRES_PASSWORD=rehearse-$$" > "$CLAWMATES_HOME/.env" $COMPOSE -p "$PROJECT" --project-directory "$CLAWMATES_HOME" up -d --no-build echo "==> Waiting for the platform" for _ in $(seq 1 60); do # 8080, not 18080: deploy/compose/docker-compose.yml publishes "8080:8080" # and deploy/airgapped/install.sh does not rewrite ports, so the old probe # could never answer — the rehearsal always timed out at "platform never # became healthy" no matter how well the install went. if curl -fsS http://127.0.0.1:8080/healthz >/dev/null 2>&1; then echo "==> Server is healthy" if curl -fsS http://127.0.0.1:3000/login | grep -q clawmates; then echo "==> Frontend serves the login page" echo "REHEARSAL OK" exit 0 fi fi sleep 2 done echo "REHEARSAL FAILED: platform never became healthy" $COMPOSE -p "$PROJECT" --project-directory "$CLAWMATES_HOME" logs --tail 30 exit 1