From 063ae25aa50ff1a7a1db92beb7bf4bc46b516d02 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:17:08 +0000 Subject: [PATCH 1/4] ci(windows): stash multi-line env vars around Enter-VsDevShell Enter-VsDevShell (Microsoft.VisualStudio.DevShell.dll) captures the VS environment by running VsDevCmd.bat and parsing the output of cmd.exe's `set` line by line. `set` cannot represent a value that contains CR/LF, so every continuation line of a multi-line value that happens to look like KEY=VALUE is promoted to its own environment variable. On Buildkite the full commit message is exported as BUILDKITE_MESSAGE. A commit body line such as BUN_JSC_collectContinuously=1 stress over serve+ws+reload and over therefore became a real BUN_JSC_collectContinuously env var with value "1 stress over serve+ws+reload and over", which JSC rejects at startup with "invalid JSC environment variable", aborting every Windows test that spawns a bun subprocess (build 73847, PR #34346). Hide any env var whose value contains CR/LF from the VS dev-shell loader and restore it afterwards, so multi-line values reach the wrapped command intact and the loader only ever sees single-line input. --- scripts/vs-shell.ps1 | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/scripts/vs-shell.ps1 b/scripts/vs-shell.ps1 index ffbf5eecaa40..de9ef8745973 100755 --- a/scripts/vs-shell.ps1 +++ b/scripts/vs-shell.ps1 @@ -33,6 +33,17 @@ if($env:VSINSTALLDIR -eq $null) { } } + # Enter-VsDevShell parses cmd.exe `set` output line by line, so a multi-line + # value (e.g. BUILDKITE_MESSAGE) leaks each `KEY=VALUE`-shaped body line as a + # real env var. Stash multi-line vars around the loader to keep them intact. + $multilineEnv = @{} + foreach ($item in (Get-ChildItem env:)) { + if ($item.Value -match "[\r\n]") { + $multilineEnv[$item.Name] = $item.Value + Remove-Item -LiteralPath "env:$($item.Name)" + } + } + Push-Location $vsDir try { $vsShell = (Join-Path -Path $vsDir -ChildPath "Common7\Tools\Launch-VsDevShell.ps1") @@ -47,6 +58,9 @@ if($env:VSINSTALLDIR -eq $null) { } } finally { Pop-Location + foreach ($name in $multilineEnv.Keys) { + Set-Item -LiteralPath "env:$name" -Value $multilineEnv[$name] + } } } From 1bd81046f34e4ae04a2eace95b7964c7cef1d695 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:44:05 +0000 Subject: [PATCH 2/4] test: cover vs-shell.ps1 multi-line env stash on Windows Windows-only; skips when vswhere.exe is absent so local Windows devs without VS are not broken. Spawns a fresh pwsh with VSINSTALLDIR unset so Enter-VsDevShell runs inside the test, with a probe env var whose value contains KEY=VALUE-shaped continuation lines. Asserts the probe round-trips intact, no leaked vars appear, and the VS toolchain env is still loaded. --- test/internal/vs-shell-multiline-env.test.ts | 71 ++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 test/internal/vs-shell-multiline-env.test.ts diff --git a/test/internal/vs-shell-multiline-env.test.ts b/test/internal/vs-shell-multiline-env.test.ts new file mode 100644 index 000000000000..7fb90be64809 --- /dev/null +++ b/test/internal/vs-shell-multiline-env.test.ts @@ -0,0 +1,71 @@ +// Regression guard for scripts/vs-shell.ps1: Enter-VsDevShell parses +// `cmd /c set` output line by line, so a multi-line env value (e.g. the +// commit message in BUILDKITE_MESSAGE) used to leak each `KEY=VALUE`-shaped +// body line as its own env var. A body line like `BUN_JSC_foo=bar baz` then +// aborted every spawned Bun process on the Windows test lanes. +import { expect, test } from "bun:test"; +import { bunEnv, bunExe, isWindows } from "harness"; +import { existsSync } from "node:fs"; +import { join } from "node:path"; + +const vswhere = "C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\vswhere.exe"; + +test.skipIf(!isWindows || !existsSync(vswhere))( + "vs-shell.ps1 shields multi-line env vars from Enter-VsDevShell", + async () => { + const repoRoot = join(import.meta.dir, "..", ".."); + const vsShell = join(repoRoot, "scripts", "vs-shell.ps1"); + const probe = [ + "line1", + "VS_SHELL_LEAKED=oops", + "BUN_JSC_notARealOption=1 some trailing text", + "line4", + ].join("\n"); + + const dump = + "process.stdout.write(JSON.stringify({" + + "probe:process.env.VS_SHELL_PROBE," + + "leaked:process.env.VS_SHELL_LEAKED," + + "jsc:process.env.BUN_JSC_notARealOption," + + "vs:process.env.VSINSTALLDIR" + + "}))"; + + const env: Record = { ...bunEnv, VS_SHELL_PROBE: probe }; + // The Windows test runner already sits inside vs-shell.ps1, so VSINSTALLDIR + // is set; unset it so the child re-runs the loader and hits the stash path. + delete env.VSINSTALLDIR; + + await using proc = Bun.spawn({ + cmd: ["pwsh", "-NoProfile", "-File", vsShell, bunExe(), "-e", dump], + env, + stdout: "pipe", + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([ + proc.stdout.text(), + proc.stderr.text(), + proc.exited, + ]); + + // vs-shell.ps1 writes status lines before `$ `; the JSON is the last line. + const jsonLine = stdout.trimEnd().split("\n").pop() ?? ""; + let result: { probe: unknown; leaked: unknown; jsc: unknown; vs: unknown }; + try { + result = JSON.parse(jsonLine); + } catch { + throw new Error( + `expected JSON on last line, got:\n--- stdout ---\n${stdout}\n--- stderr ---\n${stderr}`, + ); + } + + expect(result).toEqual({ + probe, // round-tripped intact, newlines preserved + leaked: undefined, // body line did not become its own var + jsc: undefined, // body line did not become its own var + vs: expect.any(String), // VS env was still loaded + }); + expect(stderr).not.toContain("invalid JSC environment variable"); + expect(exitCode).toBe(0); + }, + 60_000, +); From a6a36e2deb2726320865d5ebf902af301c607a7a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:46:13 +0000 Subject: [PATCH 3/4] [autofix.ci] apply automated fixes --- test/internal/vs-shell-multiline-env.test.ts | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/test/internal/vs-shell-multiline-env.test.ts b/test/internal/vs-shell-multiline-env.test.ts index 7fb90be64809..1635aca0cd6d 100644 --- a/test/internal/vs-shell-multiline-env.test.ts +++ b/test/internal/vs-shell-multiline-env.test.ts @@ -15,12 +15,7 @@ test.skipIf(!isWindows || !existsSync(vswhere))( async () => { const repoRoot = join(import.meta.dir, "..", ".."); const vsShell = join(repoRoot, "scripts", "vs-shell.ps1"); - const probe = [ - "line1", - "VS_SHELL_LEAKED=oops", - "BUN_JSC_notARealOption=1 some trailing text", - "line4", - ].join("\n"); + const probe = ["line1", "VS_SHELL_LEAKED=oops", "BUN_JSC_notARealOption=1 some trailing text", "line4"].join("\n"); const dump = "process.stdout.write(JSON.stringify({" + @@ -41,11 +36,7 @@ test.skipIf(!isWindows || !existsSync(vswhere))( stdout: "pipe", stderr: "pipe", }); - const [stdout, stderr, exitCode] = await Promise.all([ - proc.stdout.text(), - proc.stderr.text(), - proc.exited, - ]); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); // vs-shell.ps1 writes status lines before `$ `; the JSON is the last line. const jsonLine = stdout.trimEnd().split("\n").pop() ?? ""; @@ -53,9 +44,7 @@ test.skipIf(!isWindows || !existsSync(vswhere))( try { result = JSON.parse(jsonLine); } catch { - throw new Error( - `expected JSON on last line, got:\n--- stdout ---\n${stdout}\n--- stderr ---\n${stderr}`, - ); + throw new Error(`expected JSON on last line, got:\n--- stdout ---\n${stdout}\n--- stderr ---\n${stderr}`); } expect(result).toEqual({ From 9456e266d967f0914daa98636db09129b070fb25 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:58:39 +0000 Subject: [PATCH 4/4] test: drop per-test timeout (runner.node.mjs already passes --timeout) --- test/internal/vs-shell-multiline-env.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/test/internal/vs-shell-multiline-env.test.ts b/test/internal/vs-shell-multiline-env.test.ts index 1635aca0cd6d..c3c596a18437 100644 --- a/test/internal/vs-shell-multiline-env.test.ts +++ b/test/internal/vs-shell-multiline-env.test.ts @@ -56,5 +56,4 @@ test.skipIf(!isWindows || !existsSync(vswhere))( expect(stderr).not.toContain("invalid JSC environment variable"); expect(exitCode).toBe(0); }, - 60_000, );