From 25f7e062e14449d117c41840b1726af2321bba3a Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Apr 2026 22:12:27 +0100 Subject: [PATCH] fix(ci): harden cross-os release harness --- .../run-openclaw-cross-os-release-checks.sh | 20 +++++++ scripts/openclaw-cross-os-release-checks.ts | 48 +++++++++++++++- .../openclaw-cross-os-release-checks.test.ts | 55 +++++++++++++++++++ 3 files changed, 121 insertions(+), 2 deletions(-) diff --git a/scripts/github/run-openclaw-cross-os-release-checks.sh b/scripts/github/run-openclaw-cross-os-release-checks.sh index cd63b4c27b6..2025b39091f 100755 --- a/scripts/github/run-openclaw-cross-os-release-checks.sh +++ b/scripts/github/run-openclaw-cross-os-release-checks.sh @@ -4,6 +4,17 @@ set -euo pipefail tsx_version="${OPENCLAW_RELEASE_TSX_VERSION:-${TSX_VERSION:-4.21.0}}" script_path="${OPENCLAW_RELEASE_CHECKS_SCRIPT:-workflow/scripts/openclaw-cross-os-release-checks.ts}" +if ! command -v node >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1; then + if command -v cygpath >/dev/null 2>&1; then + for node_dir in /c/hostedtoolcache/windows/node/*/x64 /c/actions-runner/_work/_tool/node/*/x64; do + if [[ -x "${node_dir}/node.exe" ]]; then + export PATH="${node_dir}:${PATH}" + break + fi + done + fi +fi + temp_root="${OPENCLAW_RELEASE_TSX_TOOL_ROOT:-${RUNNER_TEMP:-${TMPDIR:-/tmp}}}" if command -v cygpath >/dev/null 2>&1; then temp_root="$(cygpath -u "${temp_root}")" @@ -12,6 +23,15 @@ fi tool_dir="${OPENCLAW_RELEASE_TSX_TOOL_DIR:-${temp_root}/openclaw-release-tsx-${tsx_version}}" loader_path="${tool_dir}/node_modules/tsx/dist/loader.mjs" +command -v node >/dev/null 2>&1 || { + echo "node is required to run cross-OS release checks." >&2 + exit 127 +} +command -v npm >/dev/null 2>&1 || { + echo "npm is required to install the cross-OS release-check loader." >&2 + exit 127 +} + if [[ ! -f "${loader_path}" ]]; then mkdir -p "${tool_dir}" npm install --prefix "${tool_dir}" --no-save --no-package-lock "tsx@${tsx_version}" >/dev/null diff --git a/scripts/openclaw-cross-os-release-checks.ts b/scripts/openclaw-cross-os-release-checks.ts index b26d23e5c2b..d1dae426bb9 100644 --- a/scripts/openclaw-cross-os-release-checks.ts +++ b/scripts/openclaw-cross-os-release-checks.ts @@ -692,18 +692,22 @@ async function runUpgradeLane(params) { "--timeout", String(updateStepTimeoutSeconds()), ]; - await runOpenClaw({ + const updateResult = await runOpenClaw({ lane, env: updateEnv, args: updateArgs, logPath: join(params.logsDir, "upgrade-update.log"), timeoutMs: updateTimeoutMs(), + check: false, + }); + verifyPackagedUpgradeUpdateResult(updateResult, { + candidateVersion: params.build.candidateVersion, }); logLanePhase(lane, "update-status"); await runOpenClaw({ lane, - env, + env: updateEnv, args: ["update", "status", "--json"], logPath: join(params.logsDir, "upgrade-update-status.log"), timeoutMs: 2 * 60 * 1000, @@ -1228,6 +1232,46 @@ export function buildRealUpdateEnv(env) { return updateEnv; } +export function verifyPackagedUpgradeUpdateResult(result, options) { + if (result.exitCode === 0) { + return; + } + + let payload = null; + try { + payload = JSON.parse(result.stdout); + } catch { + payload = null; + } + + const steps = Array.isArray(payload?.steps) ? payload.steps : []; + const allStepsSucceeded = steps.every((step) => step?.exitCode === 0); + const afterVersion = typeof payload?.after?.version === "string" ? payload.after.version : ""; + if ( + payload?.status === "ok" && + afterVersion === options.candidateVersion && + allStepsSucceeded && + isSelfSwappedPackageProcessExit(result.stderr) + ) { + return; + } + + throw new Error( + `Packaged upgrade failed (${result.exitCode}): ${trimForSummary( + `${result.stdout}\n${result.stderr}`, + )}`, + ); +} + +function isSelfSwappedPackageProcessExit(stderr) { + return ( + typeof stderr === "string" && + stderr.includes("[openclaw] Failed to start CLI:") && + stderr.includes("ERR_MODULE_NOT_FOUND") && + /[\\/]node_modules[\\/]openclaw[\\/]dist[\\/]/u.test(stderr) + ); +} + export function resolveExplicitBaselineVersion(baselineSpec) { const trimmed = baselineSpec.trim(); if (!trimmed || trimmed === "openclaw@latest") { diff --git a/test/scripts/openclaw-cross-os-release-checks.test.ts b/test/scripts/openclaw-cross-os-release-checks.test.ts index 7e89dce1146..bdb474f6131 100644 --- a/test/scripts/openclaw-cross-os-release-checks.test.ts +++ b/test/scripts/openclaw-cross-os-release-checks.test.ts @@ -55,6 +55,7 @@ import { shouldUseManagedGatewayForInstallerRuntime, shouldUseManagedGatewayService, verifyDevUpdateStatus, + verifyPackagedUpgradeUpdateResult, writePackageDistInventoryForCandidate, } from "../../scripts/openclaw-cross-os-release-checks.ts"; @@ -508,6 +509,60 @@ describe("scripts/openclaw-cross-os-release-checks", () => { }); }); + it("accepts a successful packaged update followed by the old self-swapped process import miss", () => { + expect(() => + verifyPackagedUpgradeUpdateResult( + { + exitCode: 1, + stdout: JSON.stringify({ + status: "ok", + after: { version: "2026.4.27" }, + steps: [{ name: "global update", exitCode: 0 }], + }), + stderr: + "[openclaw] Failed to start CLI: Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/tmp/prefix/lib/node_modules/openclaw/dist/memory-state-old.js'", + }, + { candidateVersion: "2026.4.27" }, + ), + ).not.toThrow(); + }); + + it("rejects packaged update failures before the candidate package lands", () => { + expect(() => + verifyPackagedUpgradeUpdateResult( + { + exitCode: 1, + stdout: JSON.stringify({ + status: "ok", + after: { version: "2026.4.26" }, + steps: [{ name: "global update", exitCode: 0 }], + }), + stderr: + "[openclaw] Failed to start CLI: Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/tmp/prefix/lib/node_modules/openclaw/dist/memory-state-old.js'", + }, + { candidateVersion: "2026.4.27" }, + ), + ).toThrow(/Packaged upgrade failed/u); + }); + + it("rejects packaged update failures with unsuccessful update steps", () => { + expect(() => + verifyPackagedUpgradeUpdateResult( + { + exitCode: 1, + stdout: JSON.stringify({ + status: "ok", + after: { version: "2026.4.27" }, + steps: [{ name: "global update", exitCode: 1 }], + }), + stderr: + "[openclaw] Failed to start CLI: Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/tmp/prefix/lib/node_modules/openclaw/dist/memory-state-old.js'", + }, + { candidateVersion: "2026.4.27" }, + ), + ).toThrow(/Packaged upgrade failed/u); + }); + it("only treats pinned baseline specs as exact installer version assertions", () => { expect(resolveExplicitBaselineVersion("")).toBe(""); expect(resolveExplicitBaselineVersion("openclaw@latest")).toBe("");