cynarAI a0bdf6ddc8
Refresh a containerized sandbox supervisor on deploy (#215)
A supervisor that runs as a standalone container kept the image it was
started with, so broker fixes never reached it. The deploy now rebuilds
that image when its sources change and recreates the container with the
same network, restart policy, mounts and environment, restoring the old
one if the new one stays unhealthy. The broker socket moves into the data
directory and is shared from containerized supervisors too.


Claude-Session: https://claude.ai/code/session_01Tgvrj34NMnaqvtgvruYQZZ

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-18 10:25:09 +02:00

585 lines
21 KiB
Bash
Executable File

#!/usr/bin/env bash
# evOo-local — idempotent deploy helper for the Evo host checkout.
# Pull (or pin) a revision, install, build the web app, migrate, restart services, health-check,
# roll back on failure.
set -euo pipefail
# --- defaults (override via env; see ops/evo-deploy/README.md) ---
EVO_DEPLOY_DIR="${EVO_DEPLOY_DIR:-${HOME}/Projects/evOo-local}"
EVO_DEPLOY_REF="${EVO_DEPLOY_REF:-main}"
EVO_GIT_REMOTE="${EVO_GIT_REMOTE:-origin}"
EVO_RESTART_MODE="${EVO_RESTART_MODE:-auto}" # auto | systemd | tmux | compose | script | none
EVO_SYSTEMD_UNIT="${EVO_SYSTEMD_UNIT:-}"
EVO_SYSTEMD_SCOPE="${EVO_SYSTEMD_SCOPE:-user}" # user | system
EVO_SYSTEMD_AUTO_UNIT="${EVO_SYSTEMD_AUTO_UNIT:-evoo-local.service}"
EVO_TMUX_SESSION="${EVO_TMUX_SESSION:-evoo}"
EVO_TMUX_COMMAND="${EVO_TMUX_COMMAND:-pnpm dev}"
EVO_COMPOSE_FILE="${EVO_COMPOSE_FILE:-infra/compose/docker-compose.yml}"
EVO_COMPOSE_ENV_FILE="${EVO_COMPOSE_ENV_FILE:-.env}"
EVO_COMPOSE_SERVICES="${EVO_COMPOSE_SERVICES:-api web worker}"
EVO_RESTART_SCRIPT="${EVO_RESTART_SCRIPT:-}"
EVO_API_HEALTH_URL="${EVO_API_HEALTH_URL:-http://127.0.0.1:3100/health}"
EVO_WEB_HEALTH_URL="${EVO_WEB_HEALTH_URL:-http://127.0.0.1:5173/}"
EVO_HEALTH_TIMEOUT_SEC="${EVO_HEALTH_TIMEOUT_SEC:-120}"
EVO_MIGRATE="${EVO_MIGRATE:-true}"
EVO_INSTALL="${EVO_INSTALL:-true}"
EVO_BUILD="${EVO_BUILD:-true}"
EVO_DRY_RUN="${EVO_DRY_RUN:-false}"
EVO_STATE_FILE="${EVO_STATE_FILE:-${EVO_DEPLOY_DIR}/.evo-deploy/state.json}"
DRY_RUN=false
log() { printf '[evo-deploy] %s\n' "$*" >&2; }
die() {
log "ERROR: $*"
if [[ "${EVO_IN_ROLLBACK:-}" != true && "${EVO_CHECKOUT_DONE:-}" == true && -n "${EVO_PREVIOUS_SHA:-}" ]]; then
EVO_IN_ROLLBACK=true
rollback "$EVO_PREVIOUS_SHA"
fi
exit 1
}
is_valid_git_sha() {
[[ "$1" =~ ^[0-9a-fA-F]{7,40}$ ]]
}
usage() {
cat <<'EOF'
Usage: deploy.sh [--dry-run] [--ref REF] [--skip-migrate] [--skip-install] [--skip-build]
Environment: EVO_DEPLOY_DIR, EVO_DEPLOY_REF, EVO_RESTART_MODE, EVO_SYSTEMD_UNIT,
EVO_TMUX_SESSION, EVO_API_HEALTH_URL, EVO_WEB_HEALTH_URL, EVO_HEALTH_TIMEOUT_SEC, …
See ops/evo-deploy/README.md.
EOF
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run)
DRY_RUN=true
EVO_DRY_RUN=true
shift
;;
--ref)
[[ $# -ge 2 ]] || die "--ref requires a value"
EVO_DEPLOY_REF="$2"
shift 2
;;
--skip-migrate)
EVO_MIGRATE=false
shift
;;
--skip-install)
EVO_INSTALL=false
shift
;;
--skip-build)
EVO_BUILD=false
shift
;;
-h | --help)
usage
exit 0
;;
*)
die "unknown argument: $1"
;;
esac
done
if [[ "$EVO_DRY_RUN" == "true" ]]; then
DRY_RUN=true
fi
}
run() {
if [[ "$DRY_RUN" == true ]]; then
log "[dry-run] $*"
return 0
fi
"$@"
}
run_shell() {
if [[ "$DRY_RUN" == true ]]; then
log "[dry-run] $*"
return 0
fi
bash -lc "$*"
}
validate_deploy_dir() {
[[ -d "$EVO_DEPLOY_DIR" ]] || die "EVO_DEPLOY_DIR does not exist: $EVO_DEPLOY_DIR"
[[ -d "$EVO_DEPLOY_DIR/.git" ]] || die "EVO_DEPLOY_DIR is not a git checkout: $EVO_DEPLOY_DIR"
}
systemd_unit_usable() {
local unit="$1"
local scope="$2"
command -v systemctl >/dev/null 2>&1 || return 1
local -a ctl=(systemctl)
if [[ "$scope" == "user" ]]; then
ctl=(systemctl --user)
fi
if "${ctl[@]}" is-active --quiet "$unit" 2>/dev/null; then
return 0
fi
if "${ctl[@]}" is-enabled --quiet "$unit" 2>/dev/null; then
return 0
fi
local load_state
load_state="$("${ctl[@]}" show -p LoadState --value "$unit" 2>/dev/null || true)"
[[ "$load_state" == "loaded" ]]
}
# When EVO_RESTART_MODE=auto and EVO_SYSTEMD_UNIT is unset, pick a unit that is active or installed.
auto_detect_systemd() {
local unit="${EVO_SYSTEMD_UNIT:-$EVO_SYSTEMD_AUTO_UNIT}"
local scope
for scope in user system; do
if systemd_unit_usable "$unit" "$scope"; then
EVO_SYSTEMD_UNIT="$unit"
EVO_SYSTEMD_SCOPE="$scope"
return 0
fi
done
return 1
}
# Side effects must run outside $(resolve_restart_mode) — command substitution uses a subshell.
prepare_auto_restart_mode() {
[[ "$EVO_RESTART_MODE" == "auto" && -z "$EVO_SYSTEMD_UNIT" ]] || return 0
auto_detect_systemd || true
}
resolve_restart_mode() {
if [[ "$EVO_RESTART_MODE" != "auto" ]]; then
printf '%s\n' "$EVO_RESTART_MODE"
return
fi
if [[ -n "$EVO_SYSTEMD_UNIT" ]]; then
printf 'systemd\n'
return
fi
if [[ -n "$EVO_RESTART_SCRIPT" && -x "$EVO_RESTART_SCRIPT" ]]; then
printf 'script\n'
return
fi
if command -v tmux >/dev/null 2>&1 && tmux has-session -t "$EVO_TMUX_SESSION" 2>/dev/null; then
printf 'tmux\n'
return
fi
if [[ -f "$EVO_DEPLOY_DIR/$EVO_COMPOSE_FILE" ]]; then
printf 'compose\n'
return
fi
printf 'none\n'
}
systemctl_cmd() {
if [[ "$EVO_SYSTEMD_SCOPE" == "system" ]]; then
if [[ "$(id -u)" -eq 0 ]]; then
systemctl "$@"
else
sudo systemctl "$@"
fi
else
systemctl --user "$@"
fi
}
restart_systemd() {
log "restarting systemd unit: $EVO_SYSTEMD_UNIT ($EVO_SYSTEMD_SCOPE scope)"
run systemctl_cmd restart "$EVO_SYSTEMD_UNIT"
}
restart_tmux() {
log "restarting tmux session '$EVO_TMUX_SESSION' in $EVO_DEPLOY_DIR"
if [[ "$DRY_RUN" != true ]] && tmux has-session -t "$EVO_TMUX_SESSION" 2>/dev/null; then
tmux kill-session -t "$EVO_TMUX_SESSION"
fi
run_shell "cd $(printf '%q' "$EVO_DEPLOY_DIR") && tmux new-session -d -s $(printf '%q' "$EVO_TMUX_SESSION") -c $(printf '%q' "$EVO_DEPLOY_DIR") $(printf '%q' "$EVO_TMUX_COMMAND")"
}
restart_compose() {
local compose_path="$EVO_DEPLOY_DIR/$EVO_COMPOSE_FILE"
log "restarting compose services ($EVO_COMPOSE_SERVICES)"
local env_args=()
[[ -f "$EVO_DEPLOY_DIR/$EVO_COMPOSE_ENV_FILE" ]] && env_args=(--env-file "$EVO_DEPLOY_DIR/$EVO_COMPOSE_ENV_FILE")
run_shell "cd $(printf '%q' "$EVO_DEPLOY_DIR") && docker compose ${env_args[*]} -f $(printf '%q' "$EVO_COMPOSE_FILE") up -d --wait $EVO_COMPOSE_SERVICES"
}
restart_script() {
log "running restart script: $EVO_RESTART_SCRIPT"
run env EVO_DEPLOY_DIR="$EVO_DEPLOY_DIR" "$EVO_RESTART_SCRIPT"
}
restart_services() {
prepare_auto_restart_mode
local mode
mode="$(resolve_restart_mode)"
case "$mode" in
systemd) restart_systemd ;;
tmux) restart_tmux ;;
compose) restart_compose ;;
script) restart_script ;;
none)
log "EVO_RESTART_MODE=none — skipping service restart (health check only)"
;;
*)
die "unsupported restart mode: $mode"
;;
esac
}
# Fail before git fetch/checkout or install when restart settings are unusable.
# Must not run inside command substitution — it sets EVO_SYSTEMD_UNIT and related env.
validate_restart_config() {
case "$EVO_RESTART_MODE" in
auto | systemd | tmux | compose | script | none) ;;
*)
die "invalid EVO_RESTART_MODE: $EVO_RESTART_MODE (expected auto, systemd, tmux, compose, script, or none)"
;;
esac
case "$EVO_SYSTEMD_SCOPE" in
user | system) ;;
*)
die "invalid EVO_SYSTEMD_SCOPE: $EVO_SYSTEMD_SCOPE (expected user or system)"
;;
esac
prepare_auto_restart_mode
if [[ "$EVO_RESTART_MODE" == "systemd" && -z "$EVO_SYSTEMD_UNIT" ]]; then
EVO_SYSTEMD_UNIT="$EVO_SYSTEMD_AUTO_UNIT"
log "EVO_SYSTEMD_UNIT unset — using $EVO_SYSTEMD_UNIT (same default as auto mode)"
fi
local mode
mode="$(resolve_restart_mode)"
case "$mode" in
systemd)
[[ -n "$EVO_SYSTEMD_UNIT" ]] || die "EVO_RESTART_MODE=systemd requires EVO_SYSTEMD_UNIT or EVO_SYSTEMD_AUTO_UNIT"
;;
tmux)
command -v tmux >/dev/null 2>&1 || die "tmux not found; set EVO_RESTART_MODE=systemd or install tmux"
;;
compose)
[[ -f "$EVO_DEPLOY_DIR/$EVO_COMPOSE_FILE" ]] ||
die "compose file not found: $EVO_DEPLOY_DIR/$EVO_COMPOSE_FILE"
;;
script)
[[ -n "$EVO_RESTART_SCRIPT" ]] || die "EVO_RESTART_MODE=script requires EVO_RESTART_SCRIPT"
[[ -x "$EVO_RESTART_SCRIPT" ]] ||
die "EVO_RESTART_SCRIPT is not executable: $EVO_RESTART_SCRIPT"
;;
none) ;;
*)
die "unsupported restart mode: $mode"
;;
esac
EVO_RESOLVED_RESTART_MODE="$mode"
}
wait_for_url() {
local url="$1"
local label="$2"
local timeout="$EVO_HEALTH_TIMEOUT_SEC"
local start now last=""
start="$(date +%s)"
log "health-check $label: $url (timeout ${timeout}s)"
if [[ "$DRY_RUN" == true ]]; then
log "[dry-run] would wait for $label at $url"
return 0
fi
# Probe before checking the clock so a slow host still gets at least one attempt.
while true; do
if curl -fsS --max-time 5 "$url" >/dev/null 2>&1; then
log "$label healthy"
return 0
fi
last="$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 "$url" 2>/dev/null || echo unreachable)"
now="$(date +%s)"
if (( now - start >= timeout )); then
log "ERROR: $label health check timed out after ${timeout}s (last: ${last:-unknown})"
return 1
fi
sleep 2
done
}
health_check() {
wait_for_url "$EVO_API_HEALTH_URL" "API" && wait_for_url "$EVO_WEB_HEALTH_URL" "web"
}
write_state() {
local previous_sha="$1" target_sha="$2" status="$3"
is_valid_git_sha "$previous_sha" || die "refusing to write state: invalid previousSha: $previous_sha"
is_valid_git_sha "$target_sha" || die "refusing to write state: invalid deployedSha: $target_sha"
if [[ "$DRY_RUN" == true ]]; then
log "[dry-run] would write state to $EVO_STATE_FILE"
return 0
fi
mkdir -p "$(dirname "$EVO_STATE_FILE")"
cat >"$EVO_STATE_FILE" <<EOF
{
"previousSha": "$previous_sha",
"deployedSha": "$target_sha",
"ref": "$EVO_DEPLOY_REF",
"status": "$status",
"at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
EOF
}
git_fetch_and_checkout() {
local previous_sha target_sha
previous_sha="$(git -C "$EVO_DEPLOY_DIR" rev-parse HEAD)"
log "previous HEAD: $previous_sha"
log "fetching $EVO_GIT_REMOTE…"
run git -C "$EVO_DEPLOY_DIR" fetch "$EVO_GIT_REMOTE" --prune >&2
# Prefer the freshly fetched remote branch: the local `main` in the deploy
# checkout never moves (we reset HEAD, not the branch), so resolving it first
# would redeploy a stale commit.
if git -C "$EVO_DEPLOY_DIR" rev-parse --verify "${EVO_GIT_REMOTE}/${EVO_DEPLOY_REF}^{commit}" >/dev/null 2>&1; then
target_sha="$(git -C "$EVO_DEPLOY_DIR" rev-parse "${EVO_GIT_REMOTE}/${EVO_DEPLOY_REF}^{commit}")"
elif git -C "$EVO_DEPLOY_DIR" rev-parse --verify "${EVO_DEPLOY_REF}^{commit}" >/dev/null 2>&1; then
target_sha="$(git -C "$EVO_DEPLOY_DIR" rev-parse "${EVO_DEPLOY_REF}^{commit}")"
else
die "cannot resolve ref: $EVO_DEPLOY_REF"
fi
log "checking out $target_sha ($EVO_DEPLOY_REF)"
run git -C "$EVO_DEPLOY_DIR" reset --hard "$target_sha" >&2
printf '%s %s\n' "$previous_sha" "$target_sha"
}
run_migrate() {
[[ "$EVO_MIGRATE" == "true" ]] || { log "skipping migrate (EVO_MIGRATE=false)"; return 0; }
if [[ ! -f "$EVO_DEPLOY_DIR/package.json" ]]; then
die "package.json missing in $EVO_DEPLOY_DIR"
fi
if ! grep -q '"db:migrate"' "$EVO_DEPLOY_DIR/package.json"; then
log "no db:migrate script in package.json — skipping migrations"
return 0
fi
log "running pnpm db:generate && pnpm db:migrate"
run_shell "cd $(printf '%q' "$EVO_DEPLOY_DIR") && pnpm db:generate && pnpm db:migrate"
}
# Rollback regenerates the client for the previous schema; applied migrations stay applied.
run_generate() {
[[ "$EVO_MIGRATE" == "true" ]] || return 0
log "running pnpm db:generate"
run_shell "cd $(printf '%q' "$EVO_DEPLOY_DIR") && pnpm db:generate"
}
run_install() {
[[ "$EVO_INSTALL" == "true" ]] || { log "skipping install (EVO_INSTALL=false)"; return 0; }
log "running pnpm install --frozen-lockfile"
run_shell "cd $(printf '%q' "$EVO_DEPLOY_DIR") && pnpm install --frozen-lockfile"
}
# The build goes to a staging directory, so a failing build never touches the one a running
# `vite preview` serves; activate_web_build swaps it in right before the restart.
WEB_STAGING_DIR="$EVO_DEPLOY_DIR/.evo-deploy/web-dist"
WEB_BUILD_LOG="$EVO_DEPLOY_DIR/.evo-deploy/web-build.log"
WEB_DIST_DIR="$EVO_DEPLOY_DIR/apps/web/dist"
WEB_BUILD_REPLACED=false
build_web() {
[[ "$EVO_BUILD" == "true" ]] || { log "skipping web build (EVO_BUILD=false)"; return 0; }
log "building the web app"
if [[ "$DRY_RUN" == true ]]; then
log "[dry-run] pnpm --filter @rakazo/web build --outDir $WEB_STAGING_DIR"
return 0
fi
rm -rf "$WEB_STAGING_DIR" && mkdir -p "$(dirname "$WEB_BUILD_LOG")" || return 1
if run_shell "cd $(printf '%q' "$EVO_DEPLOY_DIR") && pnpm --filter @rakazo/web build --outDir $(printf '%q' "$WEB_STAGING_DIR") --emptyOutDir" >"$WEB_BUILD_LOG" 2>&1 &&
[[ -f "$WEB_STAGING_DIR/index.html" ]]; then
return 0
fi
# Stack frames bury the message that names the file and line.
grep -v -E '^[[:space:]]+at ' "$WEB_BUILD_LOG" | tail -n 60 >&2 || true
log "ERROR: web build failed (full log: $WEB_BUILD_LOG)"
return 1
}
activate_web_build() {
[[ "$EVO_BUILD" == "true" ]] || return 0
local outgoing="$WEB_STAGING_DIR.outgoing"
log "activating the web build"
WEB_BUILD_REPLACED=true
run rm -rf "$outgoing" || return 1
if [[ -d "$WEB_DIST_DIR" ]]; then
run mv "$WEB_DIST_DIR" "$outgoing" || return 1
fi
run mv "$WEB_STAGING_DIR" "$WEB_DIST_DIR" || return 1
run rm -rf "$outgoing"
}
# Restores the previous commit with its dependencies, client and web build, restarts and
# health-checks it. Every step is attempted so the service comes back even if one of them fails.
rollback() {
EVO_IN_ROLLBACK=true
local previous_sha="$1" status="rolled-back"
is_valid_git_sha "$previous_sha" || die "rollback aborted: invalid previous_sha: $previous_sha"
log "ROLLBACK: resetting to $previous_sha"
if ! run git -C "$EVO_DEPLOY_DIR" reset --hard "$previous_sha" >&2; then
write_state "$previous_sha" "$previous_sha" "rollback-failed"
die "deploy failed and the checkout could not be reset to $previous_sha"
fi
run_install || status="rollback-failed"
run_generate || status="rollback-failed"
# Until activation the served build is still the previous one; rebuild only once it was replaced.
if [[ "$WEB_BUILD_REPLACED" == true ]] && ! { build_web && activate_web_build; }; then
status="rollback-failed"
fi
if ! { restart_services && health_check; }; then
status="rollback-failed"
fi
write_state "$previous_sha" "$previous_sha" "$status"
[[ "$status" == "rolled-back" ]] || die "deploy failed; rollback to $previous_sha did not complete"
die "deploy failed; rolled back to $previous_sha"
}
# New computers start from rakazo/computer:local, which only `pnpm sandbox:build` refreshes.
# Rebuild it when the revision changed its sources; a failure leaves the old image and warns.
rebuild_computer_image() {
local previous_sha="$1" target_sha="$2"
git -C "$EVO_DEPLOY_DIR" diff --quiet "$previous_sha" "$target_sha" -- infra/sandboxes/computer && return 0
if ! command -v docker >/dev/null 2>&1; then
log "WARNING: the computer image changed but docker is unavailable; run pnpm sandbox:build"
return 0
fi
log "rebuilding the computer image"
run_shell "cd $(printf '%q' "$EVO_DEPLOY_DIR") && pnpm sandbox:build" >&2 ||
log "WARNING: the computer image rebuild failed; run pnpm sandbox:build"
}
# Idle computers keep their old container until reprovisioned; drop providerRef so the next
# boot picks up the refreshed rakazo/computer image and broker shims without manual steps.
recreate_idle_computers() {
local previous_sha="$1" target_sha="$2"
git -C "$EVO_DEPLOY_DIR" diff --quiet "$previous_sha" "$target_sha" -- \
infra/sandboxes/computer \
infra/sandboxes/supervisor \
packages/adapters/src/github-broker-client.ts \
packages/adapters/src/deployment-github-token.ts && return 0
if [[ "$DRY_RUN" == true ]]; then
log "[dry-run] would clear providerRef on stopped computers"
return 0
fi
log "recreating idle computers on next boot"
run_shell "cd $(printf '%q' "$EVO_DEPLOY_DIR") && pnpm --filter @rakazo/db exec prisma db execute --stdin" <<'SQL' >&2 || \
log "WARNING: could not mark idle computers for recreation; restart stopped computers manually"
UPDATE "computers"
SET "providerRef" = NULL, "updatedAt" = NOW()
WHERE "state" = 'stopped'
AND "providerRef" IS NOT NULL
AND "maintenanceId" IS NULL
AND "executionRunId" IS NULL;
SQL
}
# A host that runs the sandbox supervisor as a standalone container keeps the image it was started
# with. Rebuild it when its sources changed and start a fresh container with the same name, image,
# network, restart policy, mounts and environment; the old one comes back if the new one is unhealthy.
refresh_supervisor_container() {
local previous_sha="$1" target_sha="$2"
local name="${EVO_SUPERVISOR_CONTAINER:-evoo-sandbox-supervisor}"
local health_url="${EVO_SUPERVISOR_HEALTH_URL:-http://127.0.0.1:7091/health}"
git -C "$EVO_DEPLOY_DIR" diff --quiet "$previous_sha" "$target_sha" -- \
infra/sandboxes/supervisor packages/contracts packages/core packages/logging pnpm-lock.yaml && return 0
command -v docker >/dev/null 2>&1 || return 0
docker container inspect "$name" >/dev/null 2>&1 || return 0
if [[ "$DRY_RUN" == true ]]; then
log "[dry-run] would rebuild and recreate the $name container"
return 0
fi
local work image network restart retries line
work="$(mktemp -d)"
image="$(docker container inspect -f '{{.Config.Image}}' "$name")"
network="$(docker container inspect -f '{{.HostConfig.NetworkMode}}' "$name")"
restart="$(docker container inspect -f '{{.HostConfig.RestartPolicy.Name}}' "$name")"
retries="$(docker container inspect -f '{{.HostConfig.RestartPolicy.MaximumRetryCount}}' "$name")"
local -a args=()
[[ -n "$network" && "$network" != default && "$network" != bridge ]] && args+=(--network "$network")
if [[ "$restart" == on-failure && "$retries" != 0 ]]; then
args+=(--restart "on-failure:$retries")
elif [[ -n "$restart" && "$restart" != no ]]; then
args+=(--restart "$restart")
fi
while IFS= read -r line; do
[[ -n "$line" ]] && args+=(-v "$line")
done < <(docker container inspect -f \
'{{range .Mounts}}{{if eq .Type "volume"}}{{.Name}}{{else}}{{.Source}}{{end}}:{{.Destination}}{{if not .RW}}:ro{{end}}{{"\n"}}{{end}}' "$name")
while IFS= read -r line; do
[[ -n "$line" ]] && args+=(-p "${line#:}")
done < <(docker container inspect -f \
'{{range $port, $bindings := .HostConfig.PortBindings}}{{range $bindings}}{{.HostIp}}:{{.HostPort}}:{{$port}}{{"\n"}}{{end}}{{end}}' "$name")
# The image sets PATH and its Node versions itself.
(umask 077 && docker container inspect -f '{{range .Config.Env}}{{println .}}{{end}}' "$name" |
grep -Ev '^(PATH|NODE_VERSION|YARN_VERSION)=' >"$work/env") || true
args+=(--env-file "$work/env")
log "rebuilding the $name container"
if ! docker build -q -t "$image" -f "$EVO_DEPLOY_DIR/infra/sandboxes/supervisor/Dockerfile" "$EVO_DEPLOY_DIR" >&2; then
log "WARNING: the supervisor image rebuild failed; $name keeps running the old image"
rm -rf "$work"
return 0
fi
docker rm -f "$name-previous" >/dev/null 2>&1 || true
if ! { docker stop "$name" >/dev/null && docker rename "$name" "$name-previous"; }; then
log "WARNING: could not stop $name; it keeps running the old image"
docker start "$name" >/dev/null 2>&1 || true
rm -rf "$work"
return 0
fi
if docker run -d --name "$name" "${args[@]}" "$image" >/dev/null && wait_for_url "$health_url" "supervisor"; then
docker rm "$name-previous" >/dev/null
log "$name now runs the new image"
else
log "WARNING: the new $name container did not come up; restoring the previous one"
docker rm -f "$name" >/dev/null 2>&1 || true
{ docker rename "$name-previous" "$name" && docker start "$name" >/dev/null; } ||
log "WARNING: could not restore $name; start it with docker start $name-previous"
fi
rm -rf "$work"
}
main() {
parse_args "$@"
validate_deploy_dir
validate_restart_config
local resolved_mode="$EVO_RESOLVED_RESTART_MODE"
log "deploy dir: $EVO_DEPLOY_DIR"
log "target ref: $EVO_DEPLOY_REF"
log "restart mode: $resolved_mode"
[[ "$DRY_RUN" == true ]] && log "dry-run enabled — no mutating commands"
local previous_sha target_sha
read -r previous_sha target_sha < <(git_fetch_and_checkout)
EVO_CHECKOUT_DONE=true
EVO_PREVIOUS_SHA="$previous_sha"
is_valid_git_sha "$previous_sha" || die "invalid previous_sha captured from checkout: $previous_sha"
is_valid_git_sha "$target_sha" || die "invalid target_sha captured from checkout: $target_sha"
if [[ "$previous_sha" == "$target_sha" && "$DRY_RUN" != true ]]; then
log "already at $target_sha — continuing with install/build/migrate/restart/health"
fi
# Any failure rolls back; a failing install, build or migration stops before the restart.
if ! { run_install && build_web && run_migrate && activate_web_build && restart_services && health_check; }; then
rollback "$previous_sha"
fi
write_state "$previous_sha" "$target_sha" "success"
rebuild_computer_image "$previous_sha" "$target_sha"
refresh_supervisor_container "$previous_sha" "$target_sha"
recreate_idle_computers "$previous_sha" "$target_sha"
log "deploy succeeded: $previous_sha -> $target_sha"
}
main "$@"