Files
clawmates/scripts/rehearse-install.sh
T
Omar SobhandClaude Opus 5 875ff948f8
deploy / test (push) Successful in 4m0s
deploy / build (push) Successful in 55s
fix(rehearsal): probe the port the bundle actually publishes
The health check polled 127.0.0.1:18080, but deploy/compose/docker-compose.yml
publishes "8080:8080" and deploy/airgapped/install.sh does not rewrite ports.
Nothing was ever listening on 18080, so the rehearsal always ended in
"platform never became healthy" — regardless of whether the install worked.

Visible now only because the earlier failures (no cargo, compose v1, project
collision) all stopped the script before it got this far.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-13 20:44:13 -07:00

121 lines
5.1 KiB
Bash
Executable File

#!/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