From ea0b4f0d83116ee37cb89eac7133fb20adb3a6ed Mon Sep 17 00:00:00 2001 From: darbsllim <677508+darbsllim@users.noreply.github.com> Date: Fri, 15 May 2026 10:09:29 -0400 Subject: [PATCH 1/2] Add controlled update cutover guardrail --- README.md | 1 + SKILL.md | 20 +++ scripts/update-cutover.sh | 343 ++++++++++++++++++++++++++++++++++++++ tests/run.sh | 109 +++++++++++- 4 files changed, 472 insertions(+), 1 deletion(-) create mode 100755 scripts/update-cutover.sh diff --git a/README.md b/README.md index bc29a8d..7e2ac68 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,7 @@ journalctl --user -u openclaw-gateway -f - `health-check.sh` can report a process uptime failure immediately after `openclaw gateway restart` if the target has a minimum uptime threshold (e.g. 300s). That is expected — lower the threshold during smoke tests, then restore it. - `security-scan.sh` reports file paths and line numbers for suspected secrets, but redacts the secret values themselves. - `check-update.sh` is intended for real post-upgrade triage. It is normal to report a version change the first time it runs after an upgrade. +- `update-cutover.sh` is the controlled update guardrail. Run it before and after production/macOS/custom-runtime upgrades to capture baseline evidence, force an official-vs-custom lane decision, record app/CLI scope, prompt hack/workaround review, and verify the post-update state. It does not run `openclaw update` itself. - `post-update.sh` is the explicit post-update orchestrator. It skips the heavy sequence when the current version matches the stored state and otherwise runs `check-update.sh --fix`, `heal.sh`, the workspace reconcile script if present, `security-scan.sh`, and a final `openclaw health --json`. - On the VPS, the workspace reconcile stage refreshes model policy, auth/profile state, voice defaults, and the gateway service through `openclaw_post_update_reconcile.py` (or the equivalent systemd oneshot wrapper). - After the health check it best-effort touches `~/.openclaw/state/policy-guard.trigger` (creating parent dirs if needed). The VPS can wire `openclaw-policy-guard.path` to that sentinel after updates. diff --git a/SKILL.md b/SKILL.md index e68c1ff..ff75514 100644 --- a/SKILL.md +++ b/SKILL.md @@ -26,6 +26,7 @@ On Knox's machine, the canonical ops checkout is `/Users/knox/Developer/openclaw | Script | When to use | |--------|-------------| | `heal.sh` | First thing on any health check — fixes gateway, auth mode, exec approvals, crons, and stuck sessions in one pass | +| `update-cutover.sh` | Controlled upgrade guardrail — preflight baseline, official/custom lane gate, macOS app-scope check, hack-audit prompt, and post-cutover verification | | `post-update.sh` | Run after `openclaw update` — orchestrates check-update, heal, workspace reconcile, security scan, and final health check in sequence | | `watchdog.sh` | Continuous monitoring; run every 5 min via LaunchAgent. HTTP health check → auto-restart → escalation after 3 failures | | `watchdog-install.sh` | Set up the watchdog as a macOS LaunchAgent (survives reboots) | @@ -57,6 +58,12 @@ On Knox's machine, the canonical ops checkout is `/Users/knox/Developer/openclaw # One-pass heal: bash scripts/heal.sh +# Controlled OpenClaw upgrade cutover: +bash scripts/update-cutover.sh --preflight --target-version v2026.X.Y --lane official --app-scope cli +# ...run the approved OpenClaw update... +bash scripts/post-update.sh +bash scripts/update-cutover.sh --post --target-version v2026.X.Y --lane official --app-scope cli --cutover-dir ~/.openclaw/update-cutovers/-v2026.X.Y + # Install always-on watchdog (macOS): bash scripts/watchdog-install.sh @@ -134,6 +141,19 @@ If outdated: `curl -fsSL https://openclaw.ai/install.sh | bash && openclaw gatew After any version upgrade, run `check-update.sh` to catch breaking config changes. +## Controlled Update Cutover + +For routine patch updates on simple installs, `post-update.sh` is enough after the update. For production gateways, macOS installs, custom/local runtimes, or any update meant to fix a live incident, treat the update as a controlled cutover: + +1. Run `update-cutover.sh --preflight --target-version --lane official|custom --app-scope cli|app|both|none`. +2. Review the generated `CUTOVER.md`: release risks, official/custom lane, macOS app scope, current config/cron/channel baseline, hack/workaround audit, rollback target, and single-restart plan. +3. Run the approved OpenClaw update only after the cutover gate is satisfied. +4. Run `post-update.sh`. +5. Run `update-cutover.sh --post ... --cutover-dir ` and any host-specific channel smoke tests. +6. If verification fails, stop layering fixes; roll back to the prior known-good runtime target and verify restored health. + +`update-cutover.sh` intentionally does **not** execute `openclaw update`; it records evidence and enforces the decision gates around the update. + --- ## Fix Priority (Health Check Order) diff --git a/scripts/update-cutover.sh b/scripts/update-cutover.sh new file mode 100755 index 0000000..38fdcf3 --- /dev/null +++ b/scripts/update-cutover.sh @@ -0,0 +1,343 @@ +#!/usr/bin/env bash +# update-cutover.sh — controlled OpenClaw upgrade cutover guardrail +# +# This script does not run `openclaw update`. It captures the before/after +# evidence and prints the human decision gates that must be satisfied around an +# update. Use it before updating and again after the update. +# +# Usage: +# bash scripts/update-cutover.sh --preflight --target-version v2026.5.12 --lane official --app-scope cli +# openclaw update +# bash scripts/post-update.sh +# bash scripts/update-cutover.sh --post --target-version v2026.5.12 --lane official --app-scope cli --cutover-dir + +set -euo pipefail + +LIB_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$LIB_DIR/lib.sh" + +require_tools openclaw python3 || exit 1 + +MODE="" +TARGET_VERSION="" +LANE="" +APP_SCOPE="" +CUTOVER_DIR="" +RELEASE_NOTES="" +HACK_AUDIT_LOG="${OPENCLAW_HACK_AUDIT_LOG:-$HOME/.openclaw/hack-audit-log.md}" +RUN_SMOKE=false + +usage() { + cat <<'EOF' +Usage: + update-cutover.sh --preflight --target-version VERSION --lane official|custom --app-scope cli|app|both|none [--release-notes PATH_OR_URL] + update-cutover.sh --post --target-version VERSION --lane official|custom --app-scope cli|app|both|none [--cutover-dir DIR] [--smoke] + +Modes: + --preflight Capture read-only baseline and generate the cutover gate report. + --post Capture after-state and run post-cutover verification checks. + +Required gates: + --lane official = packaged release is source of truth; custom = local/runtime checkout remains source of truth. + --app-scope cli, app, both, or none. On macOS, the app and CLI/gateway are separate artifacts. + +Environment: + OPENCLAW_CUTOVER_ROOT Root for reports (default: ~/.openclaw/update-cutovers) + OPENCLAW_HACK_AUDIT_LOG Optional hack/workaround audit file to include in the report +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --preflight) MODE="preflight"; shift ;; + --post) MODE="post"; shift ;; + --target-version|--lane|--app-scope|--cutover-dir|--release-notes) + [[ $# -ge 2 && -n "${2:-}" && "${2:-}" != --* ]] || { echo "Missing value for $1" >&2; usage; exit 1; } + case "$1" in + --target-version) TARGET_VERSION="$2" ;; + --lane) LANE="$2" ;; + --app-scope) APP_SCOPE="$2" ;; + --cutover-dir) CUTOVER_DIR="$2" ;; + --release-notes) RELEASE_NOTES="$2" ;; + esac + shift 2 ;; + --smoke) RUN_SMOKE=true; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown argument: $1" >&2; usage; exit 1 ;; + esac +done + +[[ -n "$MODE" ]] || { echo "Missing --preflight or --post" >&2; usage; exit 1; } +[[ "$LANE" =~ ^(official|custom)$ ]] || { echo "--lane must be official or custom" >&2; usage; exit 1; } +[[ "$APP_SCOPE" =~ ^(cli|app|both|none)$ ]] || { echo "--app-scope must be cli, app, both, or none" >&2; usage; exit 1; } +[[ -n "$TARGET_VERSION" ]] || { echo "Missing --target-version" >&2; usage; exit 1; } + +CUTOVER_ROOT="${OPENCLAW_CUTOVER_ROOT:-$HOME/.openclaw/update-cutovers}" +if [[ -z "$CUTOVER_DIR" ]]; then + stamp="$(date -u +%Y%m%dT%H%M%SZ)" + safe_target="$(printf '%s' "$TARGET_VERSION" | tr -c 'A-Za-z0-9._-' '_')" + CUTOVER_DIR="$CUTOVER_ROOT/${stamp}-${safe_target}" +fi +mkdir -p "$CUTOVER_DIR" + +log() { echo -e "${CYN}[~]${RST} $1"; } +good() { echo -e "${GRN}[✓]${RST} $1"; } +warn() { echo -e "${YLW}[!]${RST} $1"; } +bad() { echo -e "${RED}[✗]${RST} $1"; } + +run_capture() { + local label="$1" + local outfile="$2" + shift 2 + local rc=0 + set +e + { + printf '# %s\n' "$label" + printf '# captured_at=%s\n\n' "$(iso_now)" + "$@" + } 2>&1 | sanitize_sensitive >"$outfile" + rc=${PIPESTATUS[0]} + set -e + printf '%s\n' "$rc" >"$outfile.exit" + return 0 +} + +config_file_path() { + openclaw config file 2>/dev/null || printf '%s\n' "$HOME/.openclaw/openclaw.json" +} + +capture_state() { + local prefix="$1" + local state_dir="$CUTOVER_DIR/$prefix" + mkdir -p "$state_dir" + + run_capture "openclaw version" "$state_dir/openclaw-version.txt" openclaw --version + run_capture "openclaw gateway status" "$state_dir/gateway-status.txt" openclaw gateway status + run_capture "openclaw status" "$state_dir/status.txt" openclaw status + run_capture "openclaw doctor" "$state_dir/doctor.txt" openclaw doctor + run_capture "openclaw channels status --probe" "$state_dir/channels-status-probe.txt" openclaw channels status --probe + run_capture "openclaw cron list --json" "$state_dir/cron-list.json" openclaw cron list --json + + local cfg + cfg="$(config_file_path)" + if [[ -f "$cfg" ]]; then + sanitize_sensitive <"$cfg" >"$state_dir/openclaw-config.redacted.json" || true + else + printf 'config file not found: %s\n' "$cfg" >"$state_dir/openclaw-config.redacted.json" + fi + + run_capture "openclaw binaries" "$state_dir/openclaw-binaries.txt" bash -c 'type -a openclaw; echo; command -v openclaw; echo; ls -l "$(command -v openclaw)" /opt/homebrew/bin/openclaw /usr/local/bin/openclaw 2>/dev/null || true' + + if [[ "$(uname -s 2>/dev/null || true)" == "Darwin" ]]; then + run_capture "launchd gateway service" "$state_dir/launchd-gateway.txt" bash -c 'launchctl print "gui/$(id -u)/ai.openclaw.gateway" 2>/dev/null | sed -n "1,160p" || true' + run_capture "macOS app version" "$state_dir/macos-app-version.txt" bash -c 'if [[ -d /Applications/OpenClaw.app ]]; then defaults read /Applications/OpenClaw.app/Contents/Info CFBundleShortVersionString 2>/dev/null || true; defaults read /Applications/OpenClaw.app/Contents/Info CFBundleVersion 2>/dev/null || true; else echo "not installed"; fi' + fi + + { launchctl print "gui/$(id -u)/ai.openclaw.gateway" 2>/dev/null || true; type -a openclaw 2>/dev/null || true; command -v openclaw 2>/dev/null || true; } \ + | grep -E '/tmp|/private/tmp' \ + | sanitize_sensitive >"$state_dir/temp-runtime-refs.txt" || true +} + +write_preflight_report() { + local report="$CUTOVER_DIR/CUTOVER.md" + local current_version + current_version="$(get_openclaw_version)" + cat >"$report" <>"$report" + fi +} + +capture_exit_ok() { + local label="$1" + local exit_file="$2" + local rc + rc="$(cat "$exit_file" 2>/dev/null || printf 'missing')" + if [[ "$rc" == "0" ]]; then + good "$label completed successfully" + return 0 + fi + bad "$label failed or was not captured (exit=$rc)" + return 1 +} + +post_verify() { + local failed=0 + local after_dir="$CUTOVER_DIR/after" + local current_version + current_version="$(get_openclaw_version)" + + if [[ "$current_version" == "$TARGET_VERSION" || "$current_version" == "v${TARGET_VERSION#v}" ]]; then + good "service version matches target: $current_version" + else + bad "service version mismatch: current=$current_version target=$TARGET_VERSION" + failed=1 + fi + + for check in \ + "gateway status:$after_dir/gateway-status.txt.exit" \ + "doctor:$after_dir/doctor.txt.exit" \ + "channels probe:$after_dir/channels-status-probe.txt.exit" \ + "cron list:$after_dir/cron-list.json.exit" + do + if ! capture_exit_ok "${check%%:*}" "${check#*:}"; then + failed=1 + fi + done + + if grep -qE '/private/tmp|(^|[^[:alpha:]])/tmp([^[:alpha:]]|$)' "$after_dir/temp-runtime-refs.txt" 2>/dev/null; then + bad "temp-backed runtime references found: $after_dir/temp-runtime-refs.txt" + failed=1 + else + good "no /tmp or /private/tmp runtime references found in captured service/path probes" + fi + + if grep -qiE 'missing.*(asset|plugin|control-ui)|version mismatch|entrypoint mismatch' "$after_dir/doctor.txt" "$after_dir/gateway-status.txt" 2>/dev/null; then + bad "doctor/status output contains mismatch or missing asset warnings" + failed=1 + else + good "no obvious version/asset mismatch warnings in doctor/status capture" + fi + + if [[ "$APP_SCOPE" =~ ^(app|both)$ ]]; then + if [[ "$(uname -s 2>/dev/null || true)" != "Darwin" ]]; then + warn "app scope is $APP_SCOPE but host is not macOS; skipping app bundle verification" + elif [[ ! -d /Applications/OpenClaw.app ]]; then + bad "app scope is $APP_SCOPE but /Applications/OpenClaw.app is not installed" + failed=1 + elif ! grep -Eq "${TARGET_VERSION#v}|$TARGET_VERSION" "$after_dir/macos-app-version.txt" 2>/dev/null; then + bad "macOS app version capture does not mention target $TARGET_VERSION" + failed=1 + else + good "macOS app version capture mentions target $TARGET_VERSION" + fi + elif [[ "$APP_SCOPE" == "cli" && "$(uname -s 2>/dev/null || true)" == "Darwin" && -d /Applications/OpenClaw.app ]]; then + warn "CLI-only cutover with /Applications/OpenClaw.app installed; app/gateway split must be intentionally accepted" + fi + + if [[ "$RUN_SMOKE" == "true" ]]; then + run_capture "agent smoke" "$after_dir/agent-smoke.txt" openclaw agent --session-id "update-cutover-smoke-$(date +%s)" --message "Health probe. Reply exactly: OPENCLAW_ALIVE" --thinking low --timeout 240 --json + if [[ "$(cat "$after_dir/agent-smoke.txt.exit" 2>/dev/null || printf '1')" == "0" ]] && grep -q 'OPENCLAW_ALIVE' "$after_dir/agent-smoke.txt"; then + good "agent smoke passed" + else + bad "agent smoke did not return expected sentinel" + failed=1 + fi + else + warn "agent/channel smoke tests not run; use --smoke or run host-specific channel tests manually" + fi + + return "$failed" +} + +case "$MODE" in + preflight) + log "Capturing preflight baseline in $CUTOVER_DIR" + capture_state before + write_preflight_report + good "Preflight report written: $CUTOVER_DIR/CUTOVER.md" + warn "Review and complete the cutover gate before running openclaw update" + ;; + post) + log "Capturing post-update state in $CUTOVER_DIR" + capture_state after + if post_verify; then + good "Post-cutover verification passed" + else + bad "Post-cutover verification failed — use the rollback rule before layering more fixes" + exit 1 + fi + ;; +esac diff --git a/tests/run.sh b/tests/run.sh index 0d4352d..a9a66c7 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -70,7 +70,7 @@ setup_fake_env() { fi export HOME="$TEST_HOME" export USERPROFILE="$TEST_HOME" - export PATH="$TEST_ROOT/bin:$PATH" + export PATH="$TEST_ROOT/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" mkdir -p "$HOME/.openclaw/logs" "$HOME/.openclaw" "$TEST_ROOT/bin" mkdir -p "$HOME/.config/systemd/user" "$TEST_ROOT/etc/systemd/system" export OPENCLAW_SECURITY_SCAN_SYSTEMD_USER_DIR="$HOME/.config/systemd/user" @@ -84,6 +84,11 @@ if [[ -n "${OPENCLAW_CALL_LOG:-}" ]]; then printf 'openclaw|skip=%s|%s\n' "${OPENCLAW_SKIP_WRAPPER_BACKUP:-0}" "$*" >>"$OPENCLAW_CALL_LOG" fi +if [[ "${OPENCLAW_FAIL_COMMAND:-}" == "$*" ]]; then + echo "simulated openclaw failure: $*" >&2 + exit 42 +fi + case "${1:-}" in --version|-V) printf '%s\n' "${OPENCLAW_STATUS_VERSION:-v2026.2.12}" @@ -704,6 +709,103 @@ EOF rm -f "$ROOT_DIR/tests/.session-monitor-stub.sh" } + +test_update_cutover_preflight_captures_gates_without_running_update() { + setup_fake_env + trap teardown_fake_env RETURN + + local cutover_dir="$HOME/.openclaw/update-cutovers/test-cutover" + export OPENCLAW_HACK_AUDIT_LOG="$HOME/.openclaw/hack-audit-log.md" + printf 'custom runtime path workaround\n' >"$OPENCLAW_HACK_AUDIT_LOG" + + local output + output="$(bash "$ROOT_DIR/scripts/update-cutover.sh" --preflight --target-version v2026.5.12 --lane official --app-scope cli --cutover-dir "$cutover_dir" 2>&1)" + assert_contains "$output" "Preflight report written" + assert_contains "$output" "Review and complete the cutover gate before running openclaw update" + + [[ -f "$cutover_dir/CUTOVER.md" ]] || fail "CUTOVER.md was not created" + [[ -f "$cutover_dir/before/openclaw-config.redacted.json" ]] || fail "config baseline was not captured" + [[ -f "$cutover_dir/before/cron-list.json" ]] || fail "cron baseline was not captured" + + local report + report="$(cat "$cutover_dir/CUTOVER.md")" + assert_contains "$report" "Lane: official" + assert_contains "$report" "App scope: cli" + assert_contains "$report" "Release notes reviewed against current setup" + assert_contains "$report" "Hack/workaround audit reviewed" + assert_contains "$report" "Rollback target/path confirmed" + assert_contains "$report" "custom runtime path workaround" +} + +test_update_cutover_post_fails_on_version_mismatch() { + setup_fake_env + trap teardown_fake_env RETURN + + local cutover_dir="$HOME/.openclaw/update-cutovers/test-cutover-post" + export OPENCLAW_STATUS_VERSION="v2026.5.11" + + local output rc + set +e + output="$(bash "$ROOT_DIR/scripts/update-cutover.sh" --post --target-version v2026.5.12 --lane official --app-scope none --cutover-dir "$cutover_dir" 2>&1)" + rc=$? + set -e + + [[ "$rc" != "0" ]] || fail "expected post verification to fail on version mismatch" + assert_contains "$output" "service version mismatch" + assert_contains "$output" "Post-cutover verification failed" + [[ -f "$cutover_dir/after/openclaw-version.txt" ]] || fail "after-state version capture missing" +} + +test_update_cutover_post_passes_when_version_matches() { + setup_fake_env + trap teardown_fake_env RETURN + + local cutover_dir="$HOME/.openclaw/update-cutovers/test-cutover-post-pass" + export OPENCLAW_STATUS_VERSION="v2026.5.12" + + local output + output="$(bash "$ROOT_DIR/scripts/update-cutover.sh" --post --target-version v2026.5.12 --lane official --app-scope none --cutover-dir "$cutover_dir" 2>&1)" + assert_contains "$output" "service version matches target" + assert_contains "$output" "gateway status completed successfully" + assert_contains "$output" "channels probe completed successfully" + assert_contains "$output" "Post-cutover verification passed" + [[ -f "$cutover_dir/after/channels-status-probe.txt" ]] || fail "channels probe capture missing" +} + +test_update_cutover_post_fails_on_channel_probe_failure() { + setup_fake_env + trap teardown_fake_env RETURN + + local cutover_dir="$HOME/.openclaw/update-cutovers/test-cutover-channel-fail" + export OPENCLAW_STATUS_VERSION="v2026.5.12" + export OPENCLAW_FAIL_COMMAND="channels status --probe" + + local output rc + set +e + output="$(bash "$ROOT_DIR/scripts/update-cutover.sh" --post --target-version v2026.5.12 --lane official --app-scope none --cutover-dir "$cutover_dir" 2>&1)" + rc=$? + set -e + + [[ "$rc" != "0" ]] || fail "expected post verification to fail on channel probe failure" + assert_contains "$output" "channels probe failed or was not captured" + assert_contains "$output" "Post-cutover verification failed" +} + +test_update_cutover_missing_argument_value_shows_usage() { + setup_fake_env + trap teardown_fake_env RETURN + + local output rc + set +e + output="$(bash "$ROOT_DIR/scripts/update-cutover.sh" --preflight --target-version 2>&1)" + rc=$? + set -e + + [[ "$rc" != "0" ]] || fail "expected missing argument value to fail" + assert_contains "$output" "Missing value for --target-version" + assert_contains "$output" "Usage:" +} + # ── check_agent_layer_health() coverage ────────────────────────────────────── # These tests verify three behaviors of the codex/agent-layer detection added # in fix/check-update-and-codex-detection: @@ -1561,6 +1663,11 @@ run_test test_version_change_survives_watchdog_for_check_update run_test test_lib_removes_generic_eval_exec_helpers run_test test_heal_incident_logging_no_longer_embeds_shell_generated_python run_test test_security_scan_detects_nested_files_and_permissions +run_test test_update_cutover_preflight_captures_gates_without_running_update +run_test test_update_cutover_post_fails_on_version_mismatch +run_test test_update_cutover_post_passes_when_version_matches +run_test test_update_cutover_post_fails_on_channel_probe_failure +run_test test_update_cutover_missing_argument_value_shows_usage run_test test_get_openclaw_version_normalizes_missing_v_prefix run_test test_health_check_passes_for_valid_targets run_test test_health_check_falls_back_to_etime_on_macos From f7b098c75d31153a481babd5e5516eec44d825f2 Mon Sep 17 00:00:00 2001 From: knoxjones Date: Wed, 20 May 2026 14:14:23 -0500 Subject: [PATCH 2/2] fix: harden update cutover preflight report --- scripts/update-cutover.sh | 59 ++++++++++++++++++++++++++++----------- tests/run.sh | 10 +++++++ 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/scripts/update-cutover.sh b/scripts/update-cutover.sh index 38fdcf3..0a0e069 100755 --- a/scripts/update-cutover.sh +++ b/scripts/update-cutover.sh @@ -134,28 +134,32 @@ capture_state() { fi { launchctl print "gui/$(id -u)/ai.openclaw.gateway" 2>/dev/null || true; type -a openclaw 2>/dev/null || true; command -v openclaw 2>/dev/null || true; } \ - | grep -E '/tmp|/private/tmp' \ + | grep -Ei 'openclaw.*(/tmp|/private/tmp)|(/tmp|/private/tmp).*openclaw' \ | sanitize_sensitive >"$state_dir/temp-runtime-refs.txt" || true } write_preflight_report() { local report="$CUTOVER_DIR/CUTOVER.md" local current_version + local created_at + local release_notes current_version="$(get_openclaw_version)" - cat >"$report" <"$report" <<'EOF' # OpenClaw Update Cutover -- Created: $(iso_now) -- Target release: $TARGET_VERSION -- Lane: $LANE -- App scope: $APP_SCOPE -- Current CLI/gateway version: $current_version -- Release notes: ${RELEASE_NOTES:-not supplied} -- Cutover dir: $CUTOVER_DIR +- Created: __CREATED_AT__ +- Target release: __TARGET_VERSION__ +- Lane: __LANE__ +- App scope: __APP_SCOPE__ +- Current CLI/gateway version: __CURRENT_VERSION__ +- Release notes: __RELEASE_NOTES__ +- Cutover dir: __CUTOVER_DIR__ ## Gap closure this report enforces -The old post-update path starts after \\`openclaw update\\`; this cutover gate adds the missing before-change decisions: +The old post-update path starts after `openclaw update`; this cutover gate adds the missing before-change decisions: - [ ] Release notes reviewed against current setup - [ ] Required fixes are confirmed present or explicitly accepted as missing @@ -182,7 +186,7 @@ Classify each relevant release-note item: ## Hack/workaround audit -Hack audit source: \\`$HACK_AUDIT_LOG\\` +Hack audit source: `__HACK_AUDIT_LOG__` - [ ] No relevant hacks found, or no audit file exists - [ ] Relevant hacks reviewed and classified below @@ -197,11 +201,11 @@ High-risk if it touches runtime paths, launch behavior, cron behavior, auth/secr Do not run the update until these are true: -- [ ] Lane is correct: $LANE -- [ ] App scope is correct: $APP_SCOPE +- [ ] Lane is correct: __LANE__ +- [ ] App scope is correct: __APP_SCOPE__ - [ ] One runtime target path selected - [ ] Rollback target/path confirmed -- [ ] Baseline files under \\`before/\\` reviewed +- [ ] Baseline files under `before/` reviewed - [ ] No unexpected /tmp or /private/tmp runtime references - [ ] Single restart/update plan prepared @@ -209,10 +213,10 @@ Do not run the update until these are true: Run after update: -\`\`\`bash +```bash bash scripts/post-update.sh -bash scripts/update-cutover.sh --post --target-version "$TARGET_VERSION" --lane "$LANE" --app-scope "$APP_SCOPE" --cutover-dir "$CUTOVER_DIR" -\`\`\` +bash scripts/update-cutover.sh --post --target-version "__TARGET_VERSION__" --lane "__LANE__" --app-scope "__APP_SCOPE__" --cutover-dir "__CUTOVER_DIR__" +``` Pass criteria: @@ -232,6 +236,27 @@ Pass criteria: If verification fails, stop layering fixes. Roll back to the prior known-good runtime target, then verify restored health. EOF + python3 - "$report" "$created_at" "$TARGET_VERSION" "$LANE" "$APP_SCOPE" "$current_version" "$release_notes" "$CUTOVER_DIR" "$HACK_AUDIT_LOG" <<'PY' +from pathlib import Path +import sys + +report = Path(sys.argv[1]) +replacements = { + "__CREATED_AT__": sys.argv[2], + "__TARGET_VERSION__": sys.argv[3], + "__LANE__": sys.argv[4], + "__APP_SCOPE__": sys.argv[5], + "__CURRENT_VERSION__": sys.argv[6], + "__RELEASE_NOTES__": sys.argv[7], + "__CUTOVER_DIR__": sys.argv[8], + "__HACK_AUDIT_LOG__": sys.argv[9], +} +text = report.read_text(encoding="utf-8") +for marker, value in replacements.items(): + text = text.replace(marker, value) +report.write_text(text, encoding="utf-8") +PY + if [[ -f "$HACK_AUDIT_LOG" ]]; then { printf '\n## Hack audit excerpt\n\n' diff --git a/tests/run.sh b/tests/run.sh index a9a66c7..d2c40d5 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -715,6 +715,8 @@ test_update_cutover_preflight_captures_gates_without_running_update() { trap teardown_fake_env RETURN local cutover_dir="$HOME/.openclaw/update-cutovers/test-cutover" + local call_log="$HOME/.openclaw/openclaw-calls.log" + export OPENCLAW_CALL_LOG="$call_log" export OPENCLAW_HACK_AUDIT_LOG="$HOME/.openclaw/hack-audit-log.md" printf 'custom runtime path workaround\n' >"$OPENCLAW_HACK_AUDIT_LOG" @@ -735,6 +737,8 @@ test_update_cutover_preflight_captures_gates_without_running_update() { assert_contains "$report" "Hack/workaround audit reviewed" assert_contains "$report" "Rollback target/path confirmed" assert_contains "$report" "custom runtime path workaround" + assert_not_contains "$(cat "$call_log")" "openclaw|skip=0|update" + unset OPENCLAW_CALL_LOG } test_update_cutover_post_fails_on_version_mismatch() { @@ -761,6 +765,12 @@ test_update_cutover_post_passes_when_version_matches() { trap teardown_fake_env RETURN local cutover_dir="$HOME/.openclaw/update-cutovers/test-cutover-post-pass" + local stable_bin_dir="${OPENCLAW_TEST_STABLE_BIN_DIR:-$ROOT_DIR/.test-openclaw-bin}" + mkdir -p "$stable_bin_dir" + cp "$TEST_ROOT/bin/openclaw" "$stable_bin_dir/openclaw" + chmod +x "$stable_bin_dir/openclaw" + PATH="$stable_bin_dir:${PATH#*:}" + export PATH export OPENCLAW_STATUS_VERSION="v2026.5.12" local output