mirror of
https://github.com/pnpm/pnpm.git
synced 2026-06-28 09:55:39 -04:00
## Problem On Windows, **any failed `pnpm` command hangs 20–46 seconds before exiting.** The error handler (`pnpm/src/errorHandler.ts`) enumerates descendant processes via `pidtree` to terminate them on every error exit. On Windows `pidtree` shells out to `wmic` and, where wmic has been removed, a PowerShell `Get-CimInstance Win32_Process` fallback — a process listing that takes tens of seconds on busy CI runners. This also broke Windows CI: the `verifyDepsBeforeRun/*` e2e suites are full of intentional-failure assertions (e.g. `pnpm start` with `--config.verify-deps-before-run=error` when deps aren't installed). Each failure paid the ~23 s error-handler tax, so the suite blew past the 70-minute cap. `pnpm install` and success paths never hit the error handler, which is why only failures were slow. Diagnosed by sampling `process.getActiveResourcesInfo()` during the hang: it showed a lingering `ProcessWrap` (a spawned child), and hooking `child_process.spawn` named it (`wmic` → `powershell … Get-CimInstance Win32_Process`, exiting after ~23–46 s). ## Fix Race the descendant-process lookup against a 2 s timeout. If it doesn't return in time, skip the kill and exit — `exit()` calls `process.exit`, which abandons the still-running (harmless, read-only) process query instead of blocking on it. The fast path (Unix, fast Windows) is unchanged. Confirmed on Windows CI: the failing `start` invocations dropped from **~23 s to ~2.7 s**, and `multiProjectWorkspace.ts` went from **716 s to 124 s**. ## Also included The CI pnpr-binary cache is split into `restore` + an explicit `save` step that runs right after the build, so a failing test step no longer discards the ~20-minute Rust build (the combined `actions/cache` only saved in a post-job step that gets skipped on failure).
230 lines
8.5 KiB
YAML
230 lines
8.5 KiB
YAML
name: Test (reusable)
|
|
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
node:
|
|
required: true
|
|
type: string
|
|
platform:
|
|
required: true
|
|
type: string
|
|
garnet:
|
|
required: false
|
|
type: boolean
|
|
default: false
|
|
secrets:
|
|
GARNET_API_TOKEN:
|
|
required: false
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
test:
|
|
name: Test
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.platform }}-${{ inputs.node }}
|
|
cancel-in-progress: true
|
|
|
|
runs-on: ${{ inputs.platform }}
|
|
|
|
steps:
|
|
# A near-full "affected packages" sweep plus the pnpr Rust build
|
|
# can exhaust the hosted runner's free disk mid-test (the runner
|
|
# worker itself dies with "No space left on device"). Drop the
|
|
# preinstalled toolchains this job never uses (~25 GB) first.
|
|
- name: Free up runner disk space
|
|
if: ${{ runner.os == 'Linux' }}
|
|
run: |
|
|
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
|
|
df -h /
|
|
- name: Configure Git
|
|
run: |
|
|
git config --global core.autocrlf false
|
|
git config --global user.name "xyz"
|
|
git config --global user.email "x@y.z"
|
|
- name: Checkout Commit
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
- if: ${{ inputs.garnet }}
|
|
uses: garnet-org/action@2b7fc9d79b54f551b43358c27424a36064b3e078 # v2
|
|
with:
|
|
api_token: ${{ secrets.GARNET_API_TOKEN }}
|
|
- name: Install pnpm and Node
|
|
uses: pnpm/setup@b1cac37306e39c21283b9dd6cb0ac288fb35ba6b
|
|
with:
|
|
runtime: node@${{ inputs.node }}
|
|
- name: Verify Node version
|
|
shell: bash
|
|
env:
|
|
NODE_VERSION: ${{ inputs.node }}
|
|
# `pn node -v` falls back through `run`/`exec`, which would
|
|
# otherwise trigger a verifyDepsBeforeRun install just to print
|
|
# the version. Disable it so this step measures the runtime that
|
|
# pnpm/setup provisioned, not one a stray install pulled in.
|
|
pnpm_config_verify_deps_before_run: false
|
|
run: |
|
|
actual=$(pn node -v)
|
|
expected="v${NODE_VERSION}"
|
|
if [ "$actual" != "$expected" ]; then
|
|
echo "Expected Node version $expected but got $actual"
|
|
exit 1
|
|
fi
|
|
# npm is needed for preparing git-hosted dependencies (e.g. in dlx tests).
|
|
# `pnpm runtime set node` does not extract npm; the runner image's
|
|
# pre-installed Node toolchain provides it on PATH.
|
|
- name: Verify npm
|
|
run: npm --version
|
|
- name: Download compiled artifacts
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: compiled-packages
|
|
- name: Extract compiled artifacts
|
|
run: tar -xzf compiled.tar.gz
|
|
# The test harness serves package fixtures through the in-repo
|
|
# `pnpr` server; `pnpr-prepare` turns the raw fixtures under
|
|
# `pnpr/.fixtures/packages` into the storage the server reads. Both
|
|
# are built from source so the tests exercise the current server
|
|
# (e.g. the install-accelerator endpoints) rather than a published
|
|
# `@pnpm/pnpr` binary that may predate it.
|
|
#
|
|
# The built binaries are cached and keyed on the Rust sources that
|
|
# produce them, so a run that only touches TypeScript restores them
|
|
# in seconds instead of recompiling. They are copied out of `target/`
|
|
# into a stable dir so `Swatinem/rust-cache`'s `target/` cleanup
|
|
# can't strip them before this cache saves.
|
|
- name: Restore prebuilt pnpr binaries
|
|
id: pnpr-bins
|
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
with:
|
|
path: .pnpr-bin
|
|
key: pnpr-bins-${{ runner.os }}-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock', '**/Cargo.toml', 'pnpr/**/*.rs', 'pacquet/**/*.rs') }}
|
|
- name: Install Rust
|
|
if: steps.pnpr-bins.outputs.cache-hit != 'true'
|
|
uses: ./.github/actions/rustup
|
|
with:
|
|
save-cache: ${{ github.ref_name == 'main' }}
|
|
shared-key: registry-prepare
|
|
- name: Build the pnpr server and registry fixture preparer
|
|
if: steps.pnpr-bins.outputs.cache-hit != 'true'
|
|
shell: bash
|
|
run: |
|
|
cargo build --locked --release -p pnpr --bin pnpr -p pnpr-fixtures --bin pnpr-prepare
|
|
mkdir -p .pnpr-bin
|
|
ext=""
|
|
[ -f target/release/pnpr.exe ] && ext=".exe"
|
|
cp "target/release/pnpr$ext" "target/release/pnpr-prepare$ext" .pnpr-bin/
|
|
# Save the binaries to the cache right after they build, not in a post-job
|
|
# step: a later step failing (e.g. a crashing test) would otherwise skip
|
|
# the save and force a full Rust rebuild on the next run.
|
|
- name: Save prebuilt pnpr binaries
|
|
if: steps.pnpr-bins.outputs.cache-hit != 'true'
|
|
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
|
with:
|
|
path: .pnpr-bin
|
|
key: ${{ steps.pnpr-bins.outputs.cache-primary-key }}
|
|
- name: Export pnpr binary paths
|
|
shell: bash
|
|
run: |
|
|
ext=""
|
|
[ -f "$PWD/.pnpr-bin/pnpr.exe" ] && ext=".exe"
|
|
echo "PNPR_BIN=$PWD/.pnpr-bin/pnpr$ext" >> "$GITHUB_ENV"
|
|
echo "PNPR_PREPARE_BIN=$PWD/.pnpr-bin/pnpr-prepare$ext" >> "$GITHUB_ENV"
|
|
- name: Determine test scope
|
|
id: test-scope
|
|
shell: bash
|
|
env:
|
|
REF_NAME: ${{ github.ref_name }}
|
|
run: |
|
|
if [[ "$REF_NAME" == "main" || "$REF_NAME" == "chore/update-lockfile" || "$REF_NAME" == release/* ]]; then
|
|
{
|
|
echo "script=ci:test-all"
|
|
echo "scope=all"
|
|
echo "full_tests=true"
|
|
echo "benchmark=tests.all"
|
|
} >> "$GITHUB_OUTPUT"
|
|
else
|
|
git remote set-branches --add origin main && git fetch origin main --depth=1
|
|
if [ -n "$(git diff --name-only origin/main HEAD -- pnpm-workspace.yaml)" ]; then
|
|
{
|
|
echo "script=ci:test-all"
|
|
echo "scope=all — pnpm-workspace.yaml modified"
|
|
echo "full_tests=true"
|
|
echo "benchmark=tests.all"
|
|
} >> "$GITHUB_OUTPUT"
|
|
else
|
|
{
|
|
echo "script=ci:test-branch"
|
|
echo "scope=affected packages"
|
|
echo "full_tests=false"
|
|
echo "benchmark=tests.affected"
|
|
} >> "$GITHUB_OUTPUT"
|
|
fi
|
|
fi
|
|
- name: Run tests (${{ steps.test-scope.outputs.scope }})
|
|
timeout-minutes: 70
|
|
shell: bash
|
|
env:
|
|
BENCHMARK: ${{ steps.test-scope.outputs.benchmark }}
|
|
PNPM_WORKERS: 3
|
|
TEST_SCRIPT: ${{ steps.test-scope.outputs.script }}
|
|
run: |
|
|
node .github/scripts/measure-command.mjs \
|
|
--name "$BENCHMARK" \
|
|
--output ".bench/${BENCHMARK}.json" \
|
|
-- pn run "$TEST_SCRIPT"
|
|
- name: Extract pnpm CLI e2e test duration
|
|
if: steps.test-scope.outputs.full_tests == 'true'
|
|
shell: bash
|
|
run: |
|
|
node .github/scripts/bencher-result-from-pnpm-summary.mjs \
|
|
--name tests.cli \
|
|
--output .bench/tests.cli.json \
|
|
--package-dir pnpm
|
|
- name: Stage Bencher test durations
|
|
if: steps.test-scope.outputs.full_tests == 'true'
|
|
env:
|
|
NODE_VERSION: ${{ inputs.node }}
|
|
PLATFORM: ${{ inputs.platform }}
|
|
shell: bash
|
|
run: |
|
|
platform_slug=${PLATFORM%-latest}
|
|
node_major=${NODE_VERSION%%.*}
|
|
case "$platform_slug" in
|
|
ubuntu|windows) ;;
|
|
*)
|
|
echo "::error::Unsupported platform '$platform_slug' for pnpm benchmarks; expected ubuntu or windows"
|
|
exit 1
|
|
;;
|
|
esac
|
|
case "$node_major" in
|
|
22|24|26) ;;
|
|
*)
|
|
echo "::error::Unsupported Node major '$node_major' for pnpm benchmarks; expected 22, 24, or 26"
|
|
exit 1
|
|
;;
|
|
esac
|
|
artifact_dir=.bench/artifact/pnpm
|
|
mkdir -p "$artifact_dir"
|
|
|
|
node .github/scripts/merge-bencher-results.mjs \
|
|
--output "$artifact_dir/results.json" \
|
|
-- .bench/tests.all.json .bench/tests.cli.json
|
|
|
|
cat > "$artifact_dir/metadata.json" <<EOF
|
|
{
|
|
"kind": "pnpm",
|
|
"testbed": "pnpm.${platform_slug}.node${node_major}"
|
|
}
|
|
EOF
|
|
- name: Upload Bencher test duration artifact
|
|
if: steps.test-scope.outputs.full_tests == 'true'
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: ci-performance-pnpm-${{ inputs.platform }}-node-${{ inputs.node }}
|
|
path: .bench/artifact/pnpm/
|
|
if-no-files-found: error
|
|
retention-days: 14
|