mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 19:10:58 +00:00
1198 lines
39 KiB
Bash
1198 lines
39 KiB
Bash
#!/usr/bin/env bash
|
|
set -Eeuo pipefail
|
|
|
|
source scripts/lib/openclaw-e2e-instance.sh
|
|
|
|
export npm_config_loglevel=error
|
|
export npm_config_fund=false
|
|
export npm_config_audit=false
|
|
export CI=true
|
|
export OPENCLAW_NO_ONBOARD=1
|
|
export OPENCLAW_NO_PROMPT=1
|
|
export OPENCLAW_SKIP_PROVIDERS=1
|
|
export OPENCLAW_SKIP_CHANNELS=1
|
|
export OPENCLAW_DISABLE_BONJOUR=1
|
|
export GATEWAY_AUTH_TOKEN_REF="upgrade-survivor-token"
|
|
export OPENAI_API_KEY="sk-openclaw-upgrade-survivor"
|
|
export DISCORD_BOT_TOKEN="upgrade-survivor-discord-token"
|
|
export TELEGRAM_BOT_TOKEN="123456:upgrade-survivor-telegram-token"
|
|
export FEISHU_APP_SECRET="upgrade-survivor-feishu-secret"
|
|
export MATRIX_ACCESS_TOKEN="upgrade-survivor-matrix-token"
|
|
export BRAVE_API_KEY="BSA_upgrade_survivor_brave_key"
|
|
|
|
ARTIFACT_ROOT="$(dirname "${OPENCLAW_UPGRADE_SURVIVOR_SUMMARY_JSON:-/tmp/openclaw-upgrade-survivor-artifacts/summary.json}")"
|
|
mkdir -p "$ARTIFACT_ROOT"
|
|
export TMPDIR="$ARTIFACT_ROOT/tmp"
|
|
mkdir -p "$TMPDIR"
|
|
export npm_config_prefix="$ARTIFACT_ROOT/npm-prefix"
|
|
export NPM_CONFIG_PREFIX="$npm_config_prefix"
|
|
export npm_config_cache="$ARTIFACT_ROOT/npm-cache"
|
|
export npm_config_tmp="$TMPDIR"
|
|
mkdir -p "$npm_config_prefix" "$npm_config_cache"
|
|
export PATH="$npm_config_prefix/bin:$PATH"
|
|
|
|
SUMMARY_JSON="${OPENCLAW_UPGRADE_SURVIVOR_SUMMARY_JSON:-$ARTIFACT_ROOT/summary.json}"
|
|
PHASE_LOG="$ARTIFACT_ROOT/phases.jsonl"
|
|
BASELINE_RAW="${OPENCLAW_UPGRADE_SURVIVOR_BASELINE:?missing OPENCLAW_UPGRADE_SURVIVOR_BASELINE}"
|
|
CANDIDATE_KIND="${OPENCLAW_UPGRADE_SURVIVOR_CANDIDATE_KIND:-tarball}"
|
|
CANDIDATE_SPEC="${OPENCLAW_UPGRADE_SURVIVOR_CANDIDATE_SPEC:-${OPENCLAW_CURRENT_PACKAGE_TGZ:-}}"
|
|
SCENARIO="${OPENCLAW_UPGRADE_SURVIVOR_SCENARIO:-base}"
|
|
UPDATE_RESTART_MODE="${OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE:-manual}"
|
|
CURRENT_PHASE="setup"
|
|
FAILURE_PHASE=""
|
|
FAILURE_MESSAGE=""
|
|
gateway_pid=""
|
|
plugin_registry_pid=""
|
|
baseline_spec=""
|
|
baseline_version=""
|
|
baseline_version_expected="0"
|
|
candidate_version=""
|
|
installed_version=""
|
|
start_seconds=""
|
|
status_seconds=""
|
|
healthz_seconds=""
|
|
readyz_seconds=""
|
|
update_restart_seconds=""
|
|
|
|
BASELINE_INSTALL_LOG="$ARTIFACT_ROOT/baseline-install.log"
|
|
UPDATE_JSON="$ARTIFACT_ROOT/update.json"
|
|
UPDATE_ERR="$ARTIFACT_ROOT/update.err"
|
|
DOCTOR_LOG="$ARTIFACT_ROOT/doctor.log"
|
|
BASELINE_DOCTOR_LOG="$ARTIFACT_ROOT/baseline-doctor.log"
|
|
GATEWAY_LOG="$ARTIFACT_ROOT/gateway.log"
|
|
HEALTHZ_JSON="$ARTIFACT_ROOT/healthz.json"
|
|
READYZ_JSON="$ARTIFACT_ROOT/readyz.json"
|
|
STATUS_JSON="$ARTIFACT_ROOT/status.json"
|
|
STATUS_ERR="$ARTIFACT_ROOT/status.err"
|
|
BASELINE_CONFIG_VALIDATE_LOG="$ARTIFACT_ROOT/baseline-config-validate.log"
|
|
BASELINE_SERVICE_INSTALL_JSON="$ARTIFACT_ROOT/baseline-service-install.json"
|
|
BASELINE_SERVICE_INSTALL_ERR="$ARTIFACT_ROOT/baseline-service-install.err"
|
|
SYSTEMCTL_SHIM_LOG="$ARTIFACT_ROOT/systemctl-shim.log"
|
|
SYSTEMCTL_SHIM_PID_FILE="$ARTIFACT_ROOT/systemctl-shim.pid"
|
|
SYSTEMCTL_SHIM_DAEMON_LOG="$ARTIFACT_ROOT/systemctl-shim-gateway.log"
|
|
CONFIG_COVERAGE_JSON="$ARTIFACT_ROOT/config-recipe.json"
|
|
export OPENCLAW_UPGRADE_SURVIVOR_CONFIG_COVERAGE_JSON="$CONFIG_COVERAGE_JSON"
|
|
rm -f "$SUMMARY_JSON" "$CONFIG_COVERAGE_JSON"
|
|
: >"$PHASE_LOG"
|
|
|
|
validate_baseline_package_spec() {
|
|
local spec="$1"
|
|
if [[ "$spec" =~ ^openclaw@(alpha|beta|latest|[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-[1-9][0-9]*|-(alpha|beta)\.[1-9][0-9]*)?)$ ]]; then
|
|
return 0
|
|
fi
|
|
echo "OPENCLAW_UPGRADE_SURVIVOR_BASELINE must be openclaw@latest, openclaw@beta, openclaw@alpha, an exact OpenClaw release version, or a bare release version; got: $spec" >&2
|
|
return 1
|
|
}
|
|
|
|
normalize_baseline() {
|
|
local raw="${BASELINE_RAW//[[:space:]]/}"
|
|
if [ -z "$raw" ]; then
|
|
echo "OPENCLAW_UPGRADE_SURVIVOR_BASELINE cannot be empty" >&2
|
|
return 1
|
|
fi
|
|
case "$raw" in
|
|
openclaw@*)
|
|
baseline_spec="$raw"
|
|
baseline_version="${raw#openclaw@}"
|
|
;;
|
|
*@*)
|
|
echo "OPENCLAW_UPGRADE_SURVIVOR_BASELINE must be openclaw@<version> or a bare version" >&2
|
|
return 1
|
|
;;
|
|
*)
|
|
baseline_version="$raw"
|
|
baseline_spec="openclaw@$raw"
|
|
;;
|
|
esac
|
|
case "$baseline_version" in
|
|
latest | beta | alpha)
|
|
baseline_version=""
|
|
baseline_version_expected="0"
|
|
;;
|
|
dev | main | "")
|
|
echo "OPENCLAW_UPGRADE_SURVIVOR_BASELINE must be openclaw@latest, openclaw@beta, openclaw@alpha, openclaw@<version>, or a bare version" >&2
|
|
return 1
|
|
;;
|
|
*)
|
|
baseline_version_expected="1"
|
|
;;
|
|
esac
|
|
validate_baseline_package_spec "$baseline_spec"
|
|
}
|
|
|
|
validate_update_restart_mode() {
|
|
case "$UPDATE_RESTART_MODE" in
|
|
manual | auto-auth)
|
|
;;
|
|
*)
|
|
echo "OPENCLAW_UPGRADE_SURVIVOR_UPDATE_RESTART_MODE must be manual or auto-auth; got: $UPDATE_RESTART_MODE" >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
json_event() {
|
|
local phase="$1"
|
|
local status="$2"
|
|
PHASE_EVENT_PHASE="$phase" PHASE_EVENT_STATUS="$status" node <<'NODE' >>"$PHASE_LOG"
|
|
const event = {
|
|
phase: process.env.PHASE_EVENT_PHASE,
|
|
status: process.env.PHASE_EVENT_STATUS,
|
|
at: new Date().toISOString(),
|
|
};
|
|
process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
NODE
|
|
}
|
|
|
|
write_summary() {
|
|
local status="$1"
|
|
local message="${2:-}"
|
|
mkdir -p "$(dirname "$SUMMARY_JSON")"
|
|
SUMMARY_STATUS="$status" \
|
|
SUMMARY_MESSAGE="$message" \
|
|
SUMMARY_PHASE_LOG="$PHASE_LOG" \
|
|
SUMMARY_JSON="$SUMMARY_JSON" \
|
|
SUMMARY_BASELINE_SPEC="$baseline_spec" \
|
|
SUMMARY_BASELINE_VERSION="$baseline_version" \
|
|
SUMMARY_CANDIDATE_VERSION="$candidate_version" \
|
|
SUMMARY_INSTALLED_VERSION="$installed_version" \
|
|
SUMMARY_SCENARIO="$SCENARIO" \
|
|
SUMMARY_UPDATE_RESTART_MODE="$UPDATE_RESTART_MODE" \
|
|
SUMMARY_START_SECONDS="$start_seconds" \
|
|
SUMMARY_UPDATE_RESTART_SECONDS="$update_restart_seconds" \
|
|
SUMMARY_HEALTHZ_SECONDS="$healthz_seconds" \
|
|
SUMMARY_READYZ_SECONDS="$readyz_seconds" \
|
|
SUMMARY_STATUS_SECONDS="$status_seconds" \
|
|
SUMMARY_FAILURE_PHASE="$FAILURE_PHASE" \
|
|
SUMMARY_CONFIG_COVERAGE="$CONFIG_COVERAGE_JSON" \
|
|
node <<'NODE'
|
|
const fs = require("node:fs");
|
|
const phaseLog = process.env.SUMMARY_PHASE_LOG;
|
|
const phases = fs.existsSync(phaseLog)
|
|
? fs.readFileSync(phaseLog, "utf8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line))
|
|
: [];
|
|
const numberOrNull = (value) => {
|
|
if (!value) return null;
|
|
const parsed = Number(value);
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
};
|
|
const readJsonOrNull = (file) => {
|
|
if (!file || !fs.existsSync(file)) return null;
|
|
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
};
|
|
const summary = {
|
|
status: process.env.SUMMARY_STATUS,
|
|
baseline: {
|
|
spec: process.env.SUMMARY_BASELINE_SPEC || null,
|
|
version: process.env.SUMMARY_BASELINE_VERSION || null,
|
|
},
|
|
scenario: process.env.SUMMARY_SCENARIO || "base",
|
|
candidate: {
|
|
kind: process.env.OPENCLAW_UPGRADE_SURVIVOR_CANDIDATE_KIND || null,
|
|
spec: process.env.OPENCLAW_UPGRADE_SURVIVOR_CANDIDATE_SPEC || process.env.OPENCLAW_CURRENT_PACKAGE_TGZ || null,
|
|
version: process.env.SUMMARY_CANDIDATE_VERSION || null,
|
|
},
|
|
installedVersion: process.env.SUMMARY_INSTALLED_VERSION || null,
|
|
updateRestartMode: process.env.SUMMARY_UPDATE_RESTART_MODE || "manual",
|
|
timings: {
|
|
startupSeconds: numberOrNull(process.env.SUMMARY_START_SECONDS),
|
|
updateRestartSeconds: numberOrNull(process.env.SUMMARY_UPDATE_RESTART_SECONDS),
|
|
healthzSeconds: numberOrNull(process.env.SUMMARY_HEALTHZ_SECONDS),
|
|
readyzSeconds: numberOrNull(process.env.SUMMARY_READYZ_SECONDS),
|
|
statusSeconds: numberOrNull(process.env.SUMMARY_STATUS_SECONDS),
|
|
},
|
|
config: readJsonOrNull(process.env.SUMMARY_CONFIG_COVERAGE),
|
|
failure: process.env.SUMMARY_STATUS === "passed"
|
|
? null
|
|
: {
|
|
phase: process.env.SUMMARY_FAILURE_PHASE || null,
|
|
message: process.env.SUMMARY_MESSAGE || null,
|
|
},
|
|
phases,
|
|
};
|
|
fs.writeFileSync(process.env.SUMMARY_JSON, `${JSON.stringify(summary, null, 2)}\n`);
|
|
NODE
|
|
}
|
|
|
|
cleanup() {
|
|
if [ -n "${plugin_registry_pid:-}" ]; then
|
|
kill "$plugin_registry_pid" >/dev/null 2>&1 || true
|
|
fi
|
|
openclaw_e2e_terminate_gateways "${gateway_pid:-}"
|
|
if [ -s "$SYSTEMCTL_SHIM_PID_FILE" ]; then
|
|
local shim_pid
|
|
shim_pid="$(cat "$SYSTEMCTL_SHIM_PID_FILE" 2>/dev/null || true)"
|
|
if [[ "$shim_pid" =~ ^[0-9]+$ ]] && [ "$shim_pid" -gt 1 ]; then
|
|
openclaw_e2e_terminate_gateways "$shim_pid"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
on_error() {
|
|
local status="$1"
|
|
FAILURE_PHASE="${CURRENT_PHASE:-unknown}"
|
|
FAILURE_MESSAGE="phase ${FAILURE_PHASE} failed with status ${status}"
|
|
json_event "$FAILURE_PHASE" failed || true
|
|
return "$status"
|
|
}
|
|
|
|
on_exit() {
|
|
local status="$1"
|
|
set +e
|
|
cleanup
|
|
if [ "$status" -eq 0 ]; then
|
|
write_summary passed ""
|
|
else
|
|
[ -n "$FAILURE_PHASE" ] || FAILURE_PHASE="${CURRENT_PHASE:-unknown}"
|
|
[ -n "$FAILURE_MESSAGE" ] || FAILURE_MESSAGE="upgrade survivor failed with status $status"
|
|
write_summary failed "$FAILURE_MESSAGE"
|
|
fi
|
|
echo "Upgrade survivor summary: $SUMMARY_JSON"
|
|
cat "$SUMMARY_JSON" 2>/dev/null || true
|
|
exit "$status"
|
|
}
|
|
|
|
trap 'on_error $?' ERR
|
|
trap 'on_exit $?' EXIT
|
|
|
|
phase() {
|
|
local name="$1"
|
|
shift
|
|
CURRENT_PHASE="$name"
|
|
echo "==> upgrade-survivor:$name"
|
|
json_event "$name" started
|
|
"$@"
|
|
json_event "$name" passed
|
|
CURRENT_PHASE=""
|
|
}
|
|
|
|
package_root() {
|
|
printf '%s/lib/node_modules/openclaw\n' "$npm_config_prefix"
|
|
}
|
|
|
|
legacy_runtime_deps_symlink_plugin() {
|
|
local plugin="${OPENCLAW_UPGRADE_SURVIVOR_LEGACY_RUNTIME_DEPS_SYMLINK:-}"
|
|
if [ -z "$plugin" ]; then
|
|
return 1
|
|
fi
|
|
case "$plugin" in
|
|
*[!A-Za-z0-9._-]*)
|
|
echo "OPENCLAW_UPGRADE_SURVIVOR_LEGACY_RUNTIME_DEPS_SYMLINK must be a plugin id, got: $plugin" >&2
|
|
return 2
|
|
;;
|
|
esac
|
|
printf '%s\n' "$plugin"
|
|
}
|
|
|
|
legacy_runtime_deps_symlink_target() {
|
|
local plugin="$1"
|
|
printf '%s/@openclaw-upgrade-survivor/%s-runtime-dep\n' "$(dirname "$(package_root)")" "$plugin"
|
|
}
|
|
|
|
legacy_runtime_deps_symlink_source() {
|
|
local plugin="$1"
|
|
printf '%s/.local/bundled-plugin-runtime-deps/%s-upgrade-survivor/node_modules\n' \
|
|
"$(package_root)" \
|
|
"$plugin"
|
|
}
|
|
|
|
plugin_deps_cleanup_enabled() {
|
|
[ "$SCENARIO" = "plugin-deps-cleanup" ]
|
|
}
|
|
|
|
plugin_deps_cleanup_plugins() {
|
|
printf '%s\n' "${OPENCLAW_UPGRADE_SURVIVOR_PLUGIN_DEPS_CLEANUP_PLUGINS:-discord telegram}"
|
|
}
|
|
|
|
plugin_deps_cleanup_plugin_dirs() {
|
|
local plugin="$1"
|
|
printf '%s\n' \
|
|
"$(package_root)/dist/extensions/$plugin" \
|
|
"$(package_root)/extensions/$plugin"
|
|
}
|
|
|
|
configured_plugin_installs_enabled() {
|
|
[ "$SCENARIO" = "configured-plugin-installs" ]
|
|
}
|
|
|
|
source_only_plugin_shadow_enabled() {
|
|
[ "$SCENARIO" = "stale-source-plugin-shadow" ]
|
|
}
|
|
|
|
seed_source_only_plugin_shadow() {
|
|
source_only_plugin_shadow_enabled || return 0
|
|
|
|
local shadow_root="$OPENCLAW_STATE_DIR/extensions/opik-openclaw"
|
|
mkdir -p "$shadow_root/src"
|
|
cat >"$shadow_root/package.json" <<'JSON'
|
|
{
|
|
"name": "@opik/opik-openclaw",
|
|
"version": "0.0.0-upgrade-survivor",
|
|
"openclaw": {
|
|
"extensions": ["./src/index.ts"]
|
|
}
|
|
}
|
|
JSON
|
|
cat >"$shadow_root/openclaw.plugin.json" <<'JSON'
|
|
{
|
|
"id": "opik-openclaw",
|
|
"activation": {
|
|
"onStartup": false
|
|
},
|
|
"configSchema": {
|
|
"type": "object",
|
|
"additionalProperties": false,
|
|
"properties": {}
|
|
}
|
|
}
|
|
JSON
|
|
cat >"$shadow_root/src/index.ts" <<'TS'
|
|
export default {
|
|
id: "opik-openclaw",
|
|
name: "Source-only Opik shadow",
|
|
register() {},
|
|
};
|
|
TS
|
|
echo "Seeded source-only plugin shadow: $shadow_root"
|
|
}
|
|
|
|
configure_configured_plugin_install_fixture_registry() {
|
|
configured_plugin_installs_enabled || return 0
|
|
|
|
local fixture_root="$ARTIFACT_ROOT/configured-plugin-installs-npm-fixture"
|
|
local package_dir="$fixture_root/package"
|
|
local tarball="$fixture_root/openclaw-brave-plugin-2026.5.2.tgz"
|
|
local port_file="$fixture_root/npm-registry-port"
|
|
local log_file="$fixture_root/npm-registry.log"
|
|
mkdir -p "$package_dir"
|
|
FIXTURE_PACKAGE_DIR="$package_dir" node <<'NODE'
|
|
const fs = require("node:fs");
|
|
const path = require("node:path");
|
|
const root = process.env.FIXTURE_PACKAGE_DIR;
|
|
fs.mkdirSync(root, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(root, "package.json"),
|
|
`${JSON.stringify(
|
|
{
|
|
name: "@openclaw/brave-plugin",
|
|
version: "2026.5.2",
|
|
openclaw: { extensions: ["./index.js"] },
|
|
},
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(root, "openclaw.plugin.json"),
|
|
`${JSON.stringify(
|
|
{
|
|
id: "brave",
|
|
activation: { onStartup: false },
|
|
providerAuthEnvVars: { brave: ["BRAVE_API_KEY"] },
|
|
contracts: { webSearchProviders: ["brave"] },
|
|
configSchema: {
|
|
type: "object",
|
|
additionalProperties: false,
|
|
properties: {
|
|
webSearch: {
|
|
type: "object",
|
|
additionalProperties: false,
|
|
properties: {
|
|
apiKey: { type: ["string", "object"] },
|
|
mode: { type: "string", enum: ["web", "llm-context"] },
|
|
baseUrl: { type: ["string", "object"] },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
)}\n`,
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(root, "index.js"),
|
|
`module.exports = { id: "brave", name: "Brave Fixture", register() {} };\n`,
|
|
);
|
|
NODE
|
|
tar -czf "$tarball" -C "$fixture_root" package
|
|
node scripts/e2e/lib/plugins/npm-registry-server.mjs \
|
|
"$port_file" \
|
|
"@openclaw/brave-plugin" \
|
|
"2026.5.2" \
|
|
"$tarball" \
|
|
>"$log_file" 2>&1 &
|
|
plugin_registry_pid="$!"
|
|
|
|
for _ in $(seq 1 100); do
|
|
if [ -s "$port_file" ]; then
|
|
export NPM_CONFIG_REGISTRY="http://127.0.0.1:$(cat "$port_file")"
|
|
export npm_config_registry="$NPM_CONFIG_REGISTRY"
|
|
return 0
|
|
fi
|
|
if ! kill -0 "$plugin_registry_pid" 2>/dev/null; then
|
|
cat "$log_file" >&2 || true
|
|
return 1
|
|
fi
|
|
sleep 0.1
|
|
done
|
|
|
|
cat "$log_file" >&2 || true
|
|
echo "Timed out waiting for configured plugin install npm fixture registry." >&2
|
|
return 1
|
|
}
|
|
|
|
legacy_plugin_dependency_probe_paths() {
|
|
local plugin="$1"
|
|
local plugin_dir
|
|
while IFS= read -r plugin_dir; do
|
|
printf '%s\n' \
|
|
"$plugin_dir/node_modules" \
|
|
"$plugin_dir/.openclaw-runtime-deps.json" \
|
|
"$plugin_dir/.openclaw-runtime-deps-stamp.json" \
|
|
"$plugin_dir/.openclaw-runtime-deps-copy-upgrade-survivor" \
|
|
"$plugin_dir/.openclaw-install-stage-upgrade-survivor" \
|
|
"$plugin_dir/.openclaw-pnpm-store"
|
|
done < <(plugin_deps_cleanup_plugin_dirs "$plugin")
|
|
printf '%s\n' \
|
|
"$(package_root)/.local/bundled-plugin-runtime-deps/$plugin-upgrade-survivor" \
|
|
"$OPENCLAW_STATE_DIR/.local/bundled-plugin-runtime-deps/$plugin-upgrade-survivor" \
|
|
"$OPENCLAW_STATE_DIR/plugin-runtime-deps/$plugin-upgrade-survivor"
|
|
}
|
|
|
|
install_baseline_plugin_dependencies() {
|
|
plugin_deps_cleanup_enabled || return 0
|
|
echo "Skipping baseline doctor for plugin dependency cleanup scenario; candidate doctor owns stale dependency cleanup."
|
|
}
|
|
|
|
seed_legacy_plugin_dependency_debris() {
|
|
plugin_deps_cleanup_enabled || return 0
|
|
|
|
local found=0
|
|
local plugin
|
|
for plugin in $(plugin_deps_cleanup_plugins); do
|
|
local plugin_dir
|
|
plugin_dir=""
|
|
local candidate_dir
|
|
while IFS= read -r candidate_dir; do
|
|
if [ -d "$candidate_dir" ]; then
|
|
plugin_dir="$candidate_dir"
|
|
break
|
|
fi
|
|
done < <(plugin_deps_cleanup_plugin_dirs "$plugin")
|
|
[ -n "$plugin_dir" ] || continue
|
|
found=1
|
|
mkdir -p \
|
|
"$plugin_dir/node_modules/openclaw-upgrade-survivor-dep" \
|
|
"$plugin_dir/.openclaw-runtime-deps-copy-upgrade-survivor/node_modules/openclaw-upgrade-survivor-dep" \
|
|
"$plugin_dir/.openclaw-install-stage-upgrade-survivor" \
|
|
"$plugin_dir/.openclaw-pnpm-store" \
|
|
"$(package_root)/.local/bundled-plugin-runtime-deps/$plugin-upgrade-survivor/node_modules/openclaw-upgrade-survivor-dep" \
|
|
"$OPENCLAW_STATE_DIR/.local/bundled-plugin-runtime-deps/$plugin-upgrade-survivor/node_modules/openclaw-upgrade-survivor-dep" \
|
|
"$OPENCLAW_STATE_DIR/plugin-runtime-deps/$plugin-upgrade-survivor/node_modules/openclaw-upgrade-survivor-dep"
|
|
printf '{"name":"openclaw-upgrade-survivor-dep","version":"0.0.0"}\n' \
|
|
>"$plugin_dir/node_modules/openclaw-upgrade-survivor-dep/package.json"
|
|
printf '{"plugin":"%s","scenario":"plugin-deps-cleanup"}\n' "$plugin" \
|
|
>"$plugin_dir/.openclaw-runtime-deps.json"
|
|
printf '{"plugin":"%s","scenario":"plugin-deps-cleanup","stale":true}\n' "$plugin" \
|
|
>"$plugin_dir/.openclaw-runtime-deps-stamp.json"
|
|
printf '{"name":"openclaw-upgrade-survivor-dep","version":"0.0.0"}\n' \
|
|
>"$plugin_dir/.openclaw-runtime-deps-copy-upgrade-survivor/node_modules/openclaw-upgrade-survivor-dep/package.json"
|
|
printf '{"name":"openclaw-upgrade-survivor-dep","version":"0.0.0"}\n' \
|
|
>"$(package_root)/.local/bundled-plugin-runtime-deps/$plugin-upgrade-survivor/node_modules/openclaw-upgrade-survivor-dep/package.json"
|
|
printf '{"name":"openclaw-upgrade-survivor-dep","version":"0.0.0"}\n' \
|
|
>"$OPENCLAW_STATE_DIR/.local/bundled-plugin-runtime-deps/$plugin-upgrade-survivor/node_modules/openclaw-upgrade-survivor-dep/package.json"
|
|
printf '{"name":"openclaw-upgrade-survivor-dep","version":"0.0.0"}\n' \
|
|
>"$OPENCLAW_STATE_DIR/plugin-runtime-deps/$plugin-upgrade-survivor/node_modules/openclaw-upgrade-survivor-dep/package.json"
|
|
echo "Seeded legacy plugin dependency debris for configured plugin: $plugin"
|
|
done
|
|
|
|
if [ "$found" -ne 1 ]; then
|
|
echo "plugin-deps-cleanup scenario could not find a packaged Discord or Telegram plugin directory" >&2
|
|
find "$(package_root)/dist" -maxdepth 3 -type d 2>/dev/null >&2 || true
|
|
find "$(package_root)/extensions" -maxdepth 2 -type d 2>/dev/null >&2 || true
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
assert_legacy_plugin_dependency_debris_present() {
|
|
plugin_deps_cleanup_enabled || return 0
|
|
|
|
local found
|
|
found="$(legacy_plugin_dependency_debris_count)"
|
|
if [ "$found" -eq 0 ]; then
|
|
echo "plugin-deps-cleanup scenario did not create legacy plugin dependency debris" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
legacy_plugin_dependency_debris_count() {
|
|
local found=0
|
|
local plugin
|
|
for plugin in $(plugin_deps_cleanup_plugins); do
|
|
local probe
|
|
while IFS= read -r probe; do
|
|
if [ -e "$probe" ] || [ -L "$probe" ]; then
|
|
found=1
|
|
fi
|
|
done < <(legacy_plugin_dependency_probe_paths "$plugin")
|
|
done
|
|
printf '%s\n' "$found"
|
|
}
|
|
|
|
assert_legacy_plugin_dependency_debris_before_doctor() {
|
|
plugin_deps_cleanup_enabled || return 0
|
|
|
|
local found
|
|
found="$(legacy_plugin_dependency_debris_count)"
|
|
if [ "$found" -eq 0 ]; then
|
|
echo "Legacy plugin dependency debris was already removed before doctor; post-doctor cleanup assertion will verify it stays gone."
|
|
else
|
|
echo "Legacy plugin dependency debris survived update and will be cleaned by doctor."
|
|
fi
|
|
}
|
|
|
|
assert_legacy_plugin_dependency_debris_cleaned() {
|
|
plugin_deps_cleanup_enabled || return 0
|
|
|
|
local remaining=0
|
|
local plugin
|
|
for plugin in $(plugin_deps_cleanup_plugins); do
|
|
local probe
|
|
while IFS= read -r probe; do
|
|
if [ -e "$probe" ] || [ -L "$probe" ]; then
|
|
echo "legacy plugin dependency debris survived update/doctor: $probe" >&2
|
|
remaining=1
|
|
fi
|
|
done < <(legacy_plugin_dependency_probe_paths "$plugin")
|
|
done
|
|
if [ "$remaining" -ne 0 ]; then
|
|
return 1
|
|
fi
|
|
echo "Legacy plugin dependency debris cleaned for configured plugin dependencies."
|
|
}
|
|
|
|
seed_legacy_runtime_deps_symlink() {
|
|
local plugin
|
|
plugin="$(legacy_runtime_deps_symlink_plugin)" || {
|
|
local status=$?
|
|
[ "$status" -eq 1 ] && return 0
|
|
return "$status"
|
|
}
|
|
|
|
local plugin_dir
|
|
plugin_dir="$(package_root)/dist/extensions/$plugin"
|
|
if [ ! -d "$plugin_dir" ]; then
|
|
echo "cannot seed legacy runtime deps symlink; packaged plugin is missing: $plugin_dir" >&2
|
|
return 1
|
|
fi
|
|
|
|
local source_dir
|
|
local target_dir
|
|
source_dir="$(legacy_runtime_deps_symlink_source "$plugin")"
|
|
target_dir="$(legacy_runtime_deps_symlink_target "$plugin")"
|
|
mkdir -p "$source_dir"
|
|
mkdir -p "$(dirname "$target_dir")"
|
|
printf '{"name":"openclaw-upgrade-survivor-legacy-runtime-deps","version":"0.0.0"}\n' \
|
|
>"$source_dir/package.json"
|
|
rm -rf "$target_dir"
|
|
ln -s "$source_dir" "$target_dir"
|
|
if [ ! -L "$target_dir" ]; then
|
|
echo "failed to create legacy runtime deps symlink: $target_dir" >&2
|
|
return 1
|
|
fi
|
|
echo "Seeded legacy runtime deps symlink for $plugin: $target_dir -> $source_dir"
|
|
}
|
|
|
|
assert_legacy_runtime_deps_symlink_repaired() {
|
|
local plugin
|
|
plugin="$(legacy_runtime_deps_symlink_plugin)" || {
|
|
local status=$?
|
|
[ "$status" -eq 1 ] && return 0
|
|
return "$status"
|
|
}
|
|
|
|
local target_dir
|
|
target_dir="$(legacy_runtime_deps_symlink_target "$plugin")"
|
|
if [ -L "$target_dir" ]; then
|
|
echo "legacy runtime deps symlink survived update/doctor: $target_dir -> $(readlink "$target_dir")" >&2
|
|
return 1
|
|
fi
|
|
echo "Legacy runtime deps symlink repaired for $plugin."
|
|
}
|
|
|
|
read_installed_version() {
|
|
node -p 'JSON.parse(require("node:fs").readFileSync(process.argv[1] + "/package.json", "utf8")).version' "$(package_root)"
|
|
}
|
|
|
|
storage_preflight() {
|
|
echo "Storage preflight:"
|
|
df -h "$ARTIFACT_ROOT" "$TMPDIR" /tmp || true
|
|
}
|
|
|
|
rm_rf_retry() {
|
|
local attempt
|
|
for attempt in 1 2 3 4 5; do
|
|
rm -rf "$@" && return 0
|
|
sleep "$attempt"
|
|
done
|
|
rm -rf "$@"
|
|
}
|
|
|
|
reset_run_state() {
|
|
rm_rf_retry "$npm_config_prefix" "$TMPDIR" "$ARTIFACT_ROOT/state-home"
|
|
rm -f "$SYSTEMCTL_SHIM_PID_FILE" "$SYSTEMCTL_SHIM_DAEMON_LOG"
|
|
mkdir -p "$npm_config_prefix" "$npm_config_cache" "$TMPDIR"
|
|
}
|
|
|
|
install_baseline() {
|
|
normalize_baseline
|
|
echo "Installing baseline package: $baseline_spec"
|
|
if ! npm install -g --prefix "$npm_config_prefix" "$baseline_spec" --no-fund --no-audit >"$BASELINE_INSTALL_LOG" 2>&1; then
|
|
echo "baseline npm install failed" >&2
|
|
cat "$BASELINE_INSTALL_LOG" >&2 || true
|
|
return 1
|
|
fi
|
|
if ! command -v openclaw >/dev/null; then
|
|
echo "baseline install did not expose openclaw on PATH" >&2
|
|
echo "PATH=$PATH" >&2
|
|
find "$npm_config_prefix" -maxdepth 3 -type f -o -type l >&2 || true
|
|
return 1
|
|
fi
|
|
installed_version="$(read_installed_version)"
|
|
if [ "$baseline_version_expected" = "1" ] && [ "$installed_version" != "$baseline_version" ]; then
|
|
echo "baseline package version mismatch: expected $baseline_version, got $installed_version" >&2
|
|
cat "$(package_root)/package.json" >&2 || true
|
|
return 1
|
|
fi
|
|
baseline_version="$installed_version"
|
|
local version_output
|
|
if ! version_output="$(openclaw --version 2>&1)"; then
|
|
echo "baseline openclaw --version failed" >&2
|
|
echo "$version_output" >&2
|
|
return 1
|
|
fi
|
|
if [[ "$version_output" != *"$baseline_version"* ]]; then
|
|
echo "baseline openclaw --version mismatch: expected output to include $baseline_version" >&2
|
|
echo "$version_output" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
seed_state() {
|
|
openclaw_e2e_eval_test_state_from_b64 "${OPENCLAW_TEST_STATE_FUNCTION_B64:?missing OPENCLAW_TEST_STATE_FUNCTION_B64}"
|
|
openclaw_test_state_create "$ARTIFACT_ROOT/state-home" minimal
|
|
export OPENCLAW_UPGRADE_SURVIVOR_BASELINE_VERSION="$baseline_version"
|
|
node scripts/e2e/lib/upgrade-survivor/assertions.mjs seed
|
|
}
|
|
|
|
apply_baseline_config_recipe() {
|
|
node scripts/e2e/lib/upgrade-survivor/config-recipe.mjs apply \
|
|
--summary "$CONFIG_COVERAGE_JSON" \
|
|
--baseline-version "$baseline_version"
|
|
}
|
|
|
|
validate_baseline_config() {
|
|
if ! openclaw config validate >"$BASELINE_CONFIG_VALIDATE_LOG" 2>&1; then
|
|
echo "generated baseline config failed baseline validation" >&2
|
|
cat "$BASELINE_CONFIG_VALIDATE_LOG" >&2 || true
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
install_update_restart_systemctl_shim() {
|
|
local shim_dir="$npm_config_prefix/bin"
|
|
mkdir -p "$shim_dir"
|
|
cat >"$shim_dir/systemctl" <<'SHIM'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
log_file="${OPENCLAW_UPGRADE_SURVIVOR_SYSTEMCTL_SHIM_LOG:-/tmp/openclaw-systemctl-shim.log}"
|
|
pid_file="${OPENCLAW_UPGRADE_SURVIVOR_SYSTEMCTL_SHIM_PID_FILE:-/tmp/openclaw-systemctl-shim.pid}"
|
|
daemon_log="${OPENCLAW_UPGRADE_SURVIVOR_SYSTEMCTL_SHIM_DAEMON_LOG:-/tmp/openclaw-systemctl-shim-gateway.log}"
|
|
printf '%s\n' "$*" >>"$log_file"
|
|
|
|
filtered=()
|
|
for ((i = 1; i <= $#; i++)); do
|
|
arg="${!i}"
|
|
case "$arg" in
|
|
--user | --quiet | --no-page | --now)
|
|
;;
|
|
--property)
|
|
i=$((i + 1))
|
|
;;
|
|
*)
|
|
filtered+=("$arg")
|
|
;;
|
|
esac
|
|
done
|
|
|
|
command="${filtered[0]:-status}"
|
|
|
|
is_running() {
|
|
[ -s "$pid_file" ] || return 1
|
|
local pid
|
|
pid="$(cat "$pid_file" 2>/dev/null || true)"
|
|
[ -n "$pid" ] || return 1
|
|
kill -0 "$pid" >/dev/null 2>&1
|
|
}
|
|
|
|
stop_gateway() {
|
|
[ -s "$pid_file" ] || return 0
|
|
local pid
|
|
pid="$(cat "$pid_file" 2>/dev/null || true)"
|
|
if [[ "$pid" =~ ^[0-9]+$ ]] && [ "$pid" -gt 1 ] && kill -0 "$pid" >/dev/null 2>&1; then
|
|
kill "$pid" >/dev/null 2>&1 || true
|
|
for _ in $(seq 1 100); do
|
|
kill -0 "$pid" >/dev/null 2>&1 || break
|
|
sleep 0.1
|
|
done
|
|
kill -9 "$pid" >/dev/null 2>&1 || true
|
|
fi
|
|
rm -f "$pid_file"
|
|
}
|
|
|
|
unit_path() {
|
|
printf '%s/.config/systemd/user/openclaw-gateway.service\n' "${HOME:?missing HOME}"
|
|
}
|
|
|
|
load_unit_environment() {
|
|
local unit="$1"
|
|
while IFS= read -r line; do
|
|
case "$line" in
|
|
EnvironmentFile=*)
|
|
local spec="${line#EnvironmentFile=}"
|
|
for token in $spec; do
|
|
local file="${token#-}"
|
|
[ -f "$file" ] || continue
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
. "$file"
|
|
set +a
|
|
done
|
|
;;
|
|
Environment=*)
|
|
local assignment="${line#Environment=}"
|
|
assignment="${assignment#\"}"
|
|
assignment="${assignment%\"}"
|
|
export "$assignment"
|
|
;;
|
|
esac
|
|
done <"$unit"
|
|
}
|
|
|
|
start_gateway() {
|
|
local unit
|
|
local exec_start
|
|
unit="$(unit_path)"
|
|
exec_start="$(sed -n 's/^ExecStart=//p' "$unit" | tail -n 1)"
|
|
[ -n "$exec_start" ] || {
|
|
echo "systemctl shim could not find ExecStart in $unit" >&2
|
|
return 1
|
|
}
|
|
(
|
|
load_unit_environment "$unit"
|
|
nohup bash -lc "exec $exec_start" >>"$daemon_log" 2>&1 &
|
|
printf '%s\n' "$!" >"$pid_file"
|
|
)
|
|
}
|
|
|
|
case "$command" in
|
|
daemon-reload | enable | disable)
|
|
exit 0
|
|
;;
|
|
status)
|
|
is_running && exit 0
|
|
exit 0
|
|
;;
|
|
stop)
|
|
stop_gateway
|
|
exit 0
|
|
;;
|
|
restart | start)
|
|
stop_gateway
|
|
start_gateway
|
|
exit 0
|
|
;;
|
|
is-enabled)
|
|
exit 0
|
|
;;
|
|
is-active)
|
|
is_running && exit 0
|
|
exit 3
|
|
;;
|
|
show)
|
|
if is_running; then
|
|
printf 'ActiveState=active\nSubState=running\nMainPID=%s\nExecMainStatus=0\nExecMainCode=0\n' "$(cat "$pid_file")"
|
|
else
|
|
printf 'ActiveState=inactive\nSubState=dead\nMainPID=0\nExecMainStatus=0\nExecMainCode=0\n'
|
|
fi
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "systemctl shim unsupported command: $*" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
SHIM
|
|
chmod +x "$shim_dir/systemctl"
|
|
export OPENCLAW_UPGRADE_SURVIVOR_SYSTEMCTL_SHIM_LOG="$SYSTEMCTL_SHIM_LOG"
|
|
export OPENCLAW_UPGRADE_SURVIVOR_SYSTEMCTL_SHIM_PID_FILE="$SYSTEMCTL_SHIM_PID_FILE"
|
|
export OPENCLAW_UPGRADE_SURVIVOR_SYSTEMCTL_SHIM_DAEMON_LOG="$SYSTEMCTL_SHIM_DAEMON_LOG"
|
|
export PATH="$shim_dir:$PATH"
|
|
}
|
|
|
|
install_update_restart_service_unit() {
|
|
if ! env -u OPENCLAW_GATEWAY_TOKEN -u OPENCLAW_GATEWAY_PASSWORD openclaw gateway install --force --json >"$BASELINE_SERVICE_INSTALL_JSON" 2>"$BASELINE_SERVICE_INSTALL_ERR"; then
|
|
echo "baseline gateway service install failed" >&2
|
|
cat "$BASELINE_SERVICE_INSTALL_ERR" >&2 || true
|
|
cat "$BASELINE_SERVICE_INSTALL_JSON" >&2 || true
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
seed_update_restart_probe_device_auth() {
|
|
node --input-type=module <<'NODE'
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const stateDir = process.env.OPENCLAW_STATE_DIR;
|
|
if (!stateDir) {
|
|
throw new Error("missing OPENCLAW_STATE_DIR");
|
|
}
|
|
|
|
const base64UrlEncode = (buf) =>
|
|
buf.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, "");
|
|
const ed25519SpkiPrefix = Buffer.from("302a300506032b6570032100", "hex");
|
|
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
|
|
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
|
|
const spki = crypto.createPublicKey(publicKeyPem).export({ type: "spki", format: "der" });
|
|
const rawPublicKey =
|
|
spki.length === ed25519SpkiPrefix.length + 32 &&
|
|
spki.subarray(0, ed25519SpkiPrefix.length).equals(ed25519SpkiPrefix)
|
|
? spki.subarray(ed25519SpkiPrefix.length)
|
|
: spki;
|
|
const publicKeyRaw = base64UrlEncode(rawPublicKey);
|
|
const deviceId = crypto.createHash("sha256").update(rawPublicKey).digest("hex");
|
|
const token = base64UrlEncode(crypto.randomBytes(32));
|
|
const now = Date.now();
|
|
const scopes = ["operator.read"];
|
|
|
|
function writeJson(filePath, value) {
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
try {
|
|
fs.chmodSync(filePath, 0o600);
|
|
} catch {
|
|
// best-effort inside Docker
|
|
}
|
|
}
|
|
|
|
writeJson(path.join(stateDir, "identity", "device.json"), {
|
|
version: 1,
|
|
deviceId,
|
|
publicKeyPem,
|
|
privateKeyPem,
|
|
createdAtMs: now,
|
|
});
|
|
writeJson(path.join(stateDir, "identity", "device-auth.json"), {
|
|
version: 1,
|
|
deviceId,
|
|
tokens: {
|
|
operator: {
|
|
token,
|
|
role: "operator",
|
|
scopes,
|
|
updatedAtMs: now,
|
|
},
|
|
},
|
|
});
|
|
writeJson(path.join(stateDir, "devices", "paired.json"), {
|
|
[deviceId]: {
|
|
deviceId,
|
|
publicKey: publicKeyRaw,
|
|
displayName: "upgrade survivor restart probe",
|
|
platform: process.platform,
|
|
clientId: "upgrade-survivor",
|
|
clientMode: "probe",
|
|
role: "operator",
|
|
roles: ["operator"],
|
|
scopes,
|
|
approvedScopes: scopes,
|
|
tokens: {
|
|
operator: {
|
|
token,
|
|
role: "operator",
|
|
scopes,
|
|
createdAtMs: now,
|
|
},
|
|
},
|
|
createdAtMs: now,
|
|
approvedAtMs: now,
|
|
},
|
|
});
|
|
writeJson(path.join(stateDir, "devices", "pending.json"), {});
|
|
NODE
|
|
}
|
|
|
|
write_update_restart_service_secretref_env() {
|
|
mkdir -p "$OPENCLAW_STATE_DIR"
|
|
local dotenv_path="$OPENCLAW_STATE_DIR/.env"
|
|
local tmp_path="$dotenv_path.tmp.$$"
|
|
if [ -f "$dotenv_path" ]; then
|
|
grep -v '^GATEWAY_AUTH_TOKEN_REF=' "$dotenv_path" >"$tmp_path" || true
|
|
else
|
|
: >"$tmp_path"
|
|
fi
|
|
# Managed restarts resolve SecretRefs from service-owned durable env, not the update caller.
|
|
printf 'GATEWAY_AUTH_TOKEN_REF=%s\n' "$GATEWAY_AUTH_TOKEN_REF" >>"$tmp_path"
|
|
mv "$tmp_path" "$dotenv_path"
|
|
}
|
|
|
|
write_update_restart_service_auth_env() {
|
|
mkdir -p "$OPENCLAW_STATE_DIR"
|
|
local dotenv_path="$OPENCLAW_STATE_DIR/.env"
|
|
local tmp_path="$dotenv_path.tmp.$$"
|
|
if [ -f "$dotenv_path" ]; then
|
|
grep -v '^GATEWAY_AUTH_TOKEN_REF=' "$dotenv_path" >"$tmp_path" || true
|
|
else
|
|
: >"$tmp_path"
|
|
fi
|
|
printf 'GATEWAY_AUTH_TOKEN_REF=%s\n' "$GATEWAY_AUTH_TOKEN_REF" >>"$tmp_path"
|
|
mv "$tmp_path" "$dotenv_path"
|
|
local systemd_env_path="$OPENCLAW_STATE_DIR/gateway.systemd.env"
|
|
printf 'GATEWAY_AUTH_TOKEN_REF=%s\n' "$GATEWAY_AUTH_TOKEN_REF" >"$systemd_env_path"
|
|
}
|
|
|
|
prepare_update_restart_probe() {
|
|
if [ "$UPDATE_RESTART_MODE" != "auto-auth" ]; then
|
|
return 0
|
|
fi
|
|
echo "Preparing configured-auth gateway for automatic update restart."
|
|
install_update_restart_systemctl_shim
|
|
seed_update_restart_probe_device_auth
|
|
start_gateway
|
|
write_update_restart_service_secretref_env
|
|
install_update_restart_service_unit
|
|
}
|
|
|
|
prepare_update_restart_probe_current_install() {
|
|
if [ "$UPDATE_RESTART_MODE" != "auto-auth" ]; then
|
|
return 0
|
|
fi
|
|
echo "Preparing candidate-auth gateway for automatic update restart."
|
|
install_update_restart_systemctl_shim
|
|
seed_update_restart_probe_device_auth
|
|
start_gateway
|
|
write_update_restart_service_auth_env
|
|
install_update_restart_service_unit
|
|
}
|
|
|
|
assert_baseline_state() {
|
|
OPENCLAW_UPGRADE_SURVIVOR_ASSERT_STAGE=baseline \
|
|
node scripts/e2e/lib/upgrade-survivor/assertions.mjs assert-config
|
|
OPENCLAW_UPGRADE_SURVIVOR_ASSERT_STAGE=baseline \
|
|
node scripts/e2e/lib/upgrade-survivor/assertions.mjs assert-state
|
|
}
|
|
|
|
resolve_candidate_version() {
|
|
if [ -z "$CANDIDATE_SPEC" ]; then
|
|
echo "missing OPENCLAW_UPGRADE_SURVIVOR_CANDIDATE_SPEC" >&2
|
|
return 1
|
|
fi
|
|
case "$CANDIDATE_KIND" in
|
|
tarball)
|
|
candidate_version="$(
|
|
node -e '
|
|
const { execFileSync } = require("node:child_process");
|
|
const packageJson = execFileSync("tar", ["-xOf", process.argv[1], "package/package.json"], {
|
|
encoding: "utf8",
|
|
});
|
|
process.stdout.write(JSON.parse(packageJson).version);
|
|
' "$CANDIDATE_SPEC"
|
|
)"
|
|
;;
|
|
npm)
|
|
candidate_version="$(npm view "$CANDIDATE_SPEC" version --silent)"
|
|
;;
|
|
*)
|
|
echo "unknown candidate kind: $CANDIDATE_KIND" >&2
|
|
return 1
|
|
;;
|
|
esac
|
|
if [ -z "$candidate_version" ]; then
|
|
echo "could not resolve candidate version from $CANDIDATE_KIND:$CANDIDATE_SPEC" >&2
|
|
return 1
|
|
fi
|
|
OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT="$(
|
|
node scripts/e2e/lib/package-compat.mjs "$candidate_version"
|
|
)"
|
|
export OPENCLAW_PACKAGE_ACCEPTANCE_LEGACY_COMPAT
|
|
}
|
|
|
|
update_candidate() {
|
|
echo "Updating baseline $baseline_spec to candidate $CANDIDATE_KIND:$CANDIDATE_SPEC ($candidate_version)"
|
|
local update_start=""
|
|
local update_end=""
|
|
local update_args=(update --tag "$CANDIDATE_SPEC" --yes --json)
|
|
if [ "$UPDATE_RESTART_MODE" = "manual" ]; then
|
|
update_args+=(--no-restart)
|
|
else
|
|
update_start="$(node -e "process.stdout.write(String(Date.now()))")"
|
|
fi
|
|
if ! env -u OPENCLAW_GATEWAY_TOKEN -u OPENCLAW_GATEWAY_PASSWORD openclaw "${update_args[@]}" >"$UPDATE_JSON" 2>"$UPDATE_ERR"; then
|
|
echo "openclaw update failed" >&2
|
|
cat "$UPDATE_ERR" >&2 || true
|
|
cat "$UPDATE_JSON" >&2 || true
|
|
return 1
|
|
fi
|
|
if [ "$UPDATE_RESTART_MODE" = "auto-auth" ]; then
|
|
update_end="$(node -e "process.stdout.write(String(Date.now()))")"
|
|
update_restart_seconds=$(((update_end - update_start + 999) / 1000))
|
|
node -e '
|
|
const fs = require("node:fs");
|
|
const file = process.argv[1];
|
|
const result = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
if (!result || result.status !== "ok") {
|
|
throw new Error(`update JSON did not report ok status: ${JSON.stringify(result)}`);
|
|
}
|
|
' "$UPDATE_JSON"
|
|
fi
|
|
installed_version="$(read_installed_version)"
|
|
}
|
|
|
|
run_doctor() {
|
|
if ! openclaw doctor --fix --non-interactive >"$DOCTOR_LOG" 2>&1; then
|
|
echo "openclaw doctor failed" >&2
|
|
cat "$DOCTOR_LOG" >&2 || true
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
validate_post_doctor_config() {
|
|
if ! openclaw config validate >>"$DOCTOR_LOG" 2>&1; then
|
|
echo "post-doctor config validation failed" >&2
|
|
cat "$DOCTOR_LOG" >&2 || true
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
assert_survival() {
|
|
node scripts/e2e/lib/upgrade-survivor/assertions.mjs assert-config
|
|
node scripts/e2e/lib/upgrade-survivor/assertions.mjs assert-state
|
|
installed_version="$(read_installed_version)"
|
|
if [ "$installed_version" != "$candidate_version" ]; then
|
|
echo "candidate package version mismatch: expected $candidate_version, got $installed_version" >&2
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
probe_gateway_endpoint() {
|
|
local path="$1"
|
|
local expect_kind="$2"
|
|
local out_file="$3"
|
|
local start_epoch
|
|
local end_epoch
|
|
local args=(
|
|
--base-url "http://127.0.0.1:18789"
|
|
--path "$path"
|
|
--expect "$expect_kind"
|
|
)
|
|
if [ -n "${OPENCLAW_UPGRADE_SURVIVOR_READYZ_ALLOW_FAILING:-}" ]; then
|
|
args+=(--allow-failing "$OPENCLAW_UPGRADE_SURVIVOR_READYZ_ALLOW_FAILING")
|
|
fi
|
|
args+=(--out "$out_file")
|
|
start_epoch="$(node -e "process.stdout.write(String(Date.now()))")"
|
|
node scripts/e2e/lib/upgrade-survivor/probe-gateway.mjs "${args[@]}"
|
|
end_epoch="$(node -e "process.stdout.write(String(Date.now()))")"
|
|
printf '%s\n' "$(((end_epoch - start_epoch + 999) / 1000))"
|
|
}
|
|
|
|
start_gateway() {
|
|
local port=18789
|
|
local budget="${OPENCLAW_UPGRADE_SURVIVOR_START_BUDGET_SECONDS:-90}"
|
|
local start_epoch
|
|
local ready_epoch
|
|
start_epoch="$(node -e "process.stdout.write(String(Date.now()))")"
|
|
env -u OPENCLAW_GATEWAY_TOKEN -u OPENCLAW_GATEWAY_PASSWORD openclaw gateway --port "$port" --bind loopback --allow-unconfigured >"$GATEWAY_LOG" 2>&1 &
|
|
gateway_pid="$!"
|
|
if [ "$UPDATE_RESTART_MODE" = "auto-auth" ]; then
|
|
printf '%s\n' "$gateway_pid" >"$SYSTEMCTL_SHIM_PID_FILE"
|
|
fi
|
|
openclaw_e2e_wait_gateway_ready "$gateway_pid" "$GATEWAY_LOG" 360
|
|
ready_epoch="$(node -e "process.stdout.write(String(Date.now()))")"
|
|
start_seconds=$(((ready_epoch - start_epoch + 999) / 1000))
|
|
if [ "$start_seconds" -gt "$budget" ]; then
|
|
echo "gateway startup exceeded survivor budget: ${start_seconds}s > ${budget}s" >&2
|
|
cat "$GATEWAY_LOG" >&2 || true
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
ensure_gateway_started() {
|
|
if [ "$UPDATE_RESTART_MODE" = "auto-auth" ]; then
|
|
return 0
|
|
fi
|
|
start_gateway
|
|
}
|
|
|
|
check_gateway_probes() {
|
|
healthz_seconds="$(probe_gateway_endpoint /healthz live "$HEALTHZ_JSON")"
|
|
export OPENCLAW_UPGRADE_SURVIVOR_READYZ_ALLOW_FAILING="discord,telegram,whatsapp,feishu,matrix"
|
|
readyz_seconds="$(probe_gateway_endpoint /readyz ready "$READYZ_JSON")"
|
|
unset OPENCLAW_UPGRADE_SURVIVOR_READYZ_ALLOW_FAILING
|
|
}
|
|
|
|
check_gateway_status() {
|
|
local port=18789
|
|
local budget="${OPENCLAW_UPGRADE_SURVIVOR_STATUS_BUDGET_SECONDS:-30}"
|
|
local status_start
|
|
local status_end
|
|
status_start="$(node -e "process.stdout.write(String(Date.now()))")"
|
|
if ! openclaw gateway status --url "ws://127.0.0.1:$port" --token "$GATEWAY_AUTH_TOKEN_REF" --require-rpc --timeout 30000 --json >"$STATUS_JSON" 2>"$STATUS_ERR"; then
|
|
echo "gateway status failed" >&2
|
|
cat "$STATUS_ERR" >&2 || true
|
|
cat "$GATEWAY_LOG" >&2 || true
|
|
return 1
|
|
fi
|
|
status_end="$(node -e "process.stdout.write(String(Date.now()))")"
|
|
status_seconds=$(((status_end - status_start + 999) / 1000))
|
|
if [ "$status_seconds" -gt "$budget" ]; then
|
|
echo "gateway status exceeded survivor budget: ${status_seconds}s > ${budget}s" >&2
|
|
cat "$STATUS_JSON" >&2 || true
|
|
return 1
|
|
fi
|
|
node scripts/e2e/lib/upgrade-survivor/assertions.mjs assert-status-json "$STATUS_JSON"
|
|
}
|
|
|
|
phase storage-preflight storage_preflight
|
|
phase validate-update-restart-mode validate_update_restart_mode
|
|
phase reset-run-state reset_run_state
|
|
phase install-baseline install_baseline
|
|
phase seed-state seed_state
|
|
phase apply-baseline-config-recipe apply_baseline_config_recipe
|
|
phase validate-baseline-config validate_baseline_config
|
|
phase install-baseline-plugin-dependencies install_baseline_plugin_dependencies
|
|
phase seed-legacy-plugin-dependency-debris seed_legacy_plugin_dependency_debris
|
|
phase assert-legacy-plugin-dependency-debris assert_legacy_plugin_dependency_debris_present
|
|
phase seed-source-only-plugin-shadow seed_source_only_plugin_shadow
|
|
phase assert-baseline assert_baseline_state
|
|
phase seed-legacy-runtime-deps-symlink seed_legacy_runtime_deps_symlink
|
|
phase resolve-candidate resolve_candidate_version
|
|
phase prepare-update-restart-probe prepare_update_restart_probe
|
|
phase update-candidate update_candidate
|
|
phase assert-legacy-plugin-dependency-debris-before-doctor assert_legacy_plugin_dependency_debris_before_doctor
|
|
phase configure-configured-plugin-install-fixture-registry configure_configured_plugin_install_fixture_registry
|
|
phase doctor run_doctor
|
|
phase assert-legacy-plugin-dependency-debris-cleaned assert_legacy_plugin_dependency_debris_cleaned
|
|
phase assert-legacy-runtime-deps-symlink-repaired assert_legacy_runtime_deps_symlink_repaired
|
|
phase validate-post-doctor-config validate_post_doctor_config
|
|
phase assert-survival assert_survival
|
|
phase gateway-start ensure_gateway_started
|
|
phase gateway-probes check_gateway_probes
|
|
phase gateway-status check_gateway_status
|
|
|
|
echo "Upgrade survivor Docker E2E passed baseline=${baseline_spec} scenario=${SCENARIO} candidate=${candidate_version} updateRestartMode=${UPDATE_RESTART_MODE} startup=${start_seconds}s updateRestart=${update_restart_seconds:-manual}s healthz=${healthz_seconds}s readyz=${readyz_seconds}s status=${status_seconds}s."
|