From 65b868e36194262fe741db0e1af3d867a3e487b5 Mon Sep 17 00:00:00 2001 From: Dr_rOot Date: Fri, 17 Jul 2026 11:56:07 +0800 Subject: [PATCH 1/6] ci: add GitHub Actions workflow across os and node matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/workflows/ci.yml | 54 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5086a1f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,54 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + name: ${{ matrix.os }} / node ${{ matrix.node }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + node: [20, 22, 24] + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js ${{ matrix.node }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Lint + run: pnpm lint + + - name: Typecheck + run: pnpm typecheck + + - name: Test + run: pnpm test + + - name: Build + run: pnpm build From 3c4f886e006b70d9e98a67a5a474ec699368a998 Mon Sep 17 00:00:00 2001 From: Dr_rOot Date: Fri, 17 Jul 2026 12:00:51 +0800 Subject: [PATCH 2/6] chore: force lf line endings via .gitattributes 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. --- .gitattributes | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..71277b2 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Biome enforces lf (biome.json formatter.lineEnding); force lf on checkout +# everywhere so windows checkouts (core.autocrlf=true) don't fail lint. +* text=auto eol=lf From 3388c6970e2ecc1618043caff35f1724a1c2f8b8 Mon Sep 17 00:00:00 2001 From: Dr_rOot Date: Fri, 17 Jul 2026 12:00:51 +0800 Subject: [PATCH 3/6] ci: install standalone pnpm so node 20 matrix jobs can run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5086a1f..c724737 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,11 @@ jobs: - name: Setup pnpm uses: pnpm/action-setup@v4 + with: + # pnpm 11 needs node >= 22.13 to run itself; the standalone binary + # bundles its own runtime so the node 20 matrix jobs can still + # exercise the project's engines.node floor with PATH node 20. + standalone: true - name: Setup Node.js ${{ matrix.node }} uses: actions/setup-node@v4 From de60772848127c248bf472cb4892952c83abf23d Mon Sep 17 00:00:00 2001 From: Dr_rOot Date: Fri, 17 Jul 2026 12:11:22 +0800 Subject: [PATCH 4/6] ci: fetch pnpm standalone binary from GitHub release 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. --- .github/workflows/ci.yml | 41 +++++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c724737..d81bd8d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,13 +29,40 @@ jobs: with: persist-credentials: false - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - # pnpm 11 needs node >= 22.13 to run itself; the standalone binary - # bundles its own runtime so the node 20 matrix jobs can still - # exercise the project's engines.node floor with PATH node 20. - standalone: true + # pnpm 11 cannot run on node 20 (it requires node >= 22.13), so the + # node 20 jobs need the standalone build, which bundles its own + # runtime while project scripts still run on the matrix node. Both + # pnpm/action-setup standalone mode and `npm i -g @pnpm/exe` are + # broken for 11.13.0 — the @pnpm/* platform packages on npm were + # published without their binary (pnpm/action-setup#277) — so fetch + # the standalone binary straight from the GitHub release, versioned + # by package.json's packageManager field. + - name: Setup pnpm (standalone binary) + shell: bash + run: | + version="$(node -p "require('./package.json').packageManager.split('@').pop()")" + case "${RUNNER_OS}-${RUNNER_ARCH}" in + Linux-X64) asset="pnpm-linux-x64.tar.gz" ;; + Linux-ARM64) asset="pnpm-linux-arm64.tar.gz" ;; + macOS-ARM64) asset="pnpm-darwin-arm64.tar.gz" ;; + Windows-X64) asset="pnpm-win32-x64.zip" ;; + Windows-ARM64) asset="pnpm-win32-arm64.zip" ;; + *) echo "no pnpm standalone asset for ${RUNNER_OS}-${RUNNER_ARCH}" >&2; exit 1 ;; + esac + dest="${RUNNER_TEMP}/pnpm-standalone" + mkdir -p "$dest" + cd "$dest" + curl -fsSL --retry 3 -o "$asset" \ + "https://github.com/pnpm/pnpm/releases/download/v${version}/${asset}" + if [[ "$asset" == *.zip ]]; then + powershell.exe -NoProfile -Command "Expand-Archive -Path '${asset}' -DestinationPath '.' -Force" + else + tar -xzf "$asset" + chmod +x pnpm + fi + rm "$asset" + ./pnpm --version + echo "$dest" >> "$GITHUB_PATH" - name: Setup Node.js ${{ matrix.node }} uses: actions/setup-node@v4 From 7930132c876c1fb0c92b04349ee021b573e26ffe Mon Sep 17 00:00:00 2001 From: Dr_rOot Date: Fri, 17 Jul 2026 12:30:13 +0800 Subject: [PATCH 5/6] fix(run-command): stop double-escaping args for bare non-PM commands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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