test: add windows dev-update smoke lanes

This commit is contained in:
Peter Steinberger
2026-04-06 03:48:36 +01:00
parent edab013e51
commit 71d2eba0a6
2 changed files with 742 additions and 41 deletions

View File

@@ -67,14 +67,20 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
- Preferred entrypoint: `pnpm test:parallels:windows`
- Use the snapshot closest to `pre-openclaw-native-e2e-2026-03-12`.
- Default upgrade coverage on Windows should now include: fresh snapshot -> site installer pinned to the requested stable tag -> `openclaw update --channel dev` on the guest. Keep the older host-tgz upgrade path only when the caller explicitly passes `--target-package-spec`.
- Optional exact npm-tag baseline on Windows: `bash scripts/e2e/parallels-windows-smoke.sh --mode upgrade --target-package-spec openclaw@<tag> --json`. That lane installs the published npm tarball as baseline, then runs `openclaw update --channel dev`.
- Optional forward-fix Windows validation: `bash scripts/e2e/parallels-windows-smoke.sh --mode upgrade --upgrade-from-packed-main --json`. That lane installs the packed current-main npm tgz as baseline, then runs `openclaw update --channel dev`.
- Always use `prlctl exec --current-user`; plain `prlctl exec` lands in `NT AUTHORITY\\SYSTEM`.
- Prefer explicit `npm.cmd` and `openclaw.cmd`.
- Use PowerShell only as the transport with `-ExecutionPolicy Bypass`, then call the `.cmd` shims from inside it.
- Current Windows Node installs expose `corepack` as a `.cmd` shim. If a release-to-dev lane sees `corepack` on PATH but `openclaw update --channel dev` still behaves as if corepack is missing, treat that as an exec-shim regression first.
- If an exact published-tag Windows lane fails during preflight with `npm run build` and `'pnpm' is not recognized`, remember that the guest is still executing the old published updater. Validate the fix with `--upgrade-from-packed-main`, then wait for the next tagged npm release before expecting the historical tag lane to pass.
- Multi-word `openclaw agent --message ...` checks should call `& $openclaw ...` inside PowerShell, not `Start-Process ... -ArgumentList` against `openclaw.cmd`, or Commander can see split argv and throw `too many arguments for 'agent'`.
- Windows installer/tgz phases now retry once after guest-ready recheck; keep new Windows smoke steps idempotent so a transport-flake retry is safe.
- If a Windows retry sees the VM become `suspended` or `stopped`, resume/start it before the next `prlctl exec`; otherwise the second attempt just repeats the same `rc=255`.
- Windows global `npm install -g` phases can stay quiet for a minute or more even when healthy; inspect the phase log before calling it hung, and only treat it as a regression once the retry wrapper or timeout trips.
- Fresh Windows tgz install phases should also use the background PowerShell runner plus done-file/log-drain pattern; do not rely on one long-lived `prlctl exec ... powershell ... npm install -g` transport for package installs.
- Windows release-to-dev helpers should log `where pnpm` before and after the update and require `where pnpm` to succeed post-update. That proves the updater installed or enabled `pnpm` itself instead of depending on a smoke-only bootstrap.
- Fresh Windows ref-mode onboard should use the same background PowerShell runner plus done-file/log-drain pattern as the npm-update helper, including startup materialization checks, host-side timeouts on short poll `prlctl exec` calls, and retry-on-poll-failure behavior for transient transport flakes.
- Fresh Windows daemon-health reachability should use a hello-only gateway probe and a longer per-probe timeout than the default local attach path; full health RPCs are too eager during initial startup on current main.
- Fresh Windows ref-mode agent verification should set `OPENAI_API_KEY` in the PowerShell environment before invoking `openclaw.cmd agent`, for the same pairing-required fallback reason as macOS.

View File

@@ -16,6 +16,7 @@ HOST_IP=""
LATEST_VERSION=""
INSTALL_VERSION=""
TARGET_PACKAGE_SPEC=""
UPGRADE_FROM_PACKED_MAIN=0
JSON_OUTPUT=0
KEEP_SERVER=0
CHECK_LATEST_REF=1
@@ -28,8 +29,11 @@ MAIN_TGZ_DIR="$(mktemp -d)"
MAIN_TGZ_PATH=""
MINGIT_ZIP_PATH=""
MINGIT_ZIP_NAME=""
WINDOWS_LATEST_INSTALL_SCRIPT_PATH=""
WINDOWS_BASELINE_INSTALL_SCRIPT_PATH=""
WINDOWS_INSTALL_SCRIPT_PATH=""
WINDOWS_ONBOARD_SCRIPT_PATH=""
WINDOWS_DEV_UPDATE_SCRIPT_PATH=""
SERVER_PID=""
RUN_DIR="$(mktemp -d /tmp/openclaw-parallels-windows.XXXXXX)"
BUILD_LOCK_DIR="${TMPDIR:-/tmp}/openclaw-parallels-build.lock"
@@ -58,13 +62,41 @@ say() {
}
artifact_label() {
if [[ "$TARGET_PACKAGE_SPEC" == "" && "$MODE" == "upgrade" && "$UPGRADE_FROM_PACKED_MAIN" -eq 0 ]]; then
printf 'Windows smoke artifacts'
return
fi
if [[ -n "$TARGET_PACKAGE_SPEC" ]]; then
printf 'target package tgz'
printf 'baseline package tgz'
return
fi
if [[ "$UPGRADE_FROM_PACKED_MAIN" -eq 1 ]]; then
printf 'packed main tgz'
return
fi
printf 'current main tgz'
}
upgrade_uses_host_tgz() {
[[ "$UPGRADE_FROM_PACKED_MAIN" -eq 1 || -n "$TARGET_PACKAGE_SPEC" ]]
}
needs_host_tgz() {
[[ "$MODE" == "fresh" || "$MODE" == "both" ]] || upgrade_uses_host_tgz
}
upgrade_summary_label() {
if [[ -n "$TARGET_PACKAGE_SPEC" ]]; then
printf 'target-package->dev'
return
fi
if [[ "$UPGRADE_FROM_PACKED_MAIN" -eq 1 ]]; then
printf 'packed-main->dev'
return
fi
printf 'latest->dev'
}
extract_package_build_commit_from_tgz() {
tar -xOf "$1" package/dist/build-info.json | python3 -c 'import json, sys; print(json.load(sys.stdin).get("commit", ""))'
}
@@ -106,9 +138,15 @@ Options:
--host-ip <ip> Override Parallels host IP.
--latest-version <ver> Override npm latest version lookup.
--install-version <ver> Pin site-installer version/dist-tag for the baseline lane.
--upgrade-from-packed-main
Upgrade lane: install the packed current-main npm tgz as baseline,
then run openclaw update --channel dev.
--target-package-spec <npm-spec>
Install this npm package tarball instead of packing current main.
Upgrade lane: install this npm package tarball as the baseline,
then run openclaw update --channel dev.
Fresh lane: install this npm package tarball instead of packing current main.
Example: openclaw@2026.3.13-beta.1
Default upgrade lane without this flag: latest/site installer -> dev channel update.
--skip-latest-ref-check Skip latest-release ref-mode precheck.
--keep-server Leave temp host HTTP server running.
--json Print machine-readable JSON summary.
@@ -162,6 +200,10 @@ while [[ $# -gt 0 ]]; do
INSTALL_VERSION="$2"
shift 2
;;
--upgrade-from-packed-main)
UPGRADE_FROM_PACKED_MAIN=1
shift
;;
--target-package-spec)
TARGET_PACKAGE_SPEC="$2"
shift 2
@@ -653,6 +695,14 @@ resolve_latest_version() {
npm view openclaw version --userconfig "$(mktemp)"
}
baseline_install_version() {
if [[ -n "$INSTALL_VERSION" ]]; then
printf '%s\n' "$INSTALL_VERSION"
return
fi
printf '%s\n' "$LATEST_VERSION"
}
resolve_mingit_download() {
python3 - <<'PY'
import json
@@ -778,19 +828,24 @@ EOF
)"
}
ensure_mingit_zip() {
local mingit_name mingit_url
mapfile -t mingit_meta < <(resolve_mingit_download)
mingit_name="${mingit_meta[0]}"
mingit_url="${mingit_meta[1]}"
MINGIT_ZIP_NAME="$mingit_name"
MINGIT_ZIP_PATH="$MAIN_TGZ_DIR/$mingit_name"
if [[ ! -f "$MINGIT_ZIP_PATH" ]]; then
say "Download $MINGIT_ZIP_NAME"
curl -fsSL "$mingit_url" -o "$MINGIT_ZIP_PATH"
fi
}
pack_main_tgz() {
local mingit_name mingit_url short_head pkg packed_commit
local short_head pkg packed_commit
ensure_mingit_zip
if [[ -n "$TARGET_PACKAGE_SPEC" ]]; then
say "Pack target package tgz: $TARGET_PACKAGE_SPEC"
mapfile -t mingit_meta < <(resolve_mingit_download)
mingit_name="${mingit_meta[0]}"
mingit_url="${mingit_meta[1]}"
MINGIT_ZIP_NAME="$mingit_name"
MINGIT_ZIP_PATH="$MAIN_TGZ_DIR/$mingit_name"
if [[ ! -f "$MINGIT_ZIP_PATH" ]]; then
say "Download $MINGIT_ZIP_NAME"
curl -fsSL "$mingit_url" -o "$MINGIT_ZIP_PATH"
fi
pkg="$(
npm pack "$TARGET_PACKAGE_SPEC" --ignore-scripts --json --pack-destination "$MAIN_TGZ_DIR" \
| python3 -c 'import json, sys; data = json.load(sys.stdin); print(data[-1]["filename"])'
@@ -803,15 +858,6 @@ pack_main_tgz() {
fi
say "Pack current main tgz"
ensure_current_build
mapfile -t mingit_meta < <(resolve_mingit_download)
mingit_name="${mingit_meta[0]}"
mingit_url="${mingit_meta[1]}"
MINGIT_ZIP_NAME="$mingit_name"
MINGIT_ZIP_PATH="$MAIN_TGZ_DIR/$mingit_name"
if [[ ! -f "$MINGIT_ZIP_PATH" ]]; then
say "Download $MINGIT_ZIP_NAME"
curl -fsSL "$mingit_url" -o "$MINGIT_ZIP_PATH"
fi
short_head="$(git rev-parse --short HEAD)"
pkg="$(
npm pack --ignore-scripts --json --pack-destination "$MAIN_TGZ_DIR" \
@@ -838,7 +884,11 @@ verify_target_version() {
start_server() {
local host_ip="$1"
local artifact probe_url attempt
artifact="$(basename "$MAIN_TGZ_PATH")"
if [[ -n "$MAIN_TGZ_PATH" ]]; then
artifact="$(basename "$MAIN_TGZ_PATH")"
else
artifact="$MINGIT_ZIP_NAME"
fi
attempt=0
while :; do
attempt=$((attempt + 1))
@@ -864,22 +914,372 @@ start_server() {
done
}
write_latest_install_runner_script() {
local install_url_q="$1"
local version_flag_q="$2"
WINDOWS_LATEST_INSTALL_SCRIPT_PATH="$MAIN_TGZ_DIR/openclaw-install-latest.ps1"
cat >"$WINDOWS_LATEST_INSTALL_SCRIPT_PATH" <<EOF
param(
[Parameter(Mandatory = \$true)][string]\$LogPath,
[Parameter(Mandatory = \$true)][string]\$DonePath
)
\$ErrorActionPreference = 'Stop'
\$PSNativeCommandUseErrorActionPreference = \$false
function Write-ProgressLog {
param([Parameter(Mandatory = \$true)][string]\$Stage)
"==> \$Stage" | Tee-Object -FilePath \$LogPath -Append | Out-Null
}
try {
\$script = Invoke-RestMethod -Uri '$install_url_q'
Write-ProgressLog 'install.start'
& ([scriptblock]::Create(\$script)) ${version_flag_q}-NoOnboard *>&1 | Tee-Object -FilePath \$LogPath -Append | Out-Null
if (\$LASTEXITCODE -ne 0) {
throw "installer failed with exit code \$LASTEXITCODE"
}
Write-ProgressLog 'install.version'
& (Join-Path \$env:APPDATA 'npm\openclaw.cmd') --version *>&1 | Tee-Object -FilePath \$LogPath -Append | Out-Null
if (\$LASTEXITCODE -ne 0) {
throw "openclaw --version failed with exit code \$LASTEXITCODE"
}
Set-Content -Path \$DonePath -Value ([string]0)
exit 0
} catch {
if (Test-Path \$LogPath) {
Add-Content -Path \$LogPath -Value (\$_ | Out-String)
} else {
(\$_ | Out-String) | Set-Content -Path \$LogPath
}
Set-Content -Path \$DonePath -Value '1'
exit 1
}
EOF
}
write_baseline_npm_install_runner_script() {
WINDOWS_BASELINE_INSTALL_SCRIPT_PATH="$MAIN_TGZ_DIR/openclaw-install-baseline-npm.ps1"
cat >"$WINDOWS_BASELINE_INSTALL_SCRIPT_PATH" <<'EOF'
param(
[Parameter(Mandatory = $true)][string]$Version,
[Parameter(Mandatory = $true)][string]$LogPath,
[Parameter(Mandatory = $true)][string]$DonePath
)
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $false
function Write-ProgressLog {
param([Parameter(Mandatory = $true)][string]$Stage)
"==> $Stage" | Tee-Object -FilePath $LogPath -Append | Out-Null
}
function Invoke-Logged {
param(
[Parameter(Mandatory = $true)][string]$Label,
[Parameter(Mandatory = $true)][scriptblock]$Command
)
$output = $null
$previousErrorActionPreference = $ErrorActionPreference
$previousNativeErrorPreference = $PSNativeCommandUseErrorActionPreference
try {
$ErrorActionPreference = 'Continue'
$PSNativeCommandUseErrorActionPreference = $false
$output = & $Command *>&1
$exitCode = $LASTEXITCODE
} finally {
$ErrorActionPreference = $previousErrorActionPreference
$PSNativeCommandUseErrorActionPreference = $previousNativeErrorPreference
}
if ($null -ne $output) {
$output | Tee-Object -FilePath $LogPath -Append | Out-Null
}
if ($exitCode -ne 0) {
throw "$Label failed with exit code $exitCode"
}
}
try {
$portableGit = Join-Path (Join-Path (Join-Path $env:LOCALAPPDATA 'OpenClaw\deps') 'portable-git') ''
$env:PATH = "$portableGit\cmd;$portableGit\mingw64\bin;$portableGit\usr\bin;$env:PATH"
$openclaw = Join-Path $env:APPDATA 'npm\openclaw.cmd'
Write-ProgressLog 'install.start'
Invoke-Logged 'npm install baseline release' {
& npm.cmd install -g "openclaw@$Version" --no-fund --no-audit --loglevel=error
}
Write-ProgressLog 'install.version'
Invoke-Logged 'openclaw --version' { & $openclaw --version }
Set-Content -Path $DonePath -Value ([string]0)
exit 0
} catch {
if (Test-Path $LogPath) {
Add-Content -Path $LogPath -Value ($_ | Out-String)
} else {
($_ | Out-String) | Set-Content -Path $LogPath
}
Set-Content -Path $DonePath -Value '1'
exit 1
}
EOF
}
install_baseline_npm_release() {
local host_ip="$1"
local version="$2"
local script_url
local runner_name log_name done_name done_status launcher_state guest_log
local log_state_path
local start_seconds poll_deadline startup_checked poll_rc state_rc log_rc
write_baseline_npm_install_runner_script
script_url="http://$host_ip:$HOST_PORT/$(basename "$WINDOWS_BASELINE_INSTALL_SCRIPT_PATH")"
runner_name="openclaw-install-baseline-$RANDOM-$RANDOM.ps1"
log_name="openclaw-install-baseline-$RANDOM-$RANDOM.log"
done_name="openclaw-install-baseline-$RANDOM-$RANDOM.done"
log_state_path="$(mktemp "${TMPDIR:-/tmp}/openclaw-install-baseline-log-state.XXXXXX")"
: >"$log_state_path"
start_seconds="$SECONDS"
poll_deadline=$((SECONDS + TIMEOUT_INSTALL_S + 60))
startup_checked=0
guest_powershell_poll 20 "$(cat <<EOF
\$runner = Join-Path \$env:TEMP '$runner_name'
\$log = Join-Path \$env:TEMP '$log_name'
\$done = Join-Path \$env:TEMP '$done_name'
Remove-Item \$runner, \$log, \$done -Force -ErrorAction SilentlyContinue
curl.exe -fsSL '$script_url' -o \$runner
Start-Process powershell.exe -ArgumentList @(
'-NoProfile',
'-ExecutionPolicy', 'Bypass',
'-File', \$runner,
'-Version', '$version',
'-LogPath', \$log,
'-DonePath', \$done
) -WindowStyle Hidden | Out-Null
EOF
)"
stream_windows_baseline_install_log() {
set +e
guest_log="$(
guest_powershell_poll 20 "\$log = Join-Path \$env:TEMP '$log_name'; if (Test-Path \$log) { Get-Content \$log }"
)"
log_rc=$?
set -e
if [[ $log_rc -ne 0 ]] || [[ -z "$guest_log" ]]; then
return "$log_rc"
fi
GUEST_LOG="$guest_log" python3 - "$log_state_path" <<'PY'
import os
import pathlib
import sys
state_path = pathlib.Path(sys.argv[1])
previous = state_path.read_text(encoding="utf-8", errors="replace")
current = os.environ["GUEST_LOG"].replace("\r\n", "\n").replace("\r", "\n")
if current.startswith(previous):
sys.stdout.write(current[len(previous):])
else:
sys.stdout.write(current)
state_path.write_text(current, encoding="utf-8")
PY
}
while :; do
set +e
done_status="$(
guest_powershell_poll 20 "\$done = Join-Path \$env:TEMP '$done_name'; if (Test-Path \$done) { (Get-Content \$done -Raw).Trim() }"
)"
poll_rc=$?
set -e
done_status="${done_status//$'\r'/}"
if [[ $poll_rc -ne 0 ]]; then
warn "windows baseline install helper poll failed; retrying"
if (( SECONDS >= poll_deadline )); then
warn "windows baseline install helper timed out while polling done file"
rm -f "$log_state_path"
return 1
fi
sleep 2
continue
fi
set +e
stream_windows_baseline_install_log
log_rc=$?
set -e
if [[ $log_rc -ne 0 ]]; then
warn "windows baseline install helper live log poll failed; retrying"
fi
if [[ -n "$done_status" ]]; then
if ! stream_windows_baseline_install_log; then
warn "windows baseline install helper log drain failed after completion"
fi
rm -f "$log_state_path"
[[ "$done_status" == "0" ]]
return $?
fi
if [[ "$startup_checked" -eq 0 && $((SECONDS - start_seconds)) -ge 20 ]]; then
set +e
launcher_state="$(
guest_powershell_poll 20 "\$runner = Join-Path \$env:TEMP '$runner_name'; \$log = Join-Path \$env:TEMP '$log_name'; \$done = Join-Path \$env:TEMP '$done_name'; 'runner=' + (Test-Path \$runner) + ' log=' + (Test-Path \$log) + ' done=' + (Test-Path \$done)"
)"
state_rc=$?
set -e
launcher_state="${launcher_state//$'\r'/}"
startup_checked=1
if [[ $state_rc -eq 0 && "$launcher_state" == *"runner=False"* && "$launcher_state" == *"log=False"* && "$launcher_state" == *"done=False"* ]]; then
warn "windows baseline install helper failed to materialize guest files"
rm -f "$log_state_path"
return 1
fi
fi
if (( SECONDS >= poll_deadline )); then
if ! stream_windows_baseline_install_log; then
warn "windows baseline install helper log drain failed after timeout"
fi
warn "windows baseline install helper timed out waiting for done file"
rm -f "$log_state_path"
return 1
fi
sleep 2
done
}
install_latest_release() {
local install_url_q version_flag_q
local script_url
local runner_name log_name done_name done_status launcher_state guest_log
local log_state_path
local start_seconds poll_deadline startup_checked poll_rc state_rc log_rc
install_url_q="$(ps_single_quote "$INSTALL_URL")"
version_flag_q=""
if [[ -n "$INSTALL_VERSION" ]]; then
version_flag_q="-Tag '$(ps_single_quote "$INSTALL_VERSION")' "
fi
local install_script
install_script="$(cat <<EOF
\$ProgressPreference = 'SilentlyContinue'
\$script = Invoke-RestMethod -Uri '$install_url_q'
& ([scriptblock]::Create(\$script)) ${version_flag_q}-NoOnboard
& (Join-Path \$env:APPDATA 'npm\openclaw.cmd') --version
write_latest_install_runner_script "$install_url_q" "$version_flag_q"
script_url="http://$HOST_IP:$HOST_PORT/$(basename "$WINDOWS_LATEST_INSTALL_SCRIPT_PATH")"
runner_name="openclaw-install-latest-$RANDOM-$RANDOM.ps1"
log_name="openclaw-install-latest-$RANDOM-$RANDOM.log"
done_name="openclaw-install-latest-$RANDOM-$RANDOM.done"
log_state_path="$(mktemp "${TMPDIR:-/tmp}/openclaw-install-latest-log-state.XXXXXX")"
: >"$log_state_path"
start_seconds="$SECONDS"
poll_deadline=$((SECONDS + TIMEOUT_INSTALL_S + 60))
startup_checked=0
guest_powershell_poll 20 "$(cat <<EOF
\$runner = Join-Path \$env:TEMP '$runner_name'
\$log = Join-Path \$env:TEMP '$log_name'
\$done = Join-Path \$env:TEMP '$done_name'
Remove-Item \$runner, \$log, \$done -Force -ErrorAction SilentlyContinue
curl.exe -fsSL '$script_url' -o \$runner
Start-Process powershell.exe -ArgumentList @(
'-NoProfile',
'-ExecutionPolicy', 'Bypass',
'-File', \$runner,
'-LogPath', \$log,
'-DonePath', \$done
) -WindowStyle Hidden | Out-Null
EOF
)"
run_windows_retry "latest release installer" 2 guest_powershell "$install_script"
stream_windows_latest_install_log() {
set +e
guest_log="$(
guest_powershell_poll 20 "\$log = Join-Path \$env:TEMP '$log_name'; if (Test-Path \$log) { Get-Content \$log }"
)"
log_rc=$?
set -e
if [[ $log_rc -ne 0 ]] || [[ -z "$guest_log" ]]; then
return "$log_rc"
fi
GUEST_LOG="$guest_log" python3 - "$log_state_path" <<'PY'
import os
import pathlib
import sys
state_path = pathlib.Path(sys.argv[1])
previous = state_path.read_text(encoding="utf-8", errors="replace")
current = os.environ["GUEST_LOG"].replace("\r\n", "\n").replace("\r", "\n")
if current.startswith(previous):
sys.stdout.write(current[len(previous):])
else:
sys.stdout.write(current)
state_path.write_text(current, encoding="utf-8")
PY
}
while :; do
set +e
done_status="$(
guest_powershell_poll 20 "\$done = Join-Path \$env:TEMP '$done_name'; if (Test-Path \$done) { (Get-Content \$done -Raw).Trim() }"
)"
poll_rc=$?
set -e
done_status="${done_status//$'\r'/}"
if [[ $poll_rc -ne 0 ]]; then
warn "windows latest install helper poll failed; retrying"
if (( SECONDS >= poll_deadline )); then
warn "windows latest install helper timed out while polling done file"
rm -f "$log_state_path"
return 1
fi
sleep 2
continue
fi
set +e
stream_windows_latest_install_log
log_rc=$?
set -e
if [[ $log_rc -ne 0 ]]; then
warn "windows latest install helper live log poll failed; retrying"
fi
if [[ -n "$done_status" ]]; then
if ! stream_windows_latest_install_log; then
warn "windows latest install helper log drain failed after completion"
fi
rm -f "$log_state_path"
[[ "$done_status" == "0" ]]
return $?
fi
if [[ "$startup_checked" -eq 0 && $((SECONDS - start_seconds)) -ge 20 ]]; then
set +e
launcher_state="$(
guest_powershell_poll 20 "\$runner = Join-Path \$env:TEMP '$runner_name'; \$log = Join-Path \$env:TEMP '$log_name'; \$done = Join-Path \$env:TEMP '$done_name'; 'runner=' + (Test-Path \$runner) + ' log=' + (Test-Path \$log) + ' done=' + (Test-Path \$done)"
)"
state_rc=$?
set -e
launcher_state="${launcher_state//$'\r'/}"
startup_checked=1
if [[ $state_rc -eq 0 && "$launcher_state" == *"runner=False"* && "$launcher_state" == *"log=False"* && "$launcher_state" == *"done=False"* ]]; then
warn "windows latest install helper failed to materialize guest files"
rm -f "$log_state_path"
return 1
fi
fi
if (( SECONDS >= poll_deadline )); then
if ! stream_windows_latest_install_log; then
warn "windows latest install helper log drain failed after timeout"
fi
warn "windows latest install helper timed out waiting for done file"
rm -f "$log_state_path"
return 1
fi
sleep 2
done
}
install_main_tgz() {
@@ -901,7 +1301,7 @@ install_main_tgz() {
poll_deadline=$((SECONDS + TIMEOUT_INSTALL_S + 60))
startup_checked=0
guest_powershell "$(cat <<EOF
guest_powershell_poll 20 "$(cat <<EOF
\$runner = Join-Path \$env:TEMP '$runner_name'
\$log = Join-Path \$env:TEMP '$log_name'
\$done = Join-Path \$env:TEMP '$done_name'
@@ -1007,6 +1407,246 @@ PY
done
}
write_dev_update_runner_script() {
WINDOWS_DEV_UPDATE_SCRIPT_PATH="$MAIN_TGZ_DIR/openclaw-update-dev.ps1"
cat >"$WINDOWS_DEV_UPDATE_SCRIPT_PATH" <<'EOF'
param(
[Parameter(Mandatory = $true)][string]$LogPath,
[Parameter(Mandatory = $true)][string]$DonePath
)
$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $false
function Write-ProgressLog {
param([Parameter(Mandatory = $true)][string]$Stage)
"==> $Stage" | Tee-Object -FilePath $LogPath -Append | Out-Null
}
function Write-LoggedLine {
param([Parameter(Mandatory = $true)][string]$Line)
$Line | Tee-Object -FilePath $LogPath -Append | Out-Null
}
function Invoke-Logged {
param(
[Parameter(Mandatory = $true)][string]$Label,
[Parameter(Mandatory = $true)][scriptblock]$Command
)
$output = $null
$previousErrorActionPreference = $ErrorActionPreference
$previousNativeErrorPreference = $PSNativeCommandUseErrorActionPreference
try {
$ErrorActionPreference = 'Continue'
$PSNativeCommandUseErrorActionPreference = $false
$output = & $Command *>&1
$exitCode = $LASTEXITCODE
} finally {
$ErrorActionPreference = $previousErrorActionPreference
$PSNativeCommandUseErrorActionPreference = $previousNativeErrorPreference
}
if ($null -ne $output) {
$output | Tee-Object -FilePath $LogPath -Append | Out-Null
}
if ($exitCode -ne 0) {
throw "$Label failed with exit code $exitCode"
}
}
try {
$portableGit = Join-Path (Join-Path (Join-Path $env:LOCALAPPDATA 'OpenClaw\deps') 'portable-git') ''
$env:PATH = "$portableGit\cmd;$portableGit\mingw64\bin;$portableGit\usr\bin;$env:PATH"
$openclaw = Join-Path $env:APPDATA 'npm\openclaw.cmd'
$gitRoot = Join-Path $env:USERPROFILE 'openclaw'
$gitEntry = Join-Path $gitRoot 'openclaw.mjs'
Remove-Item $LogPath, $DonePath -Force -ErrorAction SilentlyContinue
Write-ProgressLog 'update.start'
Write-ProgressLog 'update.where-pnpm-pre'
$pnpmPre = Get-Command pnpm -ErrorAction SilentlyContinue
if ($null -ne $pnpmPre) {
Write-LoggedLine $pnpmPre.Source
} else {
Write-LoggedLine 'pnpm=missing-pre'
}
Write-ProgressLog 'update.where-corepack-pre'
$corepackPre = Get-Command corepack -ErrorAction SilentlyContinue
if ($null -ne $corepackPre) {
Write-LoggedLine $corepackPre.Source
Invoke-Logged 'corepack --version' { & corepack --version }
} else {
Write-LoggedLine 'corepack=missing-pre'
}
Write-ProgressLog 'update.reset-git-root'
if (Test-Path $gitRoot) {
Remove-Item $gitRoot -Recurse -Force
}
Write-ProgressLog 'update.run-dev'
Invoke-Logged 'openclaw update --channel dev --yes --json' {
& $openclaw update --channel dev --yes --json
}
if (-not (Test-Path $gitEntry)) {
throw "git entry missing after dev update: $gitEntry"
}
Write-ProgressLog 'update.where-pnpm-post'
$pnpmPost = Get-Command pnpm -ErrorAction SilentlyContinue
if ($null -eq $pnpmPost) {
throw 'pnpm missing after dev update'
}
Write-LoggedLine $pnpmPost.Source
Write-ProgressLog 'update.verify-post'
Invoke-Logged 'git openclaw --version' { & node.exe $gitEntry --version }
Invoke-Logged 'git openclaw update status --json' { & node.exe $gitEntry update status --json }
Write-ProgressLog 'update.done'
Set-Content -Path $DonePath -Value ([string]0)
exit 0
} catch {
if (Test-Path $LogPath) {
Add-Content -Path $LogPath -Value ($_ | Out-String)
} else {
($_ | Out-String) | Set-Content -Path $LogPath
}
Set-Content -Path $DonePath -Value '1'
exit 1
}
EOF
}
run_dev_channel_update() {
local host_ip="$1"
local script_url
local runner_name log_name done_name done_status launcher_state guest_log
local log_state_path
local start_seconds poll_deadline startup_checked poll_rc state_rc log_rc
write_dev_update_runner_script
script_url="http://$host_ip:$HOST_PORT/$(basename "$WINDOWS_DEV_UPDATE_SCRIPT_PATH")"
runner_name="openclaw-update-dev-$RANDOM-$RANDOM.ps1"
log_name="openclaw-update-dev-$RANDOM-$RANDOM.log"
done_name="openclaw-update-dev-$RANDOM-$RANDOM.done"
log_state_path="$(mktemp "${TMPDIR:-/tmp}/openclaw-update-dev-log-state.XXXXXX")"
: >"$log_state_path"
start_seconds="$SECONDS"
poll_deadline=$((SECONDS + TIMEOUT_INSTALL_S + 60))
startup_checked=0
guest_powershell "$(cat <<EOF
\$runner = Join-Path \$env:TEMP '$runner_name'
\$log = Join-Path \$env:TEMP '$log_name'
\$done = Join-Path \$env:TEMP '$done_name'
Remove-Item \$runner, \$log, \$done -Force -ErrorAction SilentlyContinue
curl.exe -fsSL '$script_url' -o \$runner
Start-Process powershell.exe -ArgumentList @(
'-NoProfile',
'-ExecutionPolicy', 'Bypass',
'-File', \$runner,
'-LogPath', \$log,
'-DonePath', \$done
) -WindowStyle Hidden | Out-Null
EOF
)"
stream_windows_dev_update_log() {
set +e
guest_log="$(
guest_powershell_poll 20 "\$log = Join-Path \$env:TEMP '$log_name'; if (Test-Path \$log) { Get-Content \$log }"
)"
log_rc=$?
set -e
if [[ $log_rc -ne 0 ]] || [[ -z "$guest_log" ]]; then
return "$log_rc"
fi
GUEST_LOG="$guest_log" python3 - "$log_state_path" <<'PY'
import os
import pathlib
import sys
state_path = pathlib.Path(sys.argv[1])
previous = state_path.read_text(encoding="utf-8", errors="replace")
current = os.environ["GUEST_LOG"].replace("\r\n", "\n").replace("\r", "\n")
if current.startswith(previous):
sys.stdout.write(current[len(previous):])
else:
sys.stdout.write(current)
state_path.write_text(current, encoding="utf-8")
PY
}
while :; do
set +e
done_status="$(
guest_powershell_poll 20 "\$done = Join-Path \$env:TEMP '$done_name'; if (Test-Path \$done) { (Get-Content \$done -Raw).Trim() }"
)"
poll_rc=$?
set -e
done_status="${done_status//$'\r'/}"
if [[ $poll_rc -ne 0 ]]; then
warn "windows dev update helper poll failed; retrying"
if (( SECONDS >= poll_deadline )); then
warn "windows dev update helper timed out while polling done file"
rm -f "$log_state_path"
return 1
fi
sleep 2
continue
fi
set +e
stream_windows_dev_update_log
log_rc=$?
set -e
if [[ $log_rc -ne 0 ]]; then
warn "windows dev update helper live log poll failed; retrying"
fi
if [[ -n "$done_status" ]]; then
if ! stream_windows_dev_update_log; then
warn "windows dev update helper log drain failed after completion"
fi
rm -f "$log_state_path"
[[ "$done_status" == "0" ]]
return $?
fi
if [[ "$startup_checked" -eq 0 && $((SECONDS - start_seconds)) -ge 20 ]]; then
set +e
launcher_state="$(
guest_powershell_poll 20 "\$runner = Join-Path \$env:TEMP '$runner_name'; \$log = Join-Path \$env:TEMP '$log_name'; \$done = Join-Path \$env:TEMP '$done_name'; 'runner=' + (Test-Path \$runner) + ' log=' + (Test-Path \$log) + ' done=' + (Test-Path \$done)"
)"
state_rc=$?
set -e
launcher_state="${launcher_state//$'\r'/}"
startup_checked=1
if [[ $state_rc -eq 0 && "$launcher_state" == *"runner=False"* && "$launcher_state" == *"log=False"* && "$launcher_state" == *"done=False"* ]]; then
warn "windows dev update helper failed to materialize guest files"
rm -f "$log_state_path"
return 1
fi
fi
if (( SECONDS >= poll_deadline )); then
if ! stream_windows_dev_update_log; then
warn "windows dev update helper log drain failed after timeout"
fi
warn "windows dev update helper timed out waiting for done file"
rm -f "$log_state_path"
return 1
fi
sleep 2
done
}
write_install_runner_script() {
WINDOWS_INSTALL_SCRIPT_PATH="$MAIN_TGZ_DIR/openclaw-install-main.ps1"
cat >"$WINDOWS_INSTALL_SCRIPT_PATH" <<'EOF'
@@ -1209,6 +1849,40 @@ verify_gateway() {
guest_run_openclaw "" "" gateway status --deep --require-rpc
}
verify_dev_channel_update() {
local status_json pnpm_output
status_json="$(
guest_powershell "$(cat <<'EOF'
$portableGit = Join-Path (Join-Path (Join-Path $env:LOCALAPPDATA 'OpenClaw\deps') 'portable-git') ''
$env:PATH = "$portableGit\cmd;$portableGit\mingw64\bin;$portableGit\usr\bin;$env:PATH"
$gitEntry = Join-Path (Join-Path $env:USERPROFILE 'openclaw') 'openclaw.mjs'
if (-not (Test-Path $gitEntry)) {
throw "git entry missing: $gitEntry"
}
& node.exe $gitEntry update status --json
EOF
)"
)"
pnpm_output="$(
guest_powershell "$(cat <<'EOF'
$portableGit = Join-Path (Join-Path (Join-Path $env:LOCALAPPDATA 'OpenClaw\deps') 'portable-git') ''
$env:PATH = "$portableGit\cmd;$portableGit\mingw64\bin;$portableGit\usr\bin;$env:PATH"
$pnpmCommand = Get-Command pnpm -ErrorAction SilentlyContinue
if ($null -eq $pnpmCommand) {
throw 'pnpm missing after dev update'
}
$pnpmCommand.Source
EOF
)"
)"
printf '%s\n' "$status_json"
printf '%s\n' "$status_json" | grep -F '"installKind": "git"'
printf '%s\n' "$status_json" | grep -F '"value": "dev"'
printf '%s\n' "$status_json" | grep -F '"branch": "main"'
printf '%s\n' "$pnpm_output"
printf '%s\n' "$pnpm_output" | grep -Fi 'pnpm'
}
run_gateway_daemon_action() {
local action="$1"
local runner_name log_name done_name done_status launcher_state
@@ -1347,7 +2021,10 @@ run_fresh_main_lane() {
local host_ip="$2"
phase_run "fresh.restore-snapshot" "$TIMEOUT_SNAPSHOT_S" restore_snapshot "$snapshot_id" || return $?
phase_run "fresh.wait-for-user" "$TIMEOUT_SNAPSHOT_S" wait_for_guest_ready || return $?
phase_run "fresh.ensure-git" "$TIMEOUT_INSTALL_S" ensure_guest_git "$host_ip" || return $?
if ! phase_run "fresh.ensure-git" "$TIMEOUT_INSTALL_S" ensure_guest_git "$host_ip"; then
phase_run "fresh.wait-for-user-retry" "$TIMEOUT_SNAPSHOT_S" wait_for_guest_ready || return $?
phase_run "fresh.ensure-git-retry" "$TIMEOUT_INSTALL_S" ensure_guest_git "$host_ip" || return $?
fi
phase_run "fresh.install-main" "$TIMEOUT_INSTALL_S" install_main_tgz "$host_ip" "openclaw-main-fresh.tgz" || return $?
FRESH_MAIN_VERSION="$(extract_last_version "$(phase_log_path fresh.install-main)")"
phase_run "fresh.verify-main-version" "$TIMEOUT_VERIFY_S" verify_target_version || return $?
@@ -1361,11 +2038,23 @@ run_fresh_main_lane() {
run_upgrade_lane() {
local snapshot_id="$1"
local host_ip="$2"
local baseline_version
baseline_version="$(baseline_install_version)"
phase_run "upgrade.restore-snapshot" "$TIMEOUT_SNAPSHOT_S" restore_snapshot "$snapshot_id" || return $?
phase_run "upgrade.wait-for-user" "$TIMEOUT_SNAPSHOT_S" wait_for_guest_ready || return $?
phase_run "upgrade.install-latest" "$TIMEOUT_INSTALL_S" install_latest_release || return $?
LATEST_INSTALLED_VERSION="$(extract_last_version "$(phase_log_path upgrade.install-latest)")"
phase_run "upgrade.verify-latest-version" "$TIMEOUT_VERIFY_S" verify_version_contains "$LATEST_VERSION" || return $?
if ! phase_run "upgrade.ensure-git" "$TIMEOUT_INSTALL_S" ensure_guest_git "$host_ip"; then
phase_run "upgrade.wait-for-user-retry" "$TIMEOUT_SNAPSHOT_S" wait_for_guest_ready || return $?
phase_run "upgrade.ensure-git-retry" "$TIMEOUT_INSTALL_S" ensure_guest_git "$host_ip" || return $?
fi
if upgrade_uses_host_tgz; then
phase_run "upgrade.install-baseline-package" "$TIMEOUT_INSTALL_S" install_main_tgz "$host_ip" "openclaw-main-upgrade.tgz" || return $?
LATEST_INSTALLED_VERSION="$(extract_last_version "$(phase_log_path upgrade.install-baseline-package)")"
phase_run "upgrade.verify-baseline-package-version" "$TIMEOUT_VERIFY_S" verify_target_version || return $?
else
phase_run "upgrade.install-baseline" "$TIMEOUT_INSTALL_S" install_baseline_npm_release "$host_ip" "$baseline_version" || return $?
LATEST_INSTALLED_VERSION="$(extract_last_version "$(phase_log_path upgrade.install-baseline)")"
phase_run "upgrade.verify-baseline-version" "$TIMEOUT_VERIFY_S" verify_version_contains "$baseline_version" || return $?
fi
if [[ "$CHECK_LATEST_REF" -eq 1 ]]; then
if phase_run "upgrade.latest-ref-precheck" "$TIMEOUT_ONBOARD_PHASE_S" capture_latest_ref_failure; then
UPGRADE_PRECHECK_STATUS="latest-ref-pass"
@@ -1375,10 +2064,9 @@ run_upgrade_lane() {
else
UPGRADE_PRECHECK_STATUS="skipped"
fi
phase_run "upgrade.ensure-git" "$TIMEOUT_INSTALL_S" ensure_guest_git "$host_ip" || return $?
phase_run "upgrade.install-main" "$TIMEOUT_INSTALL_S" install_main_tgz "$host_ip" "openclaw-main-upgrade.tgz" || return $?
UPGRADE_MAIN_VERSION="$(extract_last_version "$(phase_log_path upgrade.install-main)")"
phase_run "upgrade.verify-main-version" "$TIMEOUT_VERIFY_S" verify_target_version || return $?
phase_run "upgrade.update-dev" "$TIMEOUT_INSTALL_S" run_dev_channel_update "$host_ip" || return $?
UPGRADE_MAIN_VERSION="$(extract_last_version "$(phase_log_path upgrade.update-dev)")"
phase_run "upgrade.verify-dev-channel" "$TIMEOUT_VERIFY_S" verify_dev_channel_update || return $?
# Stop the old managed gateway before ref-mode onboard rewrites config and
# gateway auth. Restarting first can leave the old token alive and make the
# onboard health probe fail against a stale daemon.
@@ -1404,7 +2092,11 @@ say "Latest npm version: $LATEST_VERSION"
say "Current head: $(git rev-parse --short HEAD)"
say "Run logs: $RUN_DIR"
pack_main_tgz
if needs_host_tgz; then
pack_main_tgz
else
ensure_mingit_zip
fi
start_server "$HOST_IP"
if [[ "$MODE" == "fresh" || "$MODE" == "both" ]]; then
@@ -1467,12 +2159,15 @@ else
if [[ -n "$TARGET_PACKAGE_SPEC" ]]; then
printf ' target-package: %s\n' "$TARGET_PACKAGE_SPEC"
fi
if [[ "$UPGRADE_FROM_PACKED_MAIN" -eq 1 ]]; then
printf ' upgrade-from-packed-main: yes\n'
fi
if [[ -n "$INSTALL_VERSION" ]]; then
printf ' baseline-install-version: %s\n' "$INSTALL_VERSION"
fi
printf ' fresh-main: %s (%s)\n' "$FRESH_MAIN_STATUS" "$FRESH_MAIN_VERSION"
printf ' latest->main precheck: %s (%s)\n' "$UPGRADE_PRECHECK_STATUS" "$LATEST_INSTALLED_VERSION"
printf ' latest->main: %s (%s)\n' "$UPGRADE_STATUS" "$UPGRADE_MAIN_VERSION"
printf ' %s precheck: %s (%s)\n' "$(upgrade_summary_label)" "$UPGRADE_PRECHECK_STATUS" "$LATEST_INSTALLED_VERSION"
printf ' %s: %s (%s)\n' "$(upgrade_summary_label)" "$UPGRADE_STATUS" "$UPGRADE_MAIN_VERSION"
printf ' logs: %s\n' "$RUN_DIR"
printf ' summary: %s\n' "$SUMMARY_JSON_PATH"
fi