Skip to content

Commit d15f99d

Browse files
ci: one setup-workspace composite action replaces the setup steps copied into 38 jobs, and pull requests stop saving the pnpm store (#26419)
<!-- claude-projects-attribution: {"github_login":"os-steve"} --> _Requested by **Steve** · [project thread](https://claude.ai/code/project/chan_01FdL1XeUyvVNxLreh3F4Qi9?thread=cmsg_01FdL1XeUyvVNxLreh3F4Qi9HTCCgQfvL4bghGe5pCNu1u)_ Fixes #26083 Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:新建 `.github/actions/setup-workspace` 组合 action(Node 读 .nvmrc、pnpm、store 缓存、`pnpm install --frozen-lockfile`),38 个 job 手抄的 setup 串换成它一步;只装 Node 的 job 改读 .nvmrc。 - 为什么改:同一串五步手抄在约 40 个 job 里,改一次要改 40 处;pnpm store 缓存在 pull_request 上也会存,挤占 10 GB 缓存池、把 main 的 Turbo 种子挤掉。现在只有 push 和 schedule 存。 - 风险与回滚:纯 CI 配置,不发布任何包;出问题整 PR revert 即可。风险在 store 改为 push/schedule 才存后,PR 首跑可能更常冷装(读 main 的 store 不受影响)。 - 先看:`.github/actions/setup-workspace/action.yml`(保存条件那一步)、`.github/workflows/release.yml` 的 publish job(组合 action 放在 Guard 之后)、`.github/workflows/ci.yml` 的 ui-deps 保存条件。 - 两分钟验证:在本 PR 的 CI 任一 job 日志里展开 "Setup workspace",应看到 "Save pnpm store" 被跳过;`grep -c "node-version: '22'" .github/workflows/*.yml` 只剩 scaffold-e2e.yml 一处。 - 需要你做的:受管面(`.github/workflows/**`),请 GOVERNED_APPROVERS 审批后由席位落地。 ## What changes - **New composite `.github/actions/setup-workspace/action.yml`.** `actions/setup-node` with `node-version-file: .nvmrc` (and `package-manager-cache: false`, since pnpm is not on PATH yet), then `./.github/actions/setup-pnpm` as a step, `pnpm store path`, `actions/cache/restore` on the one store key, `pnpm install --frozen-lockfile`, and `actions/cache/save` only when `github.event_name` is `push` or `schedule` and the exact key missed (`continue-on-error: true`). Inputs: `install: 'false'` (Node and pnpm only, for the jobs that run pnpm but install nothing), `install-args` (validate-deps keeps `--prefer-offline`), `save-store: 'false'` (the jobs that were deliberately restore-only stay so). - **38 jobs call it** in place of the hand-copied steps, `showcase-smoke.yml` included: its `pnpm/action-setup` and setup-node `cache: pnpm` are gone, so the acquisition census now reads 0 uncached sites. In release.yml's `publish` job the composite sits where `Setup pnpm` was, after the Guard step, so nothing is installed before the Guard has passed; that job keeps its own setup-node step (now `.nvmrc`). - **Jobs that need Node and nothing else** keep their own setup-node step, now `node-version-file: .nvmrc`. - **Kept spelled out, each with its reason written beside it:** - ci.yml's five ui jobs (`ui-deps`, `type-check-ui`, `test-ui`, `lint-ui`, `build-e2e-ui`). Pins under `ui/scripts/__tests__/` read those steps by text and by name (see Acceptance notes); this PR does not touch `ui/**`. They switch to `node-version-file: .nvmrc`, and `ui-deps`' `Save pnpm cache` now also requires `push` or `schedule`. - governed-surface-guard.yml: every toolchain step is gated on the toolchain question and degrades on its own, pinned one by one by `check-governed-queue-guard.mjs --self-test`. Its store step becomes `actions/cache/restore` (the workflow runs on `pull_request` and `merge_group` only, so it never saves now), and its Node pin reads `.nvmrc`. - release.yml `release-integrity`: installs the version commit's tree, not the checkout. - scaffold-e2e.yml `registry-canary`: checks nothing out, so there is no `.nvmrc` to read; the one literal `node-version: '22'` left, still held to `.nvmrc` by `check:node-version`. - **setup-pnpm is not retired**: it becomes a step of the composite, because the ui pins and the two jobs above still call it by path. - **Gates that read these steps by text**: `scripts/check-pnpm-acquisition.mjs` recognises `uses: ./.github/actions/setup-workspace` as a cached mechanism (MECHANISMS row, header spelling list, two new self-test cases: recognised, and late acquisition still fails; battery floor 12 to 14). `scripts/pm/check-governed-queue-guard.mjs`'s install-chain pin matches `actions/cache/restore@` as well as `actions/cache@`. No other behaviour changes in either. - `.github/dependabot.yml`: the github-actions entry reads `directories: ["/", "/.github/actions/*"]`. - TURBO_* env lines are unchanged and stay per step: hoisting them to job level would hand `TURBO_TOKEN` to every step of the job. `fetch-depth`, the Turbo seed keys, the Next.js cache and every job name are unchanged. ## Tests The diff edits two gate scripts, so each one's own self-test ran, plus the derived family list (Verification). - `node scripts/check-pnpm-acquisition.mjs --self-test`: `14 cases pass`; `pnpm check:pnpm-acquisition`: `OK`, census `45 acquisition site(s), 0 of them uncached` (38 workspace, 7 setup-pnpm, 0 action-setup). - `node scripts/pm/check-governed-queue-guard.mjs --self-test`: `393 cases pass`, exit 0. ## Acceptance notes Counts re-taken on `origin/main` at c51aed4 before the edit and on this head after (`.github/workflows/*.yml`): | reading | main | this PR | |---|---|---| | `node-version: '22'` | 60 (59 steps + 1 comment) | 2 (registry-canary + the same comment in half-state-patrol.yml) | | store key `pnpm-store-v3-${{ hashFiles(...) }}` | 40 | 7 (the ui jobs' 6 + governed-surface-guard), plus the composite's restore/save pair | | `pnpm install --frozen-lockfile` | 43 lines | 7 (5 ui jobs, governed-surface-guard, release-integrity's version-tree script), plus the composite | | `uses: ./.github/actions/setup-workspace` | 0 | 38 | The ui pins that hold the five ui jobs' steps in place (for the ui lane to move onto the composite later; none edited here): - `ui/scripts/__tests__/ci-setup-pnpm-wiring.test.ts`: each ui job must contain the text `uses: ./.github/actions/setup-pnpm` before its first pnpm. - `ui/scripts/__tests__/turbo-cache-save-bound.test.ts`: `ui-deps` saves the store through its own bounded `actions/cache/save` step paired with a restore that has an `id`. - `pnpm install --frozen-lockfile` located by text in the job block: `check-action-forward-parity`, `check-i18n-call-site-keys`, `check-i18n-designer-table-parity`, `check-i18n-en-drift`, `check-package-self-import`, `check-phantom-dependencies`, `check-unreferenced-sources`, `check-vi-mock-override-shape` (all `ui/scripts/__tests__/*.test.ts`). The second Acceptance item (no `pnpm-store-*` save on a `pull_request` run) is read from this PR's own CI run and reported on the card. ## Verification Gate families derived by `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at the head below, each exit captured before any pipe, reconciled with `--ran`. Head `2cb5ee1f`, merge base `c51aed4f`. - `dispatch-gates --ran`: `94 derived famil(ies) accounted for — 94 run, 0 NOT-MEASURED (a DERIVED zero — all 94 recorded an exit code and none of them is 3)`. All 94 exit 0. - Four families first answered exit 3 (no built packages in a fresh container: `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:sourcemap-no-sources-content`) and `check:type-check-debt` hit the loop's 300 s cap while it built. After `turbo run build --filter='./packages/**'` under `os-verify-lock.sh` (`VERDICT command-exit 0`, 75/75 tasks) all five re-ran to exit 0. - `pnpm check:pm-dispatch-gates`: `1901 cases pass` (fast tier), exit 0. - `pnpm check:node-version`: `OK (23 setup-node step(s) across 39 workflow(s) and 3 composite action(s), all on Node 22)`. - Not derived, run because the diff edits root `ci.yml`'s ui jobs: every `ui/scripts/__tests__/*.test.ts` that reads the root workflows (41 files, `vitest run --project unit`): `Test Files 41 passed (41)`, `Tests 1284 passed (1284)`. - Not run locally: the repo-wide scans CI owns (`pnpm lint` and the rest of `Lint & Repo Gates`); this PR's check-runs are their reading. --- _Generated by [Claude Code](https://claude.ai/code/session_01ADHkmQLxLez3gFZk5XQ3JX)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9494dd8 commit d15f99d

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)