mirror of
https://github.com/pnpm/pnpm.git
synced 2026-07-18 03:32:36 -04:00
Run Windows TypeScript test jobs only on Node.js 22 and split them into three parallel chunks. Windows TS tests are significantly slower than Linux, and running the full suite on three Node versions spends the extra capacity on duplicate runtime coverage instead of reducing wall clock time. The reusable test workflow now accepts chunk inputs, includes chunk identity in concurrency and artifact names, and keeps Bencher uploads with chunk-specific benchmark names. Add a CI helper that discovers the selected workspace packages for the same full or affected test scope, shards Jest test files into balanced chunks, runs selected package files with the same Jest Node options and package pretest setup, and writes pnpm-style execution summaries so the pnpm CLI e2e duration extraction still works.
280 lines
9.9 KiB
YAML
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` job, 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
|