OpenClaw-Setup/openclaw-setup-max/scripts/update_openclaw.sh

366 lines
10 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Usage:
bash ./scripts/update_openclaw.sh [options]
Options:
--channel <stable|beta|dev> Update channel (default: stable)
--tag <dist-tag|version> One-off npm tag/version (example: beta, 2026.2.24)
--skip-guard-stop Do not stop local model guard LaunchAgents before upgrade
--skip-guard-reinstall Do not reinstall local model guard LaunchAgents after upgrade
-h, --help Show this help
Examples:
bash ./scripts/update_openclaw.sh
bash ./scripts/update_openclaw.sh --channel beta
bash ./scripts/update_openclaw.sh --tag 2026.2.24
USAGE
}
now_ts() { date '+%Y-%m-%d %H:%M:%S%z'; }
log() { printf '[%s] [update-openclaw] %s\n' "$(now_ts)" "$*"; }
warn() { printf '[%s] [update-openclaw] WARN: %s\n' "$(now_ts)" "$*" >&2; }
err() { printf '[%s] [update-openclaw] ERROR: %s\n' "$(now_ts)" "$*" >&2; }
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
err "Required command missing: $1"
exit 1
fi
}
run_with_timeout() {
local timeout_s="$1"
shift
local start_ts elapsed pid
start_ts="$(date +%s)"
"$@" &
pid=$!
while kill -0 "$pid" 2>/dev/null; do
elapsed="$(( $(date +%s) - start_ts ))"
if (( elapsed >= timeout_s )); then
warn "Command timed out after ${timeout_s}s: $*"
kill -INT "$pid" 2>/dev/null || true
sleep 2
kill -TERM "$pid" 2>/dev/null || true
sleep 1
kill -KILL "$pid" 2>/dev/null || true
wait "$pid" 2>/dev/null || true
return 124
fi
sleep 1
done
wait "$pid"
}
extract_version() {
local raw
raw="$("$1" --version 2>&1 || true)"
printf '%s\n' "$raw" | grep -Eo '[0-9]{4}\.[0-9]+\.[0-9]+' | head -n1
}
print_guard_status() {
local label="$1"
launchctl print "gui/$(id -u)/$label" 2>/dev/null | \
grep -E 'state =|last exit code|runs =|path =|program =' || true
}
is_launchagent_loaded() {
local label="$1"
launchctl print "gui/$(id -u)/$label" >/dev/null 2>&1
}
is_gateway_launchagent_running() {
local label="$1"
launchctl print "gui/$(id -u)/$label" 2>/dev/null | grep -q 'state = running'
}
fix_gateway_launchagent_log_paths_for_external_state() {
local label="$1"
local plist="$HOME/Library/LaunchAgents/$label.plist"
local state_dir_link_target=""
if [[ ! -f "$plist" ]]; then
return 0
fi
# launchd may fail with EX_CONFIG when stdout/stderr paths point into
# ~/.openclaw on an external-volume symlink. Force local /tmp logs.
if [[ -L "$HOME/.openclaw" ]]; then
state_dir_link_target="$(readlink "$HOME/.openclaw" || true)"
fi
if [[ "$state_dir_link_target" != /Volumes/* ]]; then
return 0
fi
if ! command -v /usr/libexec/PlistBuddy >/dev/null 2>&1; then
warn "PlistBuddy not found; skipping LaunchAgent log-path patch."
return 0
fi
local desired_out="/tmp/openclaw-gateway.launchd.log"
local desired_err="/tmp/openclaw-gateway.launchd.err.log"
local current_out=""
local current_err=""
current_out="$(/usr/libexec/PlistBuddy -c 'Print :StandardOutPath' "$plist" 2>/dev/null || true)"
current_err="$(/usr/libexec/PlistBuddy -c 'Print :StandardErrorPath' "$plist" 2>/dev/null || true)"
if [[ "$current_out" == "$desired_out" && "$current_err" == "$desired_err" ]]; then
return 0
fi
log "Patching gateway LaunchAgent logs to local /tmp paths (external state dir detected)"
/usr/libexec/PlistBuddy -c "Set :StandardOutPath $desired_out" "$plist"
/usr/libexec/PlistBuddy -c "Set :StandardErrorPath $desired_err" "$plist"
launchctl bootout "gui/$(id -u)/$label" 2>/dev/null || true
launchctl bootstrap "gui/$(id -u)" "$plist"
launchctl kickstart -k "gui/$(id -u)/$label"
}
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
CHANNEL="stable"
TAG=""
STOP_GUARDS="true"
REINSTALL_GUARDS="true"
UPDATE_MAX_SECONDS="${UPDATE_MAX_SECONDS:-600}"
while (( "$#" )); do
case "$1" in
--channel)
shift
if [[ $# -eq 0 ]]; then
err "--channel requires a value"
usage
exit 1
fi
CHANNEL="$1"
;;
--tag)
shift
if [[ $# -eq 0 ]]; then
err "--tag requires a value"
usage
exit 1
fi
TAG="$1"
;;
--skip-guard-stop)
STOP_GUARDS="false"
;;
--skip-guard-reinstall)
REINSTALL_GUARDS="false"
;;
-h|--help)
usage
exit 0
;;
*)
err "Unknown argument: $1"
usage
exit 1
;;
esac
shift
done
case "$CHANNEL" in
stable|beta|dev) ;;
*)
err "Invalid --channel '$CHANNEL'. Expected stable, beta, or dev."
exit 1
;;
esac
if ! [[ "$UPDATE_MAX_SECONDS" =~ ^[0-9]+$ ]] || [[ "$UPDATE_MAX_SECONDS" -lt 1 ]]; then
err "UPDATE_MAX_SECONDS must be a positive integer."
exit 1
fi
require_cmd openclaw
require_cmd launchctl
require_cmd cp
require_cmd mkdir
require_cmd date
if ! command -v npm >/dev/null 2>&1; then
warn "npm not found; fallback npm upgrade path will be unavailable."
fi
ts="$(date +%Y%m%d-%H%M%S)"
backup_dir="$HOME/.openclaw/backups/update-$ts"
mkdir -p "$backup_dir"
config_file="$HOME/.openclaw/openclaw.json"
sessions_file="$HOME/.openclaw/agents/main/sessions/sessions.json"
log "Backing up OpenClaw files to: $backup_dir"
if [[ -f "$config_file" ]]; then
cp "$config_file" "$backup_dir/openclaw.json"
else
warn "Config not found: $config_file"
fi
if [[ -f "$sessions_file" ]]; then
cp "$sessions_file" "$backup_dir/sessions.json"
else
warn "Sessions file not found: $sessions_file"
fi
if [[ "$STOP_GUARDS" == "true" ]]; then
log "Stopping local model guard LaunchAgents to avoid config races during update"
launchctl bootout "gui/$(id -u)/ai.openclaw.local.model-budget-guard" 2>/dev/null || true
launchctl bootout "gui/$(id -u)/ai.openclaw.local.model-schedule-guard" 2>/dev/null || true
fi
log "Stopping OpenClaw gateway before update"
if ! openclaw gateway stop; then
warn "Gateway stop returned non-zero (it may already be stopped). Continuing."
fi
current_version="$(extract_version openclaw || true)"
if [[ -n "$current_version" ]]; then
log "Current OpenClaw version: $current_version"
else
warn "Could not parse current OpenClaw version from 'openclaw --version'."
fi
if command -v npm >/dev/null 2>&1; then
latest_npm_version="$(npm view openclaw version 2>/dev/null || true)"
if [[ -n "$latest_npm_version" ]]; then
log "Latest npm OpenClaw version: $latest_npm_version"
fi
fi
log "Running doctor fix for schema/config migration"
if ! openclaw doctor --fix --non-interactive; then
warn "Doctor fix returned non-zero. Continuing with upgrade."
fi
update_cmd=(openclaw update --yes --no-restart --channel "$CHANNEL")
if [[ -n "$TAG" ]]; then
update_cmd+=(--tag "$TAG")
fi
log "Running OpenClaw updater: ${update_cmd[*]}"
if ! run_with_timeout "$UPDATE_MAX_SECONDS" "${update_cmd[@]}"; then
warn "Built-in updater failed."
if ! command -v npm >/dev/null 2>&1; then
err "npm not available for fallback update."
exit 1
fi
pkg="openclaw@latest"
if [[ -n "$TAG" ]]; then
pkg="openclaw@$TAG"
fi
log "Falling back to npm global install: npm install -g $pkg"
npm install -g "$pkg"
fi
new_version="$(extract_version openclaw || true)"
if [[ -n "$new_version" ]]; then
log "Updated OpenClaw version: $new_version"
else
warn "Could not parse updated version from 'openclaw --version'."
fi
log "Starting OpenClaw gateway after update"
if ! openclaw gateway start; then
err "Gateway failed to start after update."
exit 1
fi
# Some OpenClaw versions return success from "gateway start" even when the
# LaunchAgent is not installed/loaded; enforce a service-managed gateway.
gateway_label="ai.openclaw.gateway"
if ! launchctl print "gui/$(id -u)/$gateway_label" >/dev/null 2>&1; then
warn "Gateway LaunchAgent is not loaded after start; installing and retrying."
if ! openclaw gateway install --force; then
err "Failed to install gateway service."
exit 1
fi
if ! openclaw gateway start; then
err "Gateway failed to start after installing service."
exit 1
fi
if ! launchctl print "gui/$(id -u)/$gateway_label" >/dev/null 2>&1; then
err "Gateway LaunchAgent is still not loaded after install/start."
exit 1
fi
fi
fix_gateway_launchagent_log_paths_for_external_state "$gateway_label"
if ! is_gateway_launchagent_running "$gateway_label"; then
warn "Gateway LaunchAgent is loaded but not running; attempting kickstart."
launchctl kickstart -k "gui/$(id -u)/$gateway_label" || true
sleep 2
fi
if ! is_gateway_launchagent_running "$gateway_label"; then
err "Gateway LaunchAgent is still not running."
launchctl print "gui/$(id -u)/$gateway_label" | grep -E 'state =|last exit code|runs =|path =|program =' || true
exit 1
fi
log "Running post-upgrade health checks"
openclaw doctor --non-interactive || true
openclaw gateway status || true
openclaw models status || true
if [[ "$REINSTALL_GUARDS" == "true" ]]; then
installer="$SCRIPT_DIR/install_local_model_guardrails.sh"
if [[ -x "$installer" ]]; then
log "Reinstalling local model guard LaunchAgents"
bash "$installer"
# Run both periodic guards immediately after reinstall so profile/budget
# state is refreshed in this update run (not only on the next interval).
launchctl kickstart -k "gui/$(id -u)/ai.openclaw.local.model-schedule-guard" 2>/dev/null || true
launchctl kickstart -k "gui/$(id -u)/ai.openclaw.local.model-budget-guard" 2>/dev/null || true
sleep 2
if ! is_launchagent_loaded "ai.openclaw.local.model-schedule-guard"; then
err "Schedule guard LaunchAgent is not loaded after reinstall."
exit 1
fi
if ! is_launchagent_loaded "ai.openclaw.local.model-budget-guard"; then
err "Budget guard LaunchAgent is not loaded after reinstall."
exit 1
fi
else
warn "Guard installer missing or not executable: $installer"
fi
fi
log "Local guard status summary"
print_guard_status "ai.openclaw.local.model-budget-guard"
print_guard_status "ai.openclaw.local.model-schedule-guard"
if [[ -x "$SCRIPT_DIR/model_profile_switch.sh" ]]; then
log "Profile status"
bash "$SCRIPT_DIR/model_profile_switch.sh" status || true
fi
cat <<MSG
Upgrade complete.
- Backup directory: $backup_dir
- Channel: $CHANNEL
- Tag override: ${TAG:-none}
- Current version: ${new_version:-unknown}
If needed, inspect logs:
tail -n 80 /tmp/openclaw-model-budget-guard.log /tmp/openclaw-model-budget-guard.err.log
tail -n 80 /tmp/openclaw-model-schedule-guard.log /tmp/openclaw-model-schedule-guard.err.log
MSG