Skip to content

Commit 2cb5ee1

Browse files
committed
ci: one setup-workspace composite replaces the hand-copied setup sequence
.github/actions/setup-workspace sets Node up from .nvmrc, acquires pnpm through setup-pnpm, restores the pnpm store, runs pnpm install --frozen-lockfile and saves the store on push and schedule runs only, so no pull_request or merge_group run writes a pnpm-store entry into the cache pool. 38 jobs call it; the node-only jobs read .nvmrc directly. Kept spelled out, each with the reason beside it: ci.yml's five ui jobs (pinned by name and text under ui/scripts/__tests__), the governed-surface-guard toolchain (pinned step by step by the queue guard's self-test; now restore-only), release.yml's release-integrity install of the version tree, and scaffold-e2e's registry canary (no checkout, so no .nvmrc). ui-deps saves the store on push and schedule only. check-pnpm-acquisition recognises the composite (two self-test cases); the queue guard's install-chain pin reads actions/cache/restore too. Dependabot reads .github/actions/* as well as the root. Claude-Session: https://claude.ai/code/session_01ADHkmQLxLez3gFZk5XQ3JX Co-authored-by: Claude <noreply@anthropic.com>
1 parent c51aed4 commit 2cb5ee1

34 files changed

Lines changed: 335 additions & 892 deletions

‎.github/actions/setup-pnpm/action.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@
5656
# written. The separation is kept anyway, because the callers' own pins are
5757
# already in place and moving them buys nothing -- ⛔ it is no longer a
5858
# constraint, and a future composition is free to hold one.
59+
#
60+
# `.github/actions/setup-workspace` is that composition: it runs setup-node,
61+
# then this action, then the pnpm store and the install. Most jobs call it;
62+
# the ones that still call this action directly are named in its header.
5963

6064
name: Setup pnpm
6165
description: >-
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
#
3+
# setup-workspace -- the one setup sequence every job that installs the
4+
# workspace runs after its checkout: Node from .nvmrc, the pinned pnpm, the
5+
# pnpm store restored from cache, `pnpm install --frozen-lockfile`, and the
6+
# store saved back on `push` and `schedule` runs only.
7+
#
8+
# Why one place. The same five steps were copied by hand into about forty
9+
# jobs, each carrying its own `node-version` literal and its own copy of the
10+
# store cache key, so a change to any of them was a forty-site edit and a
11+
# site that missed it drifted silently. `scripts/check-node-version.mjs`
12+
# reads `.github/actions/**` as well as the workflows, so the Node pin here is
13+
# audited like any other.
14+
#
15+
# The store is RESTORED on every run and SAVED only on `push` and `schedule`.
16+
# A pull request or merge-queue run that touches the lockfile used to post-save
17+
# a multi-GB store entry under its own key into the same 10 GB Actions cache
18+
# pool that holds `main`'s Turbo seeds, and those entries evicted the seeds.
19+
# `main`'s push and scheduled runs keep the store warm for everyone: a cache
20+
# entry saved on the default branch is restorable from every pull request and
21+
# queue branch, while one saved on a pull request is restorable only from that
22+
# pull request. ⛔ No `pull_request` or `merge_group` run saves the store.
23+
#
24+
# The save is a main-phase step right after the install, not `actions/cache`'s
25+
# post-job save: the store changes only during the install, and a composite
26+
# action has no post phase of its own. It is `continue-on-error` -- a store
27+
# that failed to upload says nothing about the code under test.
28+
#
29+
# Pnpm is acquired through `.github/actions/setup-pnpm` (the cached Corepack
30+
# store), called as a step here rather than inlined, because three callers
31+
# still use it directly and keep their own steps:
32+
# - ci.yml's five ui jobs (`ui-deps`, `type-check-ui`, `test-ui`, `lint-ui`,
33+
# `build-e2e-ui`), whose steps pins under `ui/scripts/__tests__/` read by
34+
# name and by text;
35+
# - governed-surface-guard.yml, whose toolchain steps are each gated and
36+
# each degrade on their own, pinned one by one by
37+
# `scripts/pm/check-governed-queue-guard.mjs --self-test`;
38+
# - release.yml's `release-integrity`, which installs the version commit's
39+
# tree, not the checkout.
40+
#
41+
# Inputs:
42+
# install 'false' stops after pnpm is on PATH: no store, no install.
43+
# For jobs that run pnpm but install nothing.
44+
# install-args appended to `pnpm install --frozen-lockfile`.
45+
# save-store 'false' never saves the store, on any event. For jobs that
46+
# deliberately restore only.
47+
48+
name: Setup workspace
49+
description: >-
50+
Set up Node from .nvmrc and the repo-pinned pnpm, restore the pnpm store,
51+
run `pnpm install --frozen-lockfile`, and save the store on push and
52+
schedule runs only.
53+
54+
inputs:
55+
install:
56+
description: "'false' sets up Node and pnpm only, with no store cache and no install."
57+
required: false
58+
default: 'true'
59+
install-args:
60+
description: Extra arguments appended to `pnpm install --frozen-lockfile`.
61+
required: false
62+
default: ''
63+
save-store:
64+
description: "'false' never saves the pnpm store. Otherwise it is saved on push and schedule runs, on a miss of its exact key."
65+
required: false
66+
default: 'true'
67+
68+
outputs:
69+
store-path:
70+
description: The pnpm store directory (empty when `install` is 'false').
71+
value: ${{ steps.store.outputs.path }}
72+
store-cache-hit:
73+
description: "'true' when the store was restored from its exact key."
74+
value: ${{ steps.restore.outputs.cache-hit }}
75+
76+
runs:
77+
using: composite
78+
steps:
79+
- name: Setup Node.js
80+
uses: actions/setup-node@v7
81+
with:
82+
node-version-file: .nvmrc
83+
# pnpm is not on PATH yet, and a setup-node that caches for the
84+
# `packageManager` it reads shells out to it and dies here
85+
# (partof-closing-keyword-guard.yml carries the receipt). The store is
86+
# cached by the steps below instead.
87+
package-manager-cache: false
88+
89+
- name: Setup pnpm
90+
uses: ./.github/actions/setup-pnpm
91+
92+
- name: Get pnpm store directory
93+
id: store
94+
if: inputs.install != 'false'
95+
shell: bash
96+
run: |
97+
path="$(pnpm store path --silent)"
98+
echo "path=$path" >> "$GITHUB_OUTPUT"
99+
echo "STORE_PATH=$path" >> "$GITHUB_ENV"
100+
101+
# The one definition of the store cache key. `-v3-` and the lockfile hash
102+
# are unchanged from the hand-copied steps, so the entries `main` already
103+
# holds stay valid.
104+
- name: Restore pnpm store
105+
id: restore
106+
if: inputs.install != 'false'
107+
uses: actions/cache/restore@v6
108+
with:
109+
path: ${{ steps.store.outputs.path }}
110+
key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }}
111+
restore-keys: |
112+
${{ runner.os }}-pnpm-store-v3-
113+
114+
- name: Install dependencies
115+
if: inputs.install != 'false'
116+
shell: bash
117+
env:
118+
INSTALL_ARGS: ${{ inputs.install-args }}
119+
run: |
120+
# shellcheck disable=SC2086 # INSTALL_ARGS is a word list on purpose.
121+
pnpm install --frozen-lockfile $INSTALL_ARGS
122+
123+
- name: Save pnpm store
124+
if: >-
125+
inputs.install != 'false'
126+
&& inputs.save-store != 'false'
127+
&& (github.event_name == 'push' || github.event_name == 'schedule')
128+
&& steps.restore.outputs.cache-hit != 'true'
129+
continue-on-error: true
130+
uses: actions/cache/save@v6
131+
with:
132+
path: ${{ steps.store.outputs.path }}
133+
key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }}

‎.github/dependabot.yml‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,13 @@ updates:
2323
- "minor"
2424
- "patch"
2525

26-
# Enable version updates for GitHub Actions
26+
# Enable version updates for GitHub Actions. The composite actions under
27+
# .github/actions/ pin actions of their own (setup-node, actions/cache), and
28+
# Dependabot reads only the directories listed here.
2729
- package-ecosystem: "github-actions"
28-
directory: "/"
30+
directories:
31+
- "/"
32+
- "/.github/actions/*"
2933
schedule:
3034
interval: "monthly"
3135
time: "02:00"

‎.github/workflows/board-snapshot.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ jobs:
206206
- name: Setup Node.js
207207
uses: actions/setup-node@v7
208208
with:
209-
node-version: '22'
209+
node-version-file: .nvmrc
210210

211211
# No `pnpm install`: the archiver imports only `node:` builtins, global
212212
# `fetch`, and two repo-local modules — no npm dependency, so installing

‎.github/workflows/checklist-status.yml‎

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -114,23 +114,15 @@ jobs:
114114
- name: Checkout repository
115115
uses: actions/checkout@v7
116116

117-
# Kept as this job's own step rather than folded into the composite below.
118-
# ⛔ No longer because a gate cannot see it: `scripts/check-node-version.mjs`
119-
# reads `.github/actions/**` too since #19229, so its census follows a
120-
# setup-node step wherever it is written. Kept because the pin is already
121-
# here and moving it buys nothing.
122-
- name: Setup Node.js
123-
uses: actions/setup-node@v7
124-
with:
125-
node-version: '22'
126-
127117
# `pnpm` itself is the requirement, NOT the workspace. The generator is
128118
# invoked through its PACKAGE SCRIPT (`pnpm gen:checklist-status`), which
129119
# runs its `--self-test` first: an inlined `node scripts/...` copy would
130120
# drop that leg, and the whole product of this job is a page full of
131121
# numbers that nothing downstream disagrees with.
132-
- name: Setup pnpm
133-
uses: ./.github/actions/setup-pnpm
122+
- name: Setup workspace
123+
uses: ./.github/actions/setup-workspace
124+
with:
125+
install: 'false'
134126

135127
# No `pnpm install`. The generator imports `node:` builtins and one
136128
# repo-local module — no npm dependency — so installing the workspace

0 commit comments

Comments
 (0)