diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 00000000..bffe3fdd --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,187 @@ +#!/usr/bin/env bash +# nanobot deploy/rollback supervisor +# Manages git-checkout-based deployments with health checks and automatic rollback. +# +# Usage: +# deploy.sh Deploy a candidate commit +# deploy.sh rollback Rollback to last known-good +# deploy.sh status Show current deployment state + +set -euo pipefail + +CHECKOUT="/home/claw/nanobot-src" +STATE_FILE="/home/claw/.nanobot/deploy-state.json" +HEALTH_URL="http://127.0.0.1:18790/health" +STABILIZE_SEC=8 +HEALTH_TIMEOUT_SEC=30 +SERVICE="nanobot" + +log() { echo "[deploy] $(date '+%H:%M:%S') $*"; } + +# ---- helpers ---- + +current_commit() { git -C "$CHECKOUT" rev-parse HEAD; } +short_hash() { echo "${1:0:8}"; } + +read_state() { + if [[ -f "$STATE_FILE" ]]; then + python3 -c "import json; d=json.load(open('$STATE_FILE')); print(d.get('known_good',''))" + else + echo "" + fi +} + +write_state() { + local known_good="$1" + python3 -c " +import json, sys +d = {} +try: d = json.load(open('$STATE_FILE')) +except: pass +d['known_good'] = '$known_good' +d['last_updated'] = '$(date -Iseconds)' +json.dump(d, open('$STATE_FILE', 'w'), indent=2) +" +} + +restart_service() { + export XDG_RUNTIME_DIR=/run/user/1000 + systemctl --user restart "$SERVICE" +} + +service_active() { + export XDG_RUNTIME_DIR=/run/user/1000 + systemctl --user is-active --quiet "$SERVICE" +} + +health_check() { + # Returns 0 if healthy, 1 if not + local deadline=$((SECONDS + HEALTH_TIMEOUT_SEC)) + while (( SECONDS < deadline )); do + if curl -sf --max-time 3 "$HEALTH_URL" >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + return 1 +} + +# ---- commands ---- + +cmd_status() { + echo "Checkout: $CHECKOUT" + echo "Current commit: $(current_commit) ($(short_hash "$(current_commit)"))" + echo "Known-good: $(read_state)" + echo "Service: $(service_active && echo 'active' || echo 'inactive')" +} + +cmd_deploy() { + local candidate="$1" + local known_good + known_good=$(read_state) + + cd "$CHECKOUT" + + # Record current as known-good if not set + if [[ -z "$known_good" ]]; then + known_good=$(current_commit) + write_state "$known_good" + log "Initial known-good: $(short_hash "$known_good")" + fi + + # Check candidate exists + if ! git cat-file -e "$candidate^{commit}" 2>/dev/null; then + log "ERROR: candidate '$candidate' is not a valid commit" + return 1 + fi + + local candidate_hash + candidate_hash=$(git rev-parse "$candidate") + + if [[ "$candidate_hash" == "$(current_commit)" ]]; then + log "Already at candidate $(short_hash "$candidate_hash"), just restarting" + else + log "Deploying candidate: $(short_hash "$candidate_hash")" + log "Known-good: $(short_hash "$known_good")" + + git checkout "$candidate_hash" + fi + + log "Restarting service..." + restart_service + + log "Waiting ${STABILIZE_SEC}s for stabilization..." + sleep "$STABILIZE_SEC" + + if ! service_active; then + log "ERROR: service not active after restart" + cmd_rollback_internal "$known_good" "$candidate_hash" + return 1 + fi + + log "Running health check..." + if health_check; then + log "Health check PASSED" + write_state "$candidate_hash" + log "Promoted $(short_hash "$candidate_hash") to known-good" + else + log "ERROR: health check FAILED" + cmd_rollback_internal "$known_good" "$candidate_hash" + return 1 + fi +} + +cmd_rollback_internal() { + local target="$1" + local failed="${2:-unknown}" + + log "ROLLING BACK to $(short_hash "$target")" + + cd "$CHECKOUT" + git checkout "$target" + restart_service + + sleep "$STABILIZE_SEC" + + if service_active && health_check; then + log "Rollback successful, service healthy" + # Write failure report + cat > "/home/claw/.nanobot/deploy-failure.json" < "/home/claw/.nanobot/deploy-failure.json" <