From 5742da3cdba7a74ec79ea2a0b8ff1f2b785a62e8 Mon Sep 17 00:00:00 2001 From: os-steve Date: Sun, 23 Aug 2026 14:37:12 +0000 Subject: [PATCH] ci: materialise pnpm from a cached Corepack store instead of downloading per job `corepack enable` only writes shims; the pnpm tarball is fetched from registry.npmjs.org on the first pnpm invocation in the job. A merge-queue build is 24 jobs, so one merge attempt made 24 independent registry calls before any test ran, and any one of them could eject the PR and force every PR behind it to rebuild. Add a `.github/actions/setup-pnpm` composite action that restores the Corepack store (COREPACK_HOME) from the actions cache, keyed on the packageManager pin, then materialises pnpm from it. A warm store makes the happy path fully network-free. A bounded retry covers the cold-cache case only. Applied uniformly to all 7 Corepack sites in ci.yml, including the one at the temporal-conformance job that had no paired verify step. `actions/setup-node` deliberately stays in the workflow at all 7 sites: scripts/check-node-version.mjs scans .github/workflows/*.yml only and reports how many setup-node steps it audited, so moving those steps into the composite would drop them from its census while it still printed OK. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx --- .github/actions/setup-pnpm/action.yml | 111 ++++++++++++++++++++++++++ .github/workflows/ci.yml | 46 ++++------- 2 files changed, 125 insertions(+), 32 deletions(-) create mode 100644 .github/actions/setup-pnpm/action.yml diff --git a/.github/actions/setup-pnpm/action.yml b/.github/actions/setup-pnpm/action.yml new file mode 100644 index 0000000000..08a0c0667c --- /dev/null +++ b/.github/actions/setup-pnpm/action.yml @@ -0,0 +1,111 @@ +# Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. +# +# setup-pnpm -- materialise the repo-pinned pnpm from a CACHED Corepack store. +# +# Why this exists. `corepack enable` only writes shims; the pnpm tarball is +# fetched from registry.npmjs.org on the FIRST pnpm invocation in the job -- +# which is why the crash always surfaced in the innocuous-looking +# `Verify pnpm version` step (`run: pnpm --version`) rather than anywhere that +# names a download. A queue build is 24 jobs, so one merge attempt made 24 +# independent calls to the registry before a single test ran, and any one of +# them could eject the PR and force every PR behind it to rebuild. +# +# Two failure modes were measured on the queue, both inside Corepack's fetch and +# neither attributable to the PR being tested: +# +# AssertionError [ERR_ASSERTION]: assert(!this.paused) +# at Parser.finish (node:internal/deps/undici/undici:6165:9) +# +# Error: Client network socket disconnected before secure TLS connection +# was established ... code: 'ECONNRESET' +# +# The first is a crash in Node's own bundled undici parser, so it presents as an +# AssertionError with no test file attached -- the merge-queue triage heuristic +# reads that as "real behaviour change, fix the PR", exactly inverting the truth. +# +# The remedy is to stop making the call. Corepack keeps materialised package +# managers under COREPACK_HOME, so restoring that directory from the actions +# cache makes the happy path fully network-free -- verified locally: with a warm +# store and the registry pointed at an unreachable host, `corepack install` and +# `pnpm --version` both still succeed, while the same commands against a cold +# store die in `installVersion`/`fetchTarballURLAndSignature`, the same code path +# as the CI crash. +# +# The retry is the COLD-cache backstop, not the fix: it only matters on the one +# build after a `packageManager` bump, when every job misses the cache at once. +# Restoring the cache is what removes the steady-state exposure. +# +# Deliberately NOT in here: `actions/setup-node`. `scripts/check-node-version.mjs` +# scans `.github/workflows/*.yml` ONLY, and reports how many setup-node steps it +# audited. Moving those steps into this composite would drop them from its census +# and it would still print OK -- a gate silently auditing less than it says. +# Callers keep their own `setup-node` step, with its literal `node-version` pin. + +name: Setup pnpm +description: >- + Enable Corepack and materialise the pnpm version pinned in package.json, + restoring the Corepack store from cache so the happy path makes no network + call to the npm registry. + +runs: + using: composite + steps: + - name: Resolve the pinned package manager + id: pin + shell: bash + run: | + set -euo pipefail + spec="$(node -p "require('$GITHUB_WORKSPACE/package.json').packageManager ?? ''")" + if [ -z "$spec" ]; then + echo "::error::package.json declares no \"packageManager\" pin -- Corepack has nothing to materialise." + exit 1 + fi + echo "Pinned package manager: $spec" + { + echo "spec=$spec" + echo "key=$(printf '%s' "$spec" | sha256sum | cut -d' ' -f1)" + } >> "$GITHUB_OUTPUT" + # Set for the REST OF THE JOB, not just this action: every later `pnpm` + # call must read the same store this action populated. + echo "COREPACK_HOME=${{ runner.temp }}/corepack" >> "$GITHUB_ENV" + + # Keyed on the packageManager pin alone -- not on package.json's hash, which + # would churn the cache on every unrelated dependency edit. No restore-keys: + # a store for a different pnpm version cannot satisfy this pin, and a partial + # hit would only mask a cold start. A cache-service failure is non-fatal here + # and degrades to a download, which the retry below then covers. + - name: Restore the Corepack store + uses: actions/cache@v6 + with: + path: ${{ runner.temp }}/corepack + key: ${{ runner.os }}-corepack-${{ steps.pin.outputs.key }} + + - name: Enable Corepack + shell: bash + run: corepack enable + + - name: Materialise pnpm + shell: bash + working-directory: ${{ github.workspace }} + env: + PM_SPEC: ${{ steps.pin.outputs.spec }} + run: | + set -uo pipefail + attempts=3 + for i in $(seq 1 "$attempts"); do + # A warm store makes this a no-op that exits 0 without any network I/O. + if corepack install; then + exit 0 + fi + if [ "$i" -lt "$attempts" ]; then + delay=$((i * 5)) + echo "::warning::Corepack could not materialise ${PM_SPEC} (attempt ${i}/${attempts}); retrying in ${delay}s." + sleep "$delay" + fi + done + echo "::error::Corepack failed to materialise ${PM_SPEC} after ${attempts} attempts." + exit 1 + + - name: Verify pnpm version + shell: bash + run: pnpm --version diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b56adb0753..11d29a27fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -288,11 +288,8 @@ jobs: with: node-version: '22' - - name: Enable Corepack - run: corepack enable - - - name: Verify pnpm version - run: pnpm --version + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm - name: Get pnpm store directory shell: bash @@ -874,8 +871,8 @@ jobs: with: node-version: '22' - - name: Enable Corepack - run: corepack enable + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm - name: Get pnpm store directory shell: bash @@ -1068,11 +1065,8 @@ jobs: with: node-version: '22' - - name: Enable Corepack - run: corepack enable - - - name: Verify pnpm version - run: pnpm --version + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm - name: Get pnpm store directory shell: bash @@ -1207,11 +1201,8 @@ jobs: with: node-version: '22' - - name: Enable Corepack - run: corepack enable - - - name: Verify pnpm version - run: pnpm --version + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm - name: Get pnpm store directory shell: bash @@ -1401,11 +1392,8 @@ jobs: with: node-version: '22' - - name: Enable Corepack - run: corepack enable - - - name: Verify pnpm version - run: pnpm --version + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm - name: Get pnpm store directory shell: bash @@ -1550,11 +1538,8 @@ jobs: with: node-version: '22' - - name: Enable Corepack - run: corepack enable - - - name: Verify pnpm version - run: pnpm --version + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm - name: Get pnpm store directory shell: bash @@ -1723,11 +1708,8 @@ jobs: with: node-version: '22' - - name: Enable Corepack - run: corepack enable - - - name: Verify pnpm version - run: pnpm --version + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm - name: Get pnpm store directory shell: bash