feat: add deploy/rollback supervisor script
Test Suite / Detect changes (push) Has been cancelled
Test Suite / Python (Windows, 3.14) (push) Has been cancelled
Test Suite / Python (minimum, 3.11) (push) Has been cancelled
Test Suite / Python (latest, 3.14 + coverage) (push) Has been cancelled
Test Suite / webui (push) Has been cancelled
Test Suite / docker (push) Has been cancelled
Test Suite / Detect changes (push) Has been cancelled
Test Suite / Python (Windows, 3.14) (push) Has been cancelled
Test Suite / Python (minimum, 3.11) (push) Has been cancelled
Test Suite / Python (latest, 3.14 + coverage) (push) Has been cancelled
Test Suite / webui (push) Has been cancelled
Test Suite / docker (push) Has been cancelled
Git-checkout-based deployment with health checks and automatic rollback. Usage: deploy.sh <commit> | deploy.sh rollback | deploy.sh status
This commit is contained in:
Executable
+187
@@ -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 <commit-ish> 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" <<EOF
|
||||
{
|
||||
"failed_candidate": "$failed",
|
||||
"known_good": "$target",
|
||||
"timestamp": "$(date -Iseconds)",
|
||||
"rollback_successful": true
|
||||
}
|
||||
EOF
|
||||
else
|
||||
log "CRITICAL: rollback also failed! Manual intervention required."
|
||||
cat > "/home/claw/.nanobot/deploy-failure.json" <<EOF
|
||||
{
|
||||
"failed_candidate": "$failed",
|
||||
"known_good": "$target",
|
||||
"timestamp": "$(date -Iseconds)",
|
||||
"rollback_successful": false
|
||||
}
|
||||
EOF
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_rollback() {
|
||||
local known_good
|
||||
known_good=$(read_state)
|
||||
if [[ -z "$known_good" ]]; then
|
||||
log "No known-good recorded, cannot rollback"
|
||||
return 1
|
||||
fi
|
||||
cmd_rollback_internal "$known_good" "$(current_commit)"
|
||||
}
|
||||
|
||||
# ---- main ----
|
||||
|
||||
case "${1:-status}" in
|
||||
status) cmd_status ;;
|
||||
rollback) cmd_rollback ;;
|
||||
*) cmd_deploy "$1" ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user