From 2edd6e24621d73e0099660c5dbebcd699b2ba0d3 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 23 May 2026 09:16:45 +0800 Subject: [PATCH] fix(installer): fail failed Windows git builds --- CHANGELOG.md | 1 + scripts/install.ps1 | 30 +++++++++++-- test/scripts/install-ps1.test.ts | 77 +++++++++++++++++++++++++++++--- 3 files changed, 97 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff1d8f21509..39997da61fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Windows installer: fail Git checkout installs when `pnpm install` or `pnpm build` fails instead of writing a wrapper to a missing CLI build. - Agents: keep parallel OpenAI-compatible tool-call deltas in separate argument buffers so interleaved tool calls no longer corrupt streamed arguments. (#82263) Thanks @luna-system. - Memory/doctor: report missing or unusable QMD workspace directories as workspace failures instead of generic binary failures. (#63167) Thanks @sercada. - Debug proxy: record CONNECT client-socket errors and destroy the paired upstream socket so abrupt client disconnects no longer leak tunnel resources. (#82444) Thanks @SebTardif. diff --git a/scripts/install.ps1 b/scripts/install.ps1 index eff67d38fbe..0c50851c611 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -23,6 +23,12 @@ function Fail-Install { return $false } +function Test-BooleanSuccessResult { + param([object[]]$Results) + + return ($Results.Count -gt 0 -and $Results[-1] -eq $true) +} + function Complete-Install { param([bool]$Succeeded) @@ -1043,10 +1049,18 @@ function Install-OpenClawFromGit { Push-Location -LiteralPath $RepoDir $pushedRepoLocation = $true & $pnpmCommand install + if ($LASTEXITCODE -ne 0) { + Write-Host "[!] pnpm install failed for the Git checkout" -ForegroundColor Red + return $false + } if (-not (& $pnpmCommand ui:build)) { Write-Host "[!] UI build failed; continuing (CLI may still work)" -ForegroundColor Yellow } & $pnpmCommand build + if ($LASTEXITCODE -ne 0) { + Write-Host "[!] pnpm build failed for the Git checkout" -ForegroundColor Red + return $false + } } finally { if ($pushedRepoLocation) { Pop-Location @@ -1054,12 +1068,18 @@ function Install-OpenClawFromGit { $env:NPM_CONFIG_SCRIPT_SHELL = $prevPnpmScriptShell } + $entryPath = Join-Path $RepoDir "dist\\entry.js" + if (-not (Test-Path $entryPath)) { + Write-Host "[!] OpenClaw build did not produce $entryPath" -ForegroundColor Red + return $false + } + $binDir = Join-Path $env:USERPROFILE ".local\\bin" if (-not (Test-Path $binDir)) { New-Item -ItemType Directory -Force -Path $binDir | Out-Null } $cmdPath = Join-Path $binDir "openclaw.cmd" - $cmdContents = "@echo off`r`nnode ""$RepoDir\\dist\\entry.js"" %*`r`n" + $cmdContents = "@echo off`r`nnode ""$entryPath"" %*`r`n" Set-Content -Path $cmdPath -Value $cmdContents -NoNewline $userPath = [Environment]::GetEnvironmentVariable("Path", "User") @@ -1202,7 +1222,8 @@ function Main { } } catch { } $finalGitDir = $GitDir - if (-not (Install-OpenClawFromGit -RepoDir $GitDir -SkipUpdate:$NoGitUpdate)) { + $gitInstallResults = @(Install-OpenClawFromGit -RepoDir $GitDir -SkipUpdate:$NoGitUpdate) + if (-not (Test-BooleanSuccessResult -Results $gitInstallResults)) { return (Fail-Install) } } else { @@ -1211,7 +1232,8 @@ function Main { Remove-Item -Force $gitWrapper Write-Host "[OK] Removed git wrapper (switching to npm)" -ForegroundColor Green } - if (-not (Install-OpenClaw)) { + $npmInstallResults = @(Install-OpenClaw) + if (-not (Test-BooleanSuccessResult -Results $npmInstallResults)) { return (Fail-Install) } } @@ -1321,5 +1343,5 @@ function Main { } $mainResults = @(Main) -$installSucceeded = $mainResults.Count -gt 0 -and $mainResults[-1] -eq $true +$installSucceeded = Test-BooleanSuccessResult -Results $mainResults Complete-Install -Succeeded:$installSucceeded diff --git a/test/scripts/install-ps1.test.ts b/test/scripts/install-ps1.test.ts index a3c93a22701..d20829230f4 100644 --- a/test/scripts/install-ps1.test.ts +++ b/test/scripts/install-ps1.test.ts @@ -6,7 +6,12 @@ import { createScriptTestHarness } from "./test-helpers"; const SCRIPT_PATH = "scripts/install.ps1"; const ENTRYPOINT_RE = - /\r?\n\$mainResults = @\(Main\)\r?\n\$installSucceeded = \$mainResults\.Count -gt 0 -and \$mainResults\[-1\] -eq \$true\r?\nComplete-Install -Succeeded:\$installSucceeded\s*$/m; + /\r?\n\$mainResults = @\(Main\)\r?\n\$installSucceeded = Test-BooleanSuccessResult -Results \$mainResults\r?\nComplete-Install -Succeeded:\$installSucceeded\s*$/m; +const ENTRYPOINT_LINES = [ + "$mainResults = @(Main)", + "$installSucceeded = Test-BooleanSuccessResult -Results $mainResults", + "Complete-Install -Succeeded:$installSucceeded", +]; function extractFunctionBody(source: string, name: string): string { const match = source.match( @@ -50,9 +55,7 @@ function createFailingNodeFixture(source: string): string { "function Check-Node { return $false }", "function Install-Node { return $false }", "", - "$mainResults = @(Main)", - "$installSucceeded = $mainResults.Count -gt 0 -and $mainResults[-1] -eq $true", - "Complete-Install -Succeeded:$installSucceeded", + ...ENTRYPOINT_LINES, "", ].join("\n"); } @@ -77,9 +80,12 @@ describe("install.ps1 failure handling", () => { it("keeps failure termination in the top-level completion handler", () => { const completeInstallBody = extractFunctionBody(source, "Complete-Install"); + const booleanSuccessBody = extractFunctionBody(source, "Test-BooleanSuccessResult"); expect(completeInstallBody).toMatch(/\$PSCommandPath/); expect(completeInstallBody).toMatch(/\bexit \$script:InstallExitCode\b/); expect(completeInstallBody).toMatch(/\bthrow "OpenClaw installation failed with exit code/); + expect(booleanSuccessBody).toContain("$Results.Count -gt 0"); + expect(source).toContain("$installSucceeded = Test-BooleanSuccessResult -Results $mainResults"); }); it("runs npm install through the resolved command with quiet CI defaults", () => { @@ -227,6 +233,7 @@ describe("install.ps1 failure handling", () => { const pnpmVersionMatchBody = extractFunctionBody(source, "Test-PnpmCommandMatchesVersion"); const ensurePnpmBody = extractFunctionBody(source, "Ensure-Pnpm"); const gitInstallBody = extractFunctionBody(source, "Install-OpenClawFromGit"); + const mainBody = extractFunctionBody(source, "Main"); expect(pnpmVersionBody).toContain("package.json"); expect(pnpmVersionBody).toContain("$packageJson.packageManager -match '^pnpm@(?[^+]+)'"); @@ -247,9 +254,31 @@ describe("install.ps1 failure handling", () => { expect(gitInstallBody.indexOf("git -C $RepoDir pull --rebase")).toBeLessThan( gitInstallBody.indexOf("Ensure-Pnpm -RepoDir $RepoDir"), ); + expect(mainBody).toContain("$gitInstallResults = @(Install-OpenClawFromGit"); + expect(mainBody).toContain( + "Test-BooleanSuccessResult -Results $gitInstallResults", + ); + expect(mainBody).toContain("$npmInstallResults = @(Install-OpenClaw)"); + expect(mainBody).toContain( + "Test-BooleanSuccessResult -Results $npmInstallResults", + ); expect(gitInstallBody).toContain("Push-Location -LiteralPath $RepoDir"); expect(gitInstallBody).toContain("& $pnpmCommand install"); + expect(gitInstallBody).toContain( + 'Write-Host "[!] pnpm install failed for the Git checkout"', + ); + expect(gitInstallBody).toContain("& $pnpmCommand build"); + expect(gitInstallBody).toContain( + 'Write-Host "[!] pnpm build failed for the Git checkout"', + ); + expect(gitInstallBody).toContain('$entryPath = Join-Path $RepoDir "dist\\\\entry.js"'); + expect(gitInstallBody).toContain("Test-Path $entryPath"); + expect(gitInstallBody).toContain( + 'Write-Host "[!] OpenClaw build did not produce $entryPath"', + ); + expect(gitInstallBody).toContain('node ""$entryPath"" %*'); expect(gitInstallBody).not.toContain("& $pnpmCommand -C $RepoDir install"); + expect(gitInstallBody).not.toContain('node ""$RepoDir\\\\dist\\\\entry.js"" %*'); }); it("cleans legacy git submodules only from the selected git checkout", () => { @@ -309,6 +338,42 @@ describe("install.ps1 failure handling", () => { expect(result.stdout).toContain("alive-after-install"); }); + runIfPowerShell("treats noisy Git install false as failure", () => { + const tempDir = harness.createTempDir("openclaw-install-ps1-"); + const scriptPath = join(tempDir, "install.ps1"); + const scriptWithoutEntryPoint = source.replace(ENTRYPOINT_RE, ""); + writeFileSync( + scriptPath, + [ + scriptWithoutEntryPoint, + "", + "function Write-Banner { }", + "function Ensure-ExecutionPolicy { return $true }", + "function Check-Node { return $true }", + "function Check-ExistingOpenClaw { return $false }", + "function Get-NpmCommandPath { return $null }", + "function Install-OpenClawFromGit {", + " Write-Output 'pnpm stdout before failure'", + " return $false", + "}", + "function Ensure-OpenClawOnPath { throw 'should not continue after failed git install' }", + "$InstallMethod = 'git'", + "$GitDir = 'C:\\\\openclaw-test'", + "$NoOnboard = $true", + "$result = Main", + 'if ($result -ne $false) { throw "Main returned $result" }', + 'if ($script:InstallExitCode -ne 1) { throw "InstallExitCode=$script:InstallExitCode" }', + "", + ].join("\n"), + ); + chmodSync(scriptPath, 0o755); + + const result = runPowerShell(["-NoLogo", "-NoProfile", "-Command", `. ${toPowerShellSingleQuotedLiteral(scriptPath)}`]); + + expect(result.status).toBe(0); + expect(result.stderr).toBe(""); + }); + runIfPowerShell("keeps npm chatter out of Main's success return value", () => { const tempDir = harness.createTempDir("openclaw-install-ps1-"); const scriptPath = join(tempDir, "install.ps1"); @@ -371,9 +436,7 @@ describe("install.ps1 failure handling", () => { "function Refresh-GatewayServiceIfLoaded { }", "function Invoke-OpenClawCommand { return 'OpenClaw test-version' }", "$NoOnboard = $true", - "$mainResults = @(Main)", - "$installSucceeded = $mainResults.Count -gt 0 -and $mainResults[-1] -eq $true", - "Complete-Install -Succeeded:$installSucceeded", + ...ENTRYPOINT_LINES, "", ].join("\n"), );