Files
pnpm/.github/workflows/test.yml
Zoltan Kochan fe28cc6db6 ci: build pnpr per-OS so Linux tests don't wait on the Windows build (#12630)
* ci: build pnpr per-OS so Linux tests don't wait on the Windows build

The pnpr binaries were built by a single `build-pnpr` matrix job spanning
`[ubuntu-latest, windows-latest]`. A `needs` edge can only target a whole
job, never an individual matrix leg, so every consumer of `build-pnpr`
(the ubuntu smoke/test jobs and the windows test shards) blocked until both
legs finished. The Linux tests therefore waited for the slower Windows pnpr
build even though they only consume the Linux binary artifact.

Extract the build into a per-OS reusable workflow (`build-pnpr.yml`) and call
it twice from `ci.yml` as `build-pnpr-linux` and `build-pnpr-windows`. The
ubuntu test jobs now gate only on `build-pnpr-linux` and the windows shards
only on `build-pnpr-windows`, so neither platform waits on the other's build.

* ci: trigger TS CI when build-pnpr.yml changes

Add the new reusable .github/workflows/build-pnpr.yml to the dorny/paths-filter
ts: list so a PR touching only that workflow still runs the pnpr build and TS
test jobs it gates, instead of being skipped.
2026-06-24 15:21:28 +02:00

280 lines
9.9 KiB
YAML

name: Test (reusable)
on:
workflow_call:
inputs:
node:
required: true
type: string
node_major:
required: true
type: string
platform:
required: true
type: string
garnet:
required: false
type: boolean
default: false
test_chunk:
required: false
type: string
default: '1'
test_chunk_total:
required: false
type: string
default: '1'
secrets:
GARNET_API_TOKEN:
required: false
permissions:
contents: read
jobs:
test:
name: Node ${{ inputs.node_major }} / chunk ${{ inputs.test_chunk }}/${{ inputs.test_chunk_total }}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.platform }}-${{ inputs.node }}-${{ inputs.test_chunk }}-${{ inputs.test_chunk_total }}
cancel-in-progress: true
runs-on: ${{ inputs.platform }}
steps:
# A near-full "affected packages" sweep 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
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, not a
# published `@pnpm/pnpr` that may predate it) once per OS by the
# `build-pnpr` reusable workflow, then shared with every test job/shard
# here as an artifact instead of rebuilt per job.
- name: Download prebuilt pnpr binaries
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: pnpr-bins-${{ runner.os }}
path: .pnpr-bin
- name: Export pnpr binary paths
shell: bash
run: |
ext=""
[ -f "$PWD/.pnpr-bin/pnpr.exe" ] && ext=".exe"
# Artifacts don't preserve the executable bit; restore it on POSIX.
[ -z "$ext" ] && chmod +x "$PWD/.pnpr-bin/pnpr" "$PWD/.pnpr-bin/pnpr-prepare"
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: Validate test chunk inputs
shell: bash
env:
TEST_CHUNK: ${{ inputs.test_chunk }}
TEST_CHUNK_TOTAL: ${{ inputs.test_chunk_total }}
run: |
case "$TEST_CHUNK" in
''|*[!0-9]*)
echo "::error::test_chunk must be a positive integer"
exit 1
;;
esac
case "$TEST_CHUNK_TOTAL" in
''|*[!0-9]*)
echo "::error::test_chunk_total must be a positive integer"
exit 1
;;
esac
if (( TEST_CHUNK < 1 || TEST_CHUNK_TOTAL < 1 || TEST_CHUNK > TEST_CHUNK_TOTAL )); then
echo "::error::test_chunk must be between 1 and test_chunk_total"
exit 1
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 }}
TEST_CHUNK: ${{ inputs.test_chunk }}
TEST_CHUNK_TOTAL: ${{ inputs.test_chunk_total }}
run: |
benchmark="$BENCHMARK"
command=(pn run "$TEST_SCRIPT")
if [ "$TEST_CHUNK_TOTAL" != "1" ]; then
benchmark="${BENCHMARK}.chunk${TEST_CHUNK}"
command=(
node .github/scripts/run-ts-tests-chunk.mjs
--script "$TEST_SCRIPT"
--chunk "$TEST_CHUNK"
--chunks "$TEST_CHUNK_TOTAL"
)
fi
node .github/scripts/measure-command.mjs \
--name "$benchmark" \
--output ".bench/${benchmark}.json" \
-- "${command[@]}"
- name: Extract pnpm CLI e2e test duration
if: steps.test-scope.outputs.full_tests == 'true'
shell: bash
env:
TEST_CHUNK: ${{ inputs.test_chunk }}
TEST_CHUNK_TOTAL: ${{ inputs.test_chunk_total }}
run: |
benchmark=tests.cli
args=(
--name "$benchmark"
--output ".bench/${benchmark}.json"
--package-dir pnpm11/pnpm
)
if [ "$TEST_CHUNK_TOTAL" != "1" ]; then
benchmark="${benchmark}.chunk${TEST_CHUNK}"
args=(
--name "$benchmark"
--output ".bench/${benchmark}.json"
--package-dir pnpm11/pnpm
--allow-missing
)
fi
node .github/scripts/bencher-result-from-pnpm-summary.mjs "${args[@]}"
- name: Stage Bencher test durations
if: steps.test-scope.outputs.full_tests == 'true'
env:
BENCHMARK: ${{ steps.test-scope.outputs.benchmark }}
NODE_VERSION: ${{ inputs.node }}
PLATFORM: ${{ inputs.platform }}
TEST_CHUNK: ${{ inputs.test_chunk }}
TEST_CHUNK_TOTAL: ${{ inputs.test_chunk_total }}
shell: bash
run: |
benchmark="$BENCHMARK"
cli_benchmark=tests.cli
if [ "$TEST_CHUNK_TOTAL" != "1" ]; then
benchmark="${BENCHMARK}.chunk${TEST_CHUNK}"
cli_benchmark="${cli_benchmark}.chunk${TEST_CHUNK}"
fi
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"
files=(".bench/${benchmark}.json")
if [ -f ".bench/${cli_benchmark}.json" ]; then
files+=(".bench/${cli_benchmark}.json")
fi
node .github/scripts/merge-bencher-results.mjs \
--output "$artifact_dir/results.json" \
-- "${files[@]}"
cat > "$artifact_dir/metadata.json" <<EOF
{
"kind": "pnpm",
"testbed": "pnpm.${platform_slug}.node${node_major}",
"chunk": {
"index": ${TEST_CHUNK},
"total": ${TEST_CHUNK_TOTAL}
}
}
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 }}-chunk-${{ inputs.test_chunk }}-of-${{ inputs.test_chunk_total }}
path: .bench/artifact/pnpm/
if-no-files-found: error
retention-days: 14