fix(installer): fail failed Windows git builds

This commit is contained in:
Vincent Koc
2026-05-23 09:16:45 +08:00
parent e0405ecc9b
commit 2edd6e2462
3 changed files with 97 additions and 11 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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@(?<version>[^+]+)'");
@@ -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"),
);