fix(scripts): spawn pnpm without the Windows .cmd shim - #1776
fix(scripts): spawn pnpm without the Windows .cmd shim#1776HuzaifaAbdulRehman wants to merge 4 commits into
Conversation
Node has refused to spawn .cmd and .bat files without a shell since 20.12.2,
18.20.2 and 21.7.3, so spawnSync('pnpm.cmd', args) fails with EINVAL. That
broke code-change-verification on its first step and prettier-changed.mjs on
the format-check step, leaving Windows contributors unable to run the
verification the contributor guide requires.
Build the cmd.exe command line explicitly and pass it verbatim instead of
using shell: true, which joins argv without quoting and would split
`pnpm -r -F "@openai/*" dist:check` into several arguments.
Escape cmd.exe metacharacters twice. pnpm.cmd is a batch shim that forwards
%*, so the first parse consumes one layer and the shim's own expansion is
parsed again. Arguments carrying % are refused outright, since cmd.exe
expands it inside quotes and no escape survives both parses.
|
|
The Windows failure is real, but I think the custom two-pass This repository already depends on That would avoid maintaining a general-purpose cmd parser/escaper and would preserve filenames containing |
|
You are right about One thing worth recording: execa 9.6.1 still lets a crafted quote through a |
Execa is already a dev dependency, resolves the Windows pnpm shim without a shell and quotes arguments itself, so the hand-written cmd.exe escaping is not needed. That escaping also rejected arguments containing %, which is legal in a Windows file name, so prettier-changed.mjs could fail on a legitimate changed file before Prettier ran. Only the bootstrap install runs before node_modules exists. It takes no caller-supplied arguments, so it keeps a fixed cmd.exe command line and everything after it goes through execaSync.
|
@seratch this is my first PR here and the On the approach: after the review feedback I dropped the hand-rolled |
sylvesterkaczmarek
left a comment
There was a problem hiding this comment.
One failure-reporting regression remains in the Execa path. With reject: false, Execa returns the error object, and in Execa 9.6.1 exitCode is undefined when the subprocess fails to spawn. runPnpm() currently discards that error state and passes only exitCode/signal into reportExit(), so an ENOENT/spawn failure is reported as terminated by an unknown signal; the old spawnSync path explicitly checked result.error and printed the actual startup error.
Could we preserve Execa's spawn failure here, for example by checking result.failed when exitCode and signal are absent and surfacing shortMessage/originalMessage? scripts/prettier-changed.mjs has the same edge case: a failed spawn with reject: false now falls through to process.exit(prettier.exitCode ?? 1) without reporting the returned Execa error. A focused missing-pnpm test would pin both diagnostics.
With reject: false Execa returns its error rather than throwing, and a process that never started carries no exitCode and no signal. runPnpm read exitCode alone, so a missing pnpm was reported as "terminated by an unknown signal" and the startup error that spawnSync exposed through result.error was lost. prettier-changed.mjs exited 1 without reporting it at all. Add execaRunOutcome, which separates a normal exit, a signal, and a process that never started, and surface the Execa message in the last case. Both scripts now share it.
|
You are right, thanks for catching it. An Execa result for a process that never started carries My earlier check missed this because I probed a missing binary, which on Windows resolves through Fixed in 03ae793. Five tests cover it. One triggers a real Execa spawn failure instead of a hand-built object, and three of the five fail against the previous logic. |
seratch
left a comment
There was a problem hiding this comment.
Thanks for the updates. The Execa approach addresses the earlier quoting and startup-error concerns.
Before merging, please add the Windows-only pnpm shim test to Windows CI and provide results from the actual verification entrypoint on a clean Windows checkout, including the pnpm installation method. The evidence should cover bootstrap installation, the compound validation arguments, formatting a changed filename containing %, and failure exit-code propagation.
The current Windows job runs only sandbox tests, while the Linux jobs skip the new Windows-only case, so the green checks do not yet establish that the affected workflow works end to end.
|
Added the Windows-only shim test to the Windows CI job. In a clean Windows worktree with pnpm 11.24.0 installed through Corepack, the PowerShell verification script passed bootstrap, the initial build, compound validation, |
sylvesterkaczmarek
left a comment
There was a problem hiding this comment.
Rechecked current 0f012a11. My earlier spawn-failure diagnostic concern is fixed: the Execa result keeps the failed-start state and surfaces the startup message, and the focused bootstrap suite passes locally (9 passed, 1 Windows-only skipped on macOS). The author has also now provided the clean-Windows verification requested by @seratch: Corepack-installed pnpm 11.24.0, bootstrap, compound validation args, a % filename, shim tests, and failure exit-code propagation. I do not see a remaining blocker in the affected path.
Summary
Node has refused to spawn
.cmdwithout a shell since 20.12.2, sorun.mjs:55dies withspawnSync pnpm.cmd EINVAL, andAGENTS.mdmakes that script mandatory.scripts/prettier-changed.mjs:66shares the defect in the format-check step.Execa is already a dev dependency and resolves Windows shims itself, so both now use
execaSync. Only the bootstrap install runs beforenode_modulesexists; it takes no dynamic arguments, so a fixedcmd.exeline covers it.Test plan
pnpm exec vitest run scripts/pnpm-bootstrap.test.mjs --mode full.agents/skills/code-change-verification/scripts/run.shon a clean Windows checkoutbuild%PATH%probe.tsnow formats instead of failingIssue number
Closes #1775
Checks
No package code changed, so no changeset. No public API touched.