ci(windows): stash multi-line env vars around Enter-VsDevShell - #34355
ci(windows): stash multi-line env vars around Enter-VsDevShell#34355robobun wants to merge 4 commits into
Conversation
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.
|
Warning Review limit reached
Next review available in: 9 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
Comment |
|
Updated 7:30 AM PT - Jul 16th, 2026
❌ @robobun, your commit 9456e26 has 1 failures in
🧪 To try this PR locally: bunx bun-pr 34355That installs a local version of the PR into your bun-34355 --bun |
There was a problem hiding this comment.
LGTM — small, self-contained Windows CI script fix with a clear root cause and verified before/after.
What was reviewed:
- Stash/restore is symmetric:
$multilineEnvis always initialized before thetry, and restore runs infinallyon both success and throw paths. -LiteralPathonRemove-Item/Set-Itemavoids wildcard interpretation for names likeProgramFiles(x86).Get-ChildItem env:is enumerated as a snapshot, so removing entries inside the loop is safe;[\r\n]correctly catches both CR and LF.
Extended reasoning...
Overview
This PR touches a single file, scripts/vs-shell.ps1, which wraps Windows CI commands in the Visual Studio developer shell. It adds ~14 lines that stash any environment variables containing CR/LF into a hashtable, remove them before invoking Launch-VsDevShell.ps1, and restore them in the existing finally block. The root cause (Microsoft's Enter-VsDevShell parses cmd /c set output line-by-line and promotes lines from multi-line values like BUILDKITE_MESSAGE into new env vars) is well-documented in the PR description with a concrete failing build and a manual before/after repro on Windows.
Security risks
None. This is CI-only PowerShell that manipulates the process's own environment variables; no new inputs are parsed, no external commands are added, and the values being stashed/restored are already present in the environment. No src/ or test-harness code is touched.
Level of scrutiny
Low. This is build/CI tooling, not shipped runtime code, and the change is additive around an existing try/finally. The failure mode of a bug here would be a visibly broken Windows CI lane, not a silent runtime regression. There is no CODEOWNERS entry for scripts/.
Other factors
- Checked that
$multilineEnvis defined unconditionally before thetryblock, so thefinallyrestore loop cannot see an undefined variable. -LiteralPathis used for bothRemove-ItemandSet-Item, which correctly handles env var names containing(/)or other glob characters.foreachover(Get-ChildItem env:)iterates a materialized snapshot, soRemove-Iteminside the loop does not mutate the collection being walked.- VS toolchain vars (
PATH,INCLUDE,LIB, etc.) are single-line semicolon-delimited and won't be stashed; the PR author confirmed they remain set after the change. - No prior reviews from me on this PR; only a CodeRabbit rate-limit notice in the timeline. The lack of an automated test is justified in the description (no harness drives
vs-shell.ps1, andEnter-VsDevShellisn't available on Linux runners), consistent with prior CI-script PRs like #32377.
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.
There was a problem hiding this comment.
Additional findings (outside current diff — PR may have been updated during review):
-
🟡
test/internal/vs-shell-multiline-env.test.ts:70— Nit:test/CLAUDE.md:120says "CRITICAL: Do not set a timeout on tests. Bun already has timeouts." — the CI runner already passes--timeoutat the CLI level, so the trailing60_000argument here is redundant and violates that convention. Drop the60_000,argument.Extended reasoning...
What the issue is
test/CLAUDE.mdline 120 states:CRITICAL: Do not set a timeout on tests. Bun already has timeouts.
The new test file passes
60_000as the third argument totest.skipIf(...)(name, fn, 60_000)attest/internal/vs-shell-multiline-env.test.ts:70, which sets an explicit per-test timeout. This is a newly-introduced violation of an explicit repository instruction.Why the rule exists
The CI test runner (
scripts/runner.node.mjs) already passes--timeout=${perTestTimeout}at the CLI level, which is what "Bun already has timeouts" refers to. Per-test timeouts scattered across individual test files make it harder to tune timeout behavior centrally (e.g., when debug/ASAN builds need longer timeouts across the board), and they tend to accumulate as copy-paste artifacts. No other test intest/internal/sets an explicit timeout.Step-by-step
test/CLAUDE.md:120— the rule is explicit and marked CRITICAL.test/internal/vs-shell-multiline-env.test.ts:70— the trailing60_000,is the third positional arg to thetest.skipIf(...)(...)call, whichbun:testinterprets as the per-test timeout in ms.- The test spawns
pwshwhich invokesEnter-VsDevShell— this can legitimately be slow, which is presumably the motivation, but the CI runner's CLI-level--timeoutalready accounts for slow subprocess tests on Windows lanes.
Impact
None functionally — the test works either way. This is purely a repository-convention violation. It will not cause a concrete failure if merged as-is; the CLI-level timeout and the per-test timeout coexist without conflict. Marking as nit because merging without the fix causes no runtime breakage.
How to fix
Delete the trailing
60_000,argument on line 70:expect(exitCode).toBe(0); }, - 60_000, );If
Enter-VsDevShellgenuinely needs more than the default CI timeout on some Windows hosts, the right lever is the CI runner's per-file timeout configuration, not a per-test override.
|
Dropped the per-test timeout in 9456e26. The CI runner already passes |
There was a problem hiding this comment.
LGTM — the timeout nit was addressed in 9456e26.
What was reviewed:
- Stash/restore in
vs-shell.ps1:-LiteralPathused for both remove and set, restore is infinallyso it runs even ifLaunch-VsDevShell.ps1throws, and$multilineEnvis initialized beforetryso the finally never sees an unset hashtable. - Considered whether hiding a multi-line var from the VS loader could break toolchain setup — no, VS env vars are single-line, and any multi-line one was already corrupted by the
setparsing this works around. - Test: forces the loader path by unsetting
VSINSTALLDIR, drains pipes concurrently, asserts exact round-trip + no leaked vars + VS env still loaded, exitCode checked last.
Extended reasoning...
Overview
Two files: scripts/vs-shell.ps1 gains a ~14-line stash/restore around the Launch-VsDevShell.ps1 invocation, and test/internal/vs-shell-multiline-env.test.ts is a new Windows-only regression guard. No runtime (src/) code is touched — this is purely CI/build infrastructure for the Windows lanes.
Security risks
None. The script only reads and rewrites its own process environment; nothing from untrusted input is interpreted or executed. -LiteralPath is used for both Remove-Item and Set-Item, so env var names containing wildcard characters aren't glob-expanded.
Level of scrutiny
Low-to-medium. This is a Windows CI wrapper script, not shipped runtime code. The failure mode of a mistake here is a red Windows CI lane, not a user-facing bug. The change is mechanically simple (hashtable stash → remove → restore in finally) and the PR description includes a manual before/after repro on a real Windows host.
Other factors
- The root-cause analysis is precise (Enter-VsDevShell parses
cmd /c setline-by-line;BUILDKITE_MESSAGEis multi-line) and the fix targets exactly that layer — the only place that can distinguish the spurious var from a real one. $multilineEnv = @{}is assigned before thetry, so thefinallyrestore loop is safe even when zero vars were stashed or the loader threw immediately.Get-ChildItem env:is enumerated into theforeachbefore mutation, so removing items mid-loop is safe.- Hiding multi-line vars from the VS loader is strictly no worse than before: any such value was already being corrupted by the
setparser, and no MSVC toolchain variable is multi-line. - The test follows harness conventions (
bunEnv/bunExe/isWindows,Promise.allon stdout/stderr/exited, structuredtoEqual, exitCode asserted last,skipIfgated onvswhere.exe). It deletesVSINSTALLDIRfrom the child env so the stash path actually executes even though the test runner itself already sits inside a VS shell. - My only prior comment (drop the explicit
60_000timeout) was addressed and the thread is resolved.
|
CI (build 73893): the only red is Ready for review. |
|
Another instance of this, for the record: build 74278 (PR #31951) has Verified locally on Windows 2019 with the build 74278 profile binary: fails through the current |
Problem
On Windows CI lanes only, every test that spawns a Bun subprocess via
bunEnvfailed at startup with:That string is a line from the commit message body. It does not appear in any code. Example: build 73847 (PR #34346), where every Windows test lane went red with the same error; Linux/macOS lanes were unaffected. It was worked around by amending the commit message, but any future commit whose body has a line starting
BUN_JSC_*=(or any otherKEY=valuethat a downstream tool inspects) will hit it again.Cause
Windows test steps are wrapped in
scripts/vs-shell.ps1, which callsEnter-VsDevShellfromMicrosoft.VisualStudio.DevShell.dllto load the MSVC toolchain. That cmdlet captures the environment by runningVsDevCmd.batinsidecmd.exeand parsing the output of thesetcommand line by line.sethas no way to represent a value that contains CR/LF: each physical output line is treated as an independentKEY=VALUEentry.Buildkite exports the full multi-line commit message as
BUILDKITE_MESSAGE. GivenEnter-VsDevShellwrites backBUILDKITE_MESSAGE=commit subject, promotes the third line to a brand-newBUN_JSC_collectContinuouslyenv var with value1 stress over serve+ws+reload and over, and echoes the non-=line to stdout. That env var then flows throughnode scripts/runner.node.mjsinto every spawnedbunprocess, whereJSCInitializerejects it (the value is not a valid boolean) and exits 1.This cannot be fixed downstream in
test/harness.tsorrunner.node.mjs: by the time they run, the spurious var already exists under a name that does not start withBUILDKITE, so there is no way to tell it apart from a deliberately set JSC option.Fix
Before invoking
Launch-VsDevShell.ps1, stash every environment variable whose value contains CR or LF into a hashtable and remove it fromenv:. Restore them infinallyonce the loader returns. The VS dev shell only ever sees single-line input, and the wrapped command sees the original multi-line values intact.Verification
Reproduced and verified on a Windows 2019 host with VS 2022 17.14. With
Before
(exit 1;
some more textis the line without=, echoed by the DevShell parser)After
(exit 0;
BUILDKITE_MESSAGEpreserved verbatim, no spurious env var)Also checked:
VSINSTALLDIR/VCToolsVersion/INCLUDE/LIBare still set andcl.exeis onPATHafter the change; CRLF values are handled; with no multi-line vars present the stash loop is a no-op.test/internal/vs-shell-multiline-env.test.tsis the automated guard: Windows-only (skipped whenvswhere.exeis absent), it spawns a freshpwshwithVSINSTALLDIRunset soEnter-VsDevShellruns inside the test, with a probe value whose body contains both aKEY=valueline and aBUN_JSC_*=...line. It fails onmainwith the exactinvalid JSC environment variableerror and passes with this change.The fix lives entirely under
scripts/, so the fail-before gate'sgit stash push -- src/ packages/cannot demonstrate it (stashingsrc/leaves the script change in place). The before/after above and the new test are the proof.no test proof · iteration 0 · Platform-specific test-only change; deferring to CI.