Skip to content

Commit a872ce9

Browse files
claude[bot]os-steveclaude
authored
ci: materialise pnpm from a cached Corepack store instead of downloading per job (#11370)
`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. Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx Co-authored-by: os-steve <steve@objectstack.ai> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 82b65ec commit a872ce9

2 files changed

Lines changed: 125 additions & 32 deletions

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2+
#
3+
# setup-pnpm -- materialise the repo-pinned pnpm from a CACHED Corepack store.
4+
#
5+
# Why this exists. `corepack enable` only writes shims; the pnpm tarball is
6+
# fetched from registry.npmjs.org on the FIRST pnpm invocation in the job --
7+
# which is why the crash always surfaced in the innocuous-looking
8+
# `Verify pnpm version` step (`run: pnpm --version`) rather than anywhere that
9+
# names a download. A queue build is 24 jobs, so one merge attempt made 24
10+
# independent calls to the registry before a single test ran, and any one of
11+
# them could eject the PR and force every PR behind it to rebuild.
12+
#
13+
# Two failure modes were measured on the queue, both inside Corepack's fetch and
14+
# neither attributable to the PR being tested:
15+
#
16+
# AssertionError [ERR_ASSERTION]: assert(!this.paused)
17+
# at Parser.finish (node:internal/deps/undici/undici:6165:9)
18+
#
19+
# Error: Client network socket disconnected before secure TLS connection
20+
# was established ... code: 'ECONNRESET'
21+
#
22+
# The first is a crash in Node's own bundled undici parser, so it presents as an
23+
# AssertionError with no test file attached -- the merge-queue triage heuristic
24+
# reads that as "real behaviour change, fix the PR", exactly inverting the truth.
25+
#
26+
# The remedy is to stop making the call. Corepack keeps materialised package
27+
# managers under COREPACK_HOME, so restoring that directory from the actions
28+
# cache makes the happy path fully network-free -- verified locally: with a warm
29+
# store and the registry pointed at an unreachable host, `corepack install` and
30+
# `pnpm --version` both still succeed, while the same commands against a cold
31+
# store die in `installVersion`/`fetchTarballURLAndSignature`, the same code path
32+
# as the CI crash.
33+
#
34+
# The retry is the COLD-cache backstop, not the fix: it only matters on the one
35+
# build after a `packageManager` bump, when every job misses the cache at once.
36+
# Restoring the cache is what removes the steady-state exposure.
37+
#
38+
# Deliberately NOT in here: `actions/setup-node`. `scripts/check-node-version.mjs`
39+
# scans `.github/workflows/*.yml` ONLY, and reports how many setup-node steps it
40+
# audited. Moving those steps into this composite would drop them from its census
41+
# and it would still print OK -- a gate silently auditing less than it says.
42+
# Callers keep their own `setup-node` step, with its literal `node-version` pin.
43+
44+
name: Setup pnpm
45+
description: >-
46+
Enable Corepack and materialise the pnpm version pinned in package.json,
47+
restoring the Corepack store from cache so the happy path makes no network
48+
call to the npm registry.
49+
50+
runs:
51+
using: composite
52+
steps:
53+
- name: Resolve the pinned package manager
54+
id: pin
55+
shell: bash
56+
run: |
57+
set -euo pipefail
58+
spec="$(node -p "require('$GITHUB_WORKSPACE/package.json').packageManager ?? ''")"
59+
if [ -z "$spec" ]; then
60+
echo "::error::package.json declares no \"packageManager\" pin -- Corepack has nothing to materialise."
61+
exit 1
62+
fi
63+
echo "Pinned package manager: $spec"
64+
{
65+
echo "spec=$spec"
66+
echo "key=$(printf '%s' "$spec" | sha256sum | cut -d' ' -f1)"
67+
} >> "$GITHUB_OUTPUT"
68+
# Set for the REST OF THE JOB, not just this action: every later `pnpm`
69+
# call must read the same store this action populated.
70+
echo "COREPACK_HOME=${{ runner.temp }}/corepack" >> "$GITHUB_ENV"
71+
72+
# Keyed on the packageManager pin alone -- not on package.json's hash, which
73+
# would churn the cache on every unrelated dependency edit. No restore-keys:
74+
# a store for a different pnpm version cannot satisfy this pin, and a partial
75+
# hit would only mask a cold start. A cache-service failure is non-fatal here
76+
# and degrades to a download, which the retry below then covers.
77+
- name: Restore the Corepack store
78+
uses: actions/cache@v6
79+
with:
80+
path: ${{ runner.temp }}/corepack
81+
key: ${{ runner.os }}-corepack-${{ steps.pin.outputs.key }}
82+
83+
- name: Enable Corepack
84+
shell: bash
85+
run: corepack enable
86+
87+
- name: Materialise pnpm
88+
shell: bash
89+
working-directory: ${{ github.workspace }}
90+
env:
91+
PM_SPEC: ${{ steps.pin.outputs.spec }}
92+
run: |
93+
set -uo pipefail
94+
attempts=3
95+
for i in $(seq 1 "$attempts"); do
96+
# A warm store makes this a no-op that exits 0 without any network I/O.
97+
if corepack install; then
98+
exit 0
99+
fi
100+
if [ "$i" -lt "$attempts" ]; then
101+
delay=$((i * 5))
102+
echo "::warning::Corepack could not materialise ${PM_SPEC} (attempt ${i}/${attempts}); retrying in ${delay}s."
103+
sleep "$delay"
104+
fi
105+
done
106+
echo "::error::Corepack failed to materialise ${PM_SPEC} after ${attempts} attempts."
107+
exit 1
108+
109+
- name: Verify pnpm version
110+
shell: bash
111+
run: pnpm --version

.github/workflows/ci.yml

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -288,11 +288,8 @@ jobs:
288288
with:
289289
node-version: '22'
290290

291-
- name: Enable Corepack
292-
run: corepack enable
293-
294-
- name: Verify pnpm version
295-
run: pnpm --version
291+
- name: Setup pnpm
292+
uses: ./.github/actions/setup-pnpm
296293

297294
- name: Get pnpm store directory
298295
shell: bash
@@ -874,8 +871,8 @@ jobs:
874871
with:
875872
node-version: '22'
876873

877-
- name: Enable Corepack
878-
run: corepack enable
874+
- name: Setup pnpm
875+
uses: ./.github/actions/setup-pnpm
879876

880877
- name: Get pnpm store directory
881878
shell: bash
@@ -1068,11 +1065,8 @@ jobs:
10681065
with:
10691066
node-version: '22'
10701067

1071-
- name: Enable Corepack
1072-
run: corepack enable
1073-
1074-
- name: Verify pnpm version
1075-
run: pnpm --version
1068+
- name: Setup pnpm
1069+
uses: ./.github/actions/setup-pnpm
10761070

10771071
- name: Get pnpm store directory
10781072
shell: bash
@@ -1207,11 +1201,8 @@ jobs:
12071201
with:
12081202
node-version: '22'
12091203

1210-
- name: Enable Corepack
1211-
run: corepack enable
1212-
1213-
- name: Verify pnpm version
1214-
run: pnpm --version
1204+
- name: Setup pnpm
1205+
uses: ./.github/actions/setup-pnpm
12151206

12161207
- name: Get pnpm store directory
12171208
shell: bash
@@ -1401,11 +1392,8 @@ jobs:
14011392
with:
14021393
node-version: '22'
14031394

1404-
- name: Enable Corepack
1405-
run: corepack enable
1406-
1407-
- name: Verify pnpm version
1408-
run: pnpm --version
1395+
- name: Setup pnpm
1396+
uses: ./.github/actions/setup-pnpm
14091397

14101398
- name: Get pnpm store directory
14111399
shell: bash
@@ -1550,11 +1538,8 @@ jobs:
15501538
with:
15511539
node-version: '22'
15521540

1553-
- name: Enable Corepack
1554-
run: corepack enable
1555-
1556-
- name: Verify pnpm version
1557-
run: pnpm --version
1541+
- name: Setup pnpm
1542+
uses: ./.github/actions/setup-pnpm
15581543

15591544
- name: Get pnpm store directory
15601545
shell: bash
@@ -1723,11 +1708,8 @@ jobs:
17231708
with:
17241709
node-version: '22'
17251710

1726-
- name: Enable Corepack
1727-
run: corepack enable
1728-
1729-
- name: Verify pnpm version
1730-
run: pnpm --version
1711+
- name: Setup pnpm
1712+
uses: ./.github/actions/setup-pnpm
17311713

17321714
- name: Get pnpm store directory
17331715
shell: bash

0 commit comments

Comments
 (0)