ci: add GitHub Actions workflow across os and node matrix#3
Merged
Conversation
Run lint, typecheck, test, and build on every push to main and every PR targeting main, across ubuntu/windows/macos x node 20/22/24 with fail-fast disabled. The windows runners are the only environment that exercises the win32 self-update path (cmd.exe escaping, absolute-path taskkill, exit-code 9009 command-not-found normalization). pnpm is installed via pnpm/action-setup before setup-node so the pnpm cache works; the pnpm version comes from the packageManager field. CI only — publishing stays a manual pnpm publish + OTP.
GitHub windows runners check out with core.autocrlf=true, converting every file to crlf and failing pnpm lint (biome.json pins formatter.lineEnding to lf). Forcing eol=lf at the git layer keeps checkouts biome-clean on all platforms.
pnpm 11.13.0 refuses to start on node < 22.13, which killed every node 20 combo at pnpm install. The standalone @pnpm/exe bundles its own runtime, while project scripts (vitest/tsc/tsup) still run on the matrix node — so the node 20 jobs genuinely test the engines floor.
pnpm/action-setup standalone mode is broken for pnpm 11.13.0: the @pnpm/* platform packages on npm were published without their binary, so every installer path leaves @pnpm/exe's placeholder bin in place (pnpm/action-setup#277). The GitHub release assets are built by a different pipeline and verified good, so download the standalone binary directly, versioned by the packageManager field.
looksLikeBatchShim treated every extensionless bare name as a .cmd shim, so 'node' args got the double meta-escape. cmd.exe parses an .exe command line only once, leaving literal carets in argv — node -e <script> broke outright (first observed on windows CI). Only the JS package managers we invoke by bare name ship as .cmd shims; classify exactly those, keep explicit .cmd/.bat paths, and correct the isCommandMissing doc (cmd exits 1, not 9009; the stderr phrase is the live signal).
Three POSIX assumptions surfaced by the new windows CI jobs: - path expectations hardcoded '/' while the code joins with the host separator — build expectations with join() so the platform-selection logic stays covered on every host - stat.mode & 0o777 === 0o600 — windows has no POSIX mode bits (reports 0o666), skip those assertions there - missing-binary and timeout results — win32 goes through cmd.exe, so a missing command is a non-zero exit + 'is not recognized' stderr (not a spawn ENOENT), and taskkill yields a non-zero code (not a signal null); branch the assertions by platform
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds
.github/workflows/ci.yml— the repo's first CI. On every push tomain, every PR targetingmain, and manualworkflow_dispatch, it runs the four gates in order:pnpm lint→pnpm typecheck→pnpm test→pnpm buildacross a 3×3 matrix:
ubuntu-latest/windows-latest/macos-latest× Node 20 / 22 / 24, withfail-fast: falseso one failing combo doesn't mask the others. Getting the matrix green also required one real product fix and a set of test-portability fixes (below).Why windows is in the matrix
The self-update win32 code in
src/run-command.ts(cross-spawn-style cmd.exe escaping, absolute-pathtaskkill, exit-code command-not-found normalization) only executes on win32 — the windows runners are the only place it actually runs. This paid off immediately: the first windows run caught a real bug (see below).What the first runs caught
pnpm/action-setupstandalone mode andnpm i -g @pnpm/exeare broken for 11.13.0 — the@pnpm/*platform packages on npm were published without their binary (Error after installing in standalone mode when trying to install packages with a CLI globally -This: not foundpnpm/action-setup#277). The workflow instead downloads the standalone binary straight from the pnpm GitHub release (a separate, intact pipeline), versioned by thepackageManagerfield. pnpm runs on its own bundled runtime; the gates run on the matrix node.core.autocrlf=true, failingbiome check(repo pinslineEnding: lf). Fixed at the git layer with.gitattributes(* text=auto eol=lf).run-command.ts:looksLikeBatchShimtreated every extensionless bare name as a.cmdshim, sonodearguments got the double meta-escape. cmd.exe parses an.execommand line only once, so literal carets reached node's argv andnode -e <script>broke outright. Now only the JS package managers invoked by bare name (npm/npx/pnpm/pnpx/yarn/corepack — actual.cmdshims) get the double escape; the BatBadBut mitigation for them is unchanged./in path expectations (nowjoin()-built so the platform-selection logic stays covered on every host),0o600mode assertions (windows has no POSIX mode bits — skipped there), and spawn-failure/timeout semantics (win32 goes through cmd.exe: missing command = non-zero exit + "is not recognized" stderr, not spawn ENOENT; taskkill = non-zero code, not signal null).Known pre-existing limitation, unchanged by this PR: a standalone pnpm.exe invoked as bare
pnpmstill gets double-escaped (caret-mangled) args on win32 — it fails loudly, and resolving the real extension via PATHEXT (as cross-spawn does) is the complete future fix.Design notes
permissions: contents: read(least privilege) andpersist-credentials: falseon checkout — no step needs the token after clone.concurrencykeyed by workflow + ref cancels superseded runs.NPM_TOKENor other secrets. Publishing stays manualpnpm publish+ OTP.Test plan
pnpm install --frozen-lockfile.