Skip to content

ci: adopt the setup-pnpm composite action at all 16 remaining Corepack sites - #11758

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-11369-corepack-pnpm-lint-liveness
Aug 24, 2026
Merged

ci: adopt the setup-pnpm composite action at all 16 remaining Corepack sites#11758
os-steve merged 1 commit into
mainfrom
claude/issue-11369-corepack-pnpm-lint-liveness

Conversation

@os-steve

Copy link
Copy Markdown
Collaborator

Fixes#11369

corepack enable only writes shims — the pnpm tarball is fetched from
registry.npmjs.org on the firstpnpm invocation in the job. Every such site is
an independent chance for a network flake to fail a job for a reason unrelated to the
code under test. The setup-pnpm composite action removes that call by restoring the
Corepack store from the actions cache; #11300 introduced it and cleared ci.yml. This
PR applies it to the ten workflows that still carried a Corepack site.

Scope: widened from the card's 2 files to the full population of 10

The card names lint.yml + spec-liveness-check.yml, correctly, for queue-ejection
exposure (7 of the 24 jobs in a merge-queue build). The remaining eight workflows carry
the identical per-job download outside that frame, and three of them change the risk
class
rather than the count: release.yml and cut-rc.yml fail a release rather
than ejecting a re-queueable PR, and publish-smoke.yml turns a network flake into
"the release candidate is bad". Widening was the dispatching PM's call, made explicitly.

Non-vacuity: counts across .github/workflows/, both directions

pinbefore (2a6122bd9)after (5796ff6a3)delta
corepack enable occurrences160−16
files containing corepack enable100−10
uses: ./.github/actions/setup-pnpm723+16 (matching)
name: Verify pnpm version steps80−8 (folded into the composite)
uses: actions/setup-node@ steps32320 — invariant

The −16 and the +16 are the same 16 sites, which is what makes the count a measurement
rather than an artefact of one probe. Controls, each zero paired with a positive:
corepack enable in ci.yml = 0 while Setup pnpm in ci.yml = 14 (the grep is live
and #11300's removal is complete); corepack disable across all workflows = 0 (the probe
is not indiscriminate).

Per-site classification — all 16 sites, in 16 distinct jobs

Every site was read individually rather than assumed uniform. All 16 sit in a job that
runs checkoutsetup-node → Corepack, on ubuntu-latest, with no container:, no
checkout path:/repository:, and no corepack prepare/explicit version activation
anywhere in the tree.

Class A — plain mechanical swap (14 sites). Identical to the shape ci.yml already
landed.

workflowjobpaired Verify pnpm version?
lint.ymllintno
lint.ymltypecheck-source-gatesyes
lint.ymltypecheck-workspaceyes
lint.ymltypecheck-debtyes
lint.ymltypecheck-consumersyes
spec-liveness-check.ymllivenessno
cut-rc.ymlcutyes
release.ymlversion-pryes
release.ymlpublishyes
validate-deps.ymlvalidateyes
publish-smoke.ymlpack-smokeno
coverage-nightly.ymlcoverageno
rerun-safety-nightly.ymlrerun-safetyno
scaffold-e2e.ymlscaffold-localno

Class B — mechanical, but only with the caller's if: carried verbatim (2 sites).
These were the two sites that were not a blind swap:

workflowjobcondition carried
pr-automation.ymlchangeset-checksteps.labels.outputs.skip != 'true' && steps.diffbase.outputs.base_error == '' (a folded >- scalar, kept verbatim)
release.ymlrelease-integritysteps.audit.outputs.releases-missing == 'true'

Both gate a following pnpm install on the same condition, so the composite inherits
exactly the reachability the corepack enable step had. release-integrity's Corepack
step is deliberately deferred to the repair path rather than sitting at the top of the
job; the composite stays in that position and the comment explaining the deferral is
untouched.

Zero sites were left unconverted. Nothing in the population pinned a different pnpm
version, passed different flags, or ran Corepack at a point the composite changes.

Two preconditions checked rather than assumed

  • publish-smoke.yml / pack-smoke checks out a non-default ref
    (needs.resolve.outputs.ref), and uses: ./… resolves from the checked-out tree
    so a ref without the composite would hard-fail. It cannot arise: on workflow_run the
    ref is the open changeset-release/main head, a descendant of main; on
    workflow_dispatch the ref is github.ref, which is also where the workflow file
    itself comes from, so file and tree always agree.
  • coverage-nightly.yml / rerun-safety-nightly.yml use actions/cache/restore@v6
    (restore-only) for their pnpm store.
    The composite embeds a save-capable
    actions/cache@v6 for the Corepack store. Naming it rather than hiding it: the
    restore-only comment is attached to the pnpm-store step and is about that cache, and
    the Corepack entry is keyed on the packageManager pin alone (one small entry
    repo-wide, saved only on a miss — which ci.yml on main will already have filled).

The census constraint the card states, held

Each caller keeps its own actions/setup-node with its literal node-version pin.
scripts/check-node-version.mjs scans .github/workflows/*.yml only and reports how many
setup-node steps it audited; folding those steps into the composite would drop them from
its census while it still printed OK. It still reports 32 setup-node steps across 26
workflows
, byte-identical to the pre-change census.

Verification — gate union at 5796ff6a3, 18/18 green

Gate families derived from the real changeset via
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (10 paths vs merge
base 2a6122bd9), not from a hand-written list. Quoting each gate's own verdict line:

check-node-version: OK (32 setup-node step(s) across 26 workflow(s), all on Node 22).
check-workflow-status-functions: OK (scanned 26 workflow file(s), 49 job(s), 25 job-level
if: expression(s); 10 read needs.*.outputs.*, all naming a status function).
check-step-collectors: 327 `run:` steps across 26 workflow(s) ...
check-aggregator-roster: 3 aggregator(s) across 2 workflow(s); roster == needs: in both
directions, and all 3 required-context aggregate(s) declared.
check-required-contexts: 6 required context name(s) pinned across 2 workflow(s) ...
check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) ...
check-nul-bytes: OK (scanned 6568 text file(s) ... no raw ASCII control bytes).

Those gates parse the workflow files themselves, so their green is a real measurement
over the edited files rather than a bystander pass. All 10 files additionally re-parse as
valid YAML with the expected composite-step count per job.

One family in the derived set, check:type-check-debt (--re-measure), declined to
run
rather than failing: it refuses without a built TypeScript closure, because a number
measured from an unbuilt tree would silently record a different world. This diff contains
zero TypeScript, and its non---re-measure sibling check:type-check-coverage is green.

No changeset: this PR is .github/workflows/** only and releases nothing, so it takes the
skip-changeset route.

Generated by Claude Code


Generated by Claude Code

…k sites
`corepack enable` only writes shims; the pnpm tarball is fetched from
registry.npmjs.org on the first pnpm invocation in the job. Every such site
is an independent chance for a network flake to fail a job for a reason that
has nothing to do with the code under test.
The setup-pnpm composite action removes that call by restoring the Corepack
store from the actions cache. It was introduced for ci.yml, which now has
zero Corepack sites. This applies it to the ten workflows that still had one.
Counts across .github/workflows/ (before -> after):
corepack enable occurrences 16 -> 0 (10 files -> 0)
uses: ./.github/actions/setup-pnpm 7 -> 23 (+16, the matching amount)
name: Verify pnpm version 8 -> 0 (folded into the composite)
uses: actions/setup-node@ 32 -> 32 (unchanged, deliberately)
Every caller keeps its own actions/setup-node step with its literal
node-version pin. scripts/check-node-version.mjs scans .github/workflows/*.yml
only and reports how many setup-node steps it audited; moving those steps into
the composite would drop them from its census while it still printed OK. It
still reports 32 setup-node steps across 26 workflows.
The two sites whose Corepack step was conditional keep their condition
verbatim on the composite step, and stay in position:
pr-automation.yml changeset-check (skip label / diff-base guard)
release.yml release-integrity (deferred to the repair path only)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 24, 2026
@os-steveClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT. Verified by content on origin/main, not by reading the report back.

The population re-measurement holds exactly

probe on mainreading
files with corepack enable under .github/workflows/10
occurrences16
existing uses: ./.github/actions/setup-pnpm callers7
uses: actions/setup-node@ steps32

Same ten files the prior seat named at a872ce9, so the day-old number survived. Their controls still hold too: corepack enable in ci.yml = 0 paired with Setup pnpm in ci.yml = 14 — a live grep with a demonstrated zero, which is what makes the 16 a reading rather than an artefact. Your arithmetic closes: 7 + 16 = 23, and setup-node invariant at 32 both before and after.

The part I asked for and did not expect to get back this cleanly

I flagged "the swap is mechanical and identical at all sites" as the load-bearing assumption behind my widening call, and said plainly I had not verified it. You classified all 16 individually instead of asserting uniformity, and found two that are not a plain swap: pr-automation.yml changeset-check and release.yml release-integrity, where the caller's if: has to be carried verbatim.

That is the finding. A blind find-and-replace across ten files would have dropped those conditions and defeated release-integrity's deliberate mid-job deferral — on the release path, which is the exact exposure I widened the card to cover. So the widening would have introduced, in the release workflow, a bug of the same class it was meant to remove. My call was right and my reasoning for it was one step short of sufficient; the per-site classification is what made it safe.

Two preconditions you checked that I did not think to name

Both are the kind that only bite after landing:

  • publish-smoke.yml/pack-smoke checks out a non-default ref, and uses: ./… resolves from the checked-out tree — so a composite action that exists on main is not automatically the one that runs. You established the ref is always either a descendant of main (workflow_run) or the same ref the workflow file itself came from (workflow_dispatch). Had that not held, two sites would have silently referenced a path that may not exist.
  • The two nightlies use restore-only caching for their pnpm store while the composite embeds a save-capable cache. You named it in the PR body with its cost (droppable at 2 of 16) rather than burying it. That is a convention question for whoever owns the nightly cache budget, and putting it where they can see it is the right disposition.

#11765 is the better half of this card's result

You filed that showcase-smoke.yml:22 acquires pnpm via pnpm/action-setup@v6 — verified on main, one occurrence, exactly there. A third acquisition path invisible to a corepack enable-keyed census. That matters more than the count: after this PR the census reads 0, and 0 reads as "class closed" when one workflow still downloads pnpm per job. A metric that goes to zero while the thing it measures survives is worse than no metric.

And you were right not to fold it in. That job relies on setup-node's cache: pnpm, which needs pnpm on PATHbeforesetup-node — the opposite ordering from every setup-pnpm caller. Converting it would not have been a find-and-replace, and doing it inside a 10-file mechanical PR is how a careful change acquires a careless rider.

Gates

The green here is a measurement over the edited files rather than a bystander pass, which is worth stating: check-node-version ("32 setup-node step(s) across 26 workflow(s)"), check-workflow-status-functions, check-step-collectors, check-aggregator-roster, check-required-contexts, check-shard-attestation all parse the workflow files themselves. And check:type-check-debtdeclining to run without a built closure — reported as a declared narrowing rather than dressed as a pass — is the honest handling.

The first-run ERR_MODULE_NOT_FOUND on six gates is the fresh-worktree trap, correctly called an artefact and not a verdict. That is now the third card this shift to hit it; it is #11557's subject and the recurrence is becoming its own argument.

⚠️Sequencing note for me, not a change request: #11759 also edits .github/workflows/lint.yml (adding a docs-guard step). Different region, so it should auto-merge, but I am watching the single-writer-path check on both and will sequence them if it reds.

Flipping to ready. Arming once every check is green.


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review August 24, 2026 15:46
@os-steve
os-steve added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit 006c181Aug 24, 2026
35 checks passed
@os-steve
os-steve deleted the claude/issue-11369-corepack-pnpm-lint-liveness branch August 24, 2026 16:11
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cdsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-steve@claude