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]>
121 lines
5.1 KiB
Bash
Executable File
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
|