diff --git a/.github/actions/setup-pnpm/action.yml b/.github/actions/setup-pnpm/action.yml index 30ef22c1bbe..84fca9bb30c 100644 --- a/.github/actions/setup-pnpm/action.yml +++ b/.github/actions/setup-pnpm/action.yml @@ -56,6 +56,10 @@ # written. The separation is kept anyway, because the callers' own pins are # already in place and moving them buys nothing -- ⛔ it is no longer a # constraint, and a future composition is free to hold one. +# +# `.github/actions/setup-workspace` is that composition: it runs setup-node, +# then this action, then the pnpm store and the install. Most jobs call it; +# the ones that still call this action directly are named in its header. name: Setup pnpm description: >- diff --git a/.github/actions/setup-workspace/action.yml b/.github/actions/setup-workspace/action.yml new file mode 100644 index 00000000000..f5e70846c9a --- /dev/null +++ b/.github/actions/setup-workspace/action.yml @@ -0,0 +1,133 @@ +# Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +# +# setup-workspace -- the one setup sequence every job that installs the +# workspace runs after its checkout: Node from .nvmrc, the pinned pnpm, the +# pnpm store restored from cache, `pnpm install --frozen-lockfile`, and the +# store saved back on `push` and `schedule` runs only. +# +# Why one place. The same five steps were copied by hand into about forty +# jobs, each carrying its own `node-version` literal and its own copy of the +# store cache key, so a change to any of them was a forty-site edit and a +# site that missed it drifted silently. `scripts/check-node-version.mjs` +# reads `.github/actions/**` as well as the workflows, so the Node pin here is +# audited like any other. +# +# The store is RESTORED on every run and SAVED only on `push` and `schedule`. +# A pull request or merge-queue run that touches the lockfile used to post-save +# a multi-GB store entry under its own key into the same 10 GB Actions cache +# pool that holds `main`'s Turbo seeds, and those entries evicted the seeds. +# `main`'s push and scheduled runs keep the store warm for everyone: a cache +# entry saved on the default branch is restorable from every pull request and +# queue branch, while one saved on a pull request is restorable only from that +# pull request. ⛔ No `pull_request` or `merge_group` run saves the store. +# +# The save is a main-phase step right after the install, not `actions/cache`'s +# post-job save: the store changes only during the install, and a composite +# action has no post phase of its own. It is `continue-on-error` -- a store +# that failed to upload says nothing about the code under test. +# +# Pnpm is acquired through `.github/actions/setup-pnpm` (the cached Corepack +# store), called as a step here rather than inlined, because three callers +# still use it directly and keep their own steps: +# - ci.yml's five ui jobs (`ui-deps`, `type-check-ui`, `test-ui`, `lint-ui`, +# `build-e2e-ui`), whose steps pins under `ui/scripts/__tests__/` read by +# name and by text; +# - governed-surface-guard.yml, whose toolchain steps are each gated and +# each degrade on their own, pinned one by one by +# `scripts/pm/check-governed-queue-guard.mjs --self-test`; +# - release.yml's `release-integrity`, which installs the version commit's +# tree, not the checkout. +# +# Inputs: +# install 'false' stops after pnpm is on PATH: no store, no install. +# For jobs that run pnpm but install nothing. +# install-args appended to `pnpm install --frozen-lockfile`. +# save-store 'false' never saves the store, on any event. For jobs that +# deliberately restore only. + +name: Setup workspace +description: >- + Set up Node from .nvmrc and the repo-pinned pnpm, restore the pnpm store, + run `pnpm install --frozen-lockfile`, and save the store on push and + schedule runs only. + +inputs: + install: + description: "'false' sets up Node and pnpm only, with no store cache and no install." + required: false + default: 'true' + install-args: + description: Extra arguments appended to `pnpm install --frozen-lockfile`. + required: false + default: '' + save-store: + description: "'false' never saves the pnpm store. Otherwise it is saved on push and schedule runs, on a miss of its exact key." + required: false + default: 'true' + +outputs: + store-path: + description: The pnpm store directory (empty when `install` is 'false'). + value: ${{ steps.store.outputs.path }} + store-cache-hit: + description: "'true' when the store was restored from its exact key." + value: ${{ steps.restore.outputs.cache-hit }} + +runs: + using: composite + steps: + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc + # pnpm is not on PATH yet, and a setup-node that caches for the + # `packageManager` it reads shells out to it and dies here + # (partof-closing-keyword-guard.yml carries the receipt). The store is + # cached by the steps below instead. + package-manager-cache: false + + - name: Setup pnpm + uses: ./.github/actions/setup-pnpm + + - name: Get pnpm store directory + id: store + if: inputs.install != 'false' + shell: bash + run: | + path="$(pnpm store path --silent)" + echo "path=$path" >> "$GITHUB_OUTPUT" + echo "STORE_PATH=$path" >> "$GITHUB_ENV" + + # The one definition of the store cache key. `-v3-` and the lockfile hash + # are unchanged from the hand-copied steps, so the entries `main` already + # holds stay valid. + - name: Restore pnpm store + id: restore + if: inputs.install != 'false' + uses: actions/cache/restore@v6 + with: + path: ${{ steps.store.outputs.path }} + key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} + restore-keys: | + ${{ runner.os }}-pnpm-store-v3- + + - name: Install dependencies + if: inputs.install != 'false' + shell: bash + env: + INSTALL_ARGS: ${{ inputs.install-args }} + run: | + # shellcheck disable=SC2086 # INSTALL_ARGS is a word list on purpose. + pnpm install --frozen-lockfile $INSTALL_ARGS + + - name: Save pnpm store + if: >- + inputs.install != 'false' + && inputs.save-store != 'false' + && (github.event_name == 'push' || github.event_name == 'schedule') + && steps.restore.outputs.cache-hit != 'true' + continue-on-error: true + uses: actions/cache/save@v6 + with: + path: ${{ steps.store.outputs.path }} + key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2e276ab53c9..bcd160b24f9 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -23,9 +23,13 @@ updates: - "minor" - "patch" - # Enable version updates for GitHub Actions + # Enable version updates for GitHub Actions. The composite actions under + # .github/actions/ pin actions of their own (setup-node, actions/cache), and + # Dependabot reads only the directories listed here. - package-ecosystem: "github-actions" - directory: "/" + directories: + - "/" + - "/.github/actions/*" schedule: interval: "monthly" time: "02:00" diff --git a/.github/workflows/board-snapshot.yml b/.github/workflows/board-snapshot.yml index 90a0d5d559a..6352e8f98e7 100644 --- a/.github/workflows/board-snapshot.yml +++ b/.github/workflows/board-snapshot.yml @@ -206,7 +206,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: .nvmrc # No `pnpm install`: the archiver imports only `node:` builtins, global # `fetch`, and two repo-local modules — no npm dependency, so installing diff --git a/.github/workflows/checklist-status.yml b/.github/workflows/checklist-status.yml index 6cc537d62ce..f6dd1230c1d 100644 --- a/.github/workflows/checklist-status.yml +++ b/.github/workflows/checklist-status.yml @@ -114,23 +114,15 @@ jobs: - name: Checkout repository uses: actions/checkout@v7 - # Kept as this job's own step rather than folded into the composite below. - # ⛔ No longer because a gate cannot see it: `scripts/check-node-version.mjs` - # reads `.github/actions/**` too since #19229, so its census follows a - # setup-node step wherever it is written. Kept because the pin is already - # here and moving it buys nothing. - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - # `pnpm` itself is the requirement, NOT the workspace. The generator is # invoked through its PACKAGE SCRIPT (`pnpm gen:checklist-status`), which # runs its `--self-test` first: an inlined `node scripts/...` copy would # drop that leg, and the whole product of this job is a page full of # numbers that nothing downstream disagrees with. - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm + - name: Setup workspace + uses: ./.github/actions/setup-workspace + with: + install: 'false' # No `pnpm install`. The generator imports `node:` builtins and one # repo-local module — no npm dependency — so installing the workspace diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1f1a2db094..b1014f87af2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -546,30 +546,11 @@ jobs: # the PR's merge base, the queue's base_sha or the push's before sha. fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - # Restore only: the `test` shards save this key. - - name: Restore pnpm cache - uses: actions/cache/restore@v6 + - name: Setup workspace + uses: ./.github/actions/setup-workspace with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - - - name: Install dependencies - run: pnpm install --frozen-lockfile + save-store: 'false' # Its own self-test, where it runs (check:self-test-wired). Milliseconds. # A red here fails this job before it publishes a plan, so the matrix @@ -794,26 +775,8 @@ jobs: # Full history so `turbo --affected` can diff against the PR base. fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- + - name: Setup workspace + uses: ./.github/actions/setup-workspace # Restore-only on PRs: PR-side saves (~5 turbo entries per push) churned # the repo's 10 GB Actions cache pool and evicted the main-branch seeds — @@ -834,9 +797,6 @@ jobs: ${{ runner.os }}-turbo-${{ github.job }}-${{ matrix.shard }}- ${{ runner.os }}-turbo-${{ github.job }}- - - name: Install dependencies - run: pnpm install --frozen-lockfile - # PRs and merge-queue builds: only test packages affected by the diff — # against the PR base on pull_request, against the group's own # `base_sha` on merge_group (#16453). spec sits at the root of the @@ -2077,26 +2037,8 @@ jobs: docker exec ${{ job.services.mysql.id }} mysql -uroot -proot -e "SET GLOBAL time_zone = '+08:00'" docker exec ${{ job.services.mysql.id }} mysql -uroot -proot -N -e "SELECT @@global.time_zone" - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- + - name: Setup workspace + uses: ./.github/actions/setup-workspace # Restore-only (same policy as every other job); falls back to the Build # Core namespace because that job builds every package (a superset of the @@ -2113,9 +2055,6 @@ jobs: ${{ runner.os }}-turbo-build-core-${{ github.ref_name }}- ${{ runner.os }}-turbo-build-core- - - name: Install dependencies - run: pnpm install --frozen-lockfile - # Turbo remote cache (#22077), on all four of this job's build steps: the # four TURBO_* lines are Build Core's, verbatim, and Build Core's "Turbo # remote cache (#21186)" comment holds the write rule, the signing and the @@ -2376,26 +2315,8 @@ jobs: - name: Checkout repository uses: actions/checkout@v7 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- + - name: Setup workspace + uses: ./.github/actions/setup-workspace # Shard-scoped key: the turbo test hash differs per shard (the task-declared # env `OS_TEST_SHARD` is part of the task hash). Restore-only on PRs — see the Restore @@ -2414,9 +2335,6 @@ jobs: ${{ runner.os }}-turbo-${{ github.job }}-${{ matrix.shard }}- ${{ runner.os }}-turbo-${{ github.job }}- - - name: Install dependencies - run: pnpm install --frozen-lockfile - # ⛔ THE DOGFOOD PACKAGE'S DEPENDENCY CLOSURE IS BUILT HERE, IN A RUN # THAT CARRIES NO PASSTHROUGH. (#20820: the next step no longer needs this # for correctness -- it carries its shard in the task env `OS_TEST_SHARD`, @@ -2612,26 +2530,8 @@ jobs: - name: Checkout repository uses: actions/checkout@v7 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- + - name: Setup workspace + uses: ./.github/actions/setup-workspace # Restore-only, with no save step at all: the # job's only build is the CLI closure, and the build-core fallbacks are @@ -2648,9 +2548,6 @@ jobs: ${{ runner.os }}-turbo-build-core-${{ github.ref_name }}- ${{ runner.os }}-turbo-build-core- - - name: Install dependencies - run: pnpm install --frozen-lockfile - # Build the CLI plus BOTH example apps' full dependency closures (the # `pkg...` filter form). When this pass rode dogfood shard 1 the whole # workspace was already built by the test step, so `--filter=@objectstack/cli` @@ -2820,26 +2717,8 @@ jobs: - name: Checkout repository uses: actions/checkout@v7 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- + - name: Setup workspace + uses: ./.github/actions/setup-workspace # Restore-only on PRs: PR-side saves (~5 turbo entries per push) churned # the repo's 10 GB Actions cache pool and evicted the main-branch seeds — @@ -2855,9 +2734,6 @@ jobs: ${{ runner.os }}-turbo-${{ github.job }}-${{ github.ref_name }}- ${{ runner.os }}-turbo-${{ github.job }}- - - name: Install dependencies - run: pnpm install --frozen-lockfile - # 67 package build commands end with `node …/scripts/check-dts-emitted.mjs`, # so the `pnpm build` below runs that checker on every package it builds, # and until this step no workflow ran its `--self-test`. It surfaced when @@ -3127,27 +3003,9 @@ jobs: - name: Checkout repository uses: actions/checkout@v7 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm + - name: Setup workspace + uses: ./.github/actions/setup-workspace - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - - name: Setup Next.js cache uses: actions/cache@v6 with: @@ -3156,9 +3014,6 @@ jobs: restore-keys: | ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- - - name: Install dependencies - run: pnpm install --frozen-lockfile - # ── This gate builds what SHIPS, not a hand-copied lookalike (#10642) ── # # This step used to run `pnpm --filter @objectstack/docs build`: a SECOND, @@ -3285,8 +3140,11 @@ jobs: # `ui/scripts/__tests__/turbo-cache-save-bound.test.ts` pins all of it. # # The pnpm store is RESTORED by every ui job and SAVED by `ui-deps` alone, on - # a miss of its exact key: twenty jobs racing to upload one store under one - # key reserve it once and fail nineteen times. + # a miss of its exact key and on `push` and `schedule` runs only: twenty jobs + # racing to upload one store under one key reserve it once and fail nineteen + # times. These five jobs keep the setup steps spelled out instead of calling + # `.github/actions/setup-workspace`, because pins under `ui/scripts/__tests__/` + # read those steps by name and by text. # ── Build UI Deps: the seven packages, built once per run (#25869) ─────── # Each ui job used to build the seven @objectstack packages itself. Measured @@ -3343,7 +3201,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: .nvmrc - name: Setup pnpm uses: ./.github/actions/setup-pnpm @@ -3438,8 +3296,13 @@ jobs: path: .turbo/cache key: ${{ runner.os }}-turbo-${{ github.job }}-${{ github.ref_name }}-${{ github.sha }} + # On `push` and `schedule` only, the rule `.github/actions/setup-workspace` + # holds for every other job: a pull request's or queue entry's store is + # restorable only from that ref and evicts `main`'s seeds from the pool. - name: Save pnpm cache - if: steps.pnpm-cache.outputs.cache-hit != 'true' + if: >- + (github.event_name == 'push' || github.event_name == 'schedule') + && steps.pnpm-cache.outputs.cache-hit != 'true' uses: actions/cache/save@v6 timeout-minutes: 5 continue-on-error: true @@ -3474,7 +3337,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: .nvmrc - name: Setup pnpm uses: ./.github/actions/setup-pnpm @@ -3732,7 +3595,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: .nvmrc - name: Setup pnpm uses: ./.github/actions/setup-pnpm @@ -3903,7 +3766,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: .nvmrc - name: Setup pnpm uses: ./.github/actions/setup-pnpm @@ -4225,7 +4088,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: .nvmrc - name: Setup pnpm uses: ./.github/actions/setup-pnpm diff --git a/.github/workflows/closing-target-claim-guard.yml b/.github/workflows/closing-target-claim-guard.yml index 487d19aa639..023b93d7572 100644 --- a/.github/workflows/closing-target-claim-guard.yml +++ b/.github/workflows/closing-target-claim-guard.yml @@ -111,7 +111,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: .nvmrc # Everything reaches the script through `env:`, never through `${{ }}` # inside the `run:` line. A PR body is arbitrary author-controlled text, diff --git a/.github/workflows/coverage-nightly.yml b/.github/workflows/coverage-nightly.yml index 71eaefabf36..07ab618f6f4 100644 --- a/.github/workflows/coverage-nightly.yml +++ b/.github/workflows/coverage-nightly.yml @@ -23,31 +23,12 @@ jobs: - name: Checkout repository uses: actions/checkout@v7 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - # Restore-only: scheduled runs read main's store cache; the per-push # workflows own saving it. - - name: Restore pnpm cache - uses: actions/cache/restore@v6 + - name: Setup workspace + uses: ./.github/actions/setup-workspace with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - - - name: Install dependencies - run: pnpm install --frozen-lockfile + save-store: 'false' # The spec package sits at the root of the workspace graph (no workspace # deps) and its suite reads src/ directly, so no build step is needed — diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index d1320f414c0..c690aeaedc1 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -129,34 +129,13 @@ jobs: - name: Checkout repository uses: actions/checkout@v7 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> "$GITHUB_ENV" - # The same key ci.yml's jobs use, so a scheduled run restores the store # CI already saved for this lockfile instead of downloading it again. - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - # `vercel build` runs vercel.json's `installCommand` again; with the tree # already installed from the frozen lockfile that pass has nothing to do, # and a lockfile problem fails HERE, under its own name. - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup workspace + uses: ./.github/actions/setup-workspace # ## Every Vercel command runs from the REPOSITORY ROOT, not apps/docs # diff --git a/.github/workflows/docs-drift-check.yml b/.github/workflows/docs-drift-check.yml index 804813f1e83..60368cd73b2 100644 --- a/.github/workflows/docs-drift-check.yml +++ b/.github/workflows/docs-drift-check.yml @@ -95,7 +95,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: .nvmrc - name: Fetch base branch run: git fetch --no-tags origin "${{ github.base_ref }}" diff --git a/.github/workflows/fleet-write.yml b/.github/workflows/fleet-write.yml index ad585a79022..4f22f116fbf 100644 --- a/.github/workflows/fleet-write.yml +++ b/.github/workflows/fleet-write.yml @@ -78,7 +78,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: .nvmrc # Everything reaches the scripts through `env:`, never through `${{ }}` # inside a `run:` line. A payload is seat-authored text, and an diff --git a/.github/workflows/governed-surface-guard.yml b/.github/workflows/governed-surface-guard.yml index f6f5b067ac4..aa82be04830 100644 --- a/.github/workflows/governed-surface-guard.yml +++ b/.github/workflows/governed-surface-guard.yml @@ -130,18 +130,17 @@ jobs: .github # Pinned to the same major and spelling as every other setup-node in this - # repo, and it keeps its literal `node-version` (rather than - # `node-version-file`) for the measured reason the Part-of Closing-Keyword - # Guard records: `actions/setup-node@v5`+ defaults - # `package-manager-cache: true`, which shells out to pnpm and hard-errors - # before any `run:` line when pnpm is not yet on PATH. This job now DOES - # acquire pnpm, but strictly after this step — the same order lint.yml has - # been green on for months — so nothing here may start depending on pnpm - # existing yet. + # repo, reading `.nvmrc` like `.github/actions/setup-workspace` does. The + # Part-of Closing-Keyword Guard records why `node-version-file` once died + # here: `actions/setup-node@v5` defaulted `package-manager-cache: true`, + # which shells out to pnpm and hard-errors when pnpm is not yet on PATH. + # That guard runs this same v7 with `node-version-file` and no pnpm at + # all, green. This job DOES acquire pnpm, but strictly after this step, + # so nothing here may start depending on pnpm existing yet. - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: .nvmrc # The self-test runs FIRST and in its own step, because it is the # precondition for trusting the run after it: this guard's predicates @@ -232,11 +231,16 @@ jobs: run: | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - # Deliberately the same key as lint.yml's: one store, one entry, and this - # job is a cache READER in the steady state rather than a second writer. - - name: Setup pnpm cache + # Deliberately the same key as `.github/actions/setup-workspace`'s: one + # store, one entry. This job only READS it -- it runs on `pull_request` + # and `merge_group` alone, and no run on either event saves the store. + # The chain stays spelled out here rather than calling that composite: + # `check-governed-queue-guard.mjs --self-test` pins every toolchain step + # one by one, each gated on the question above and each degrading on + # its own. + - name: Restore pnpm cache if: steps.toolchain.outputs.needed != 'false' - uses: actions/cache@v6 + uses: actions/cache/restore@v6 continue-on-error: true with: path: ${{ env.STORE_PATH }} diff --git a/.github/workflows/half-state-patrol.yml b/.github/workflows/half-state-patrol.yml index c206e1bccab..76386f8ab3a 100644 --- a/.github/workflows/half-state-patrol.yml +++ b/.github/workflows/half-state-patrol.yml @@ -259,7 +259,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: .nvmrc # No `pnpm install`: the sweeper imports only `node:` builtins # (`process`, `child_process`, `fs`, `url`), global `fetch`, and one diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 31b24e95f72..f300f6dff1f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -459,36 +459,8 @@ jobs: # check would degrade to "not verified" on every run. fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - # No "migration registry matches its entry files" step since #22554: - # `packages/spec/src/migrations/registry.ts` is git-ignored and generated - # whole from `registry.ts.template` + `entries/` by `gen:migration-registry`, - # which `pnpm install` above already ran (spec's `prepare`, self-test - # included). There is no committed copy left to compare, and an entry the - # generator refuses fails that install step, loudly, in every job. + - name: Setup workspace + uses: ./.github/actions/setup-workspace # Enforces the no-restricted-imports guard against @objectstack/spec root # namespace imports (the dormant rule was never run in CI). Syntactic @@ -566,29 +538,8 @@ jobs: # `fetch-depth: 0`: same reason as the `lint-eslint` checkout above. fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup workspace + uses: ./.github/actions/setup-workspace # #7484 docs anchors: `lychee.toml` sets `include_fragments = "none"`, so # the `Check Documentation Links` job resolves a link's FILE and never its @@ -2007,29 +1958,8 @@ jobs: # `fetch-depth: 0`: same reason as the `lint-eslint` checkout above. fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup workspace + uses: ./.github/actions/setup-workspace # Rationale: `scripts/check-docs-single-h1.mjs` header (#12236, #12249, #12250, # #4690). @@ -3118,29 +3048,8 @@ jobs: # `fetch-depth: 0`: same reason as the `lint-eslint` checkout above. fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup workspace + uses: ./.github/actions/setup-workspace # Lockstep package-count guard (#17039). `.changeset/config.json`'s # `fixed` array is the one true count of packages one stray `major` @@ -3299,8 +3208,9 @@ jobs: # moving onto .github/actions/setup-pnpm, and when that finishes a grep # for `corepack enable` returns 0 — a true answer about Corepack that # reads as a complete answer about pnpm acquisition. It is not one: - # showcase-smoke.yml acquires pnpm through `pnpm/action-setup`, which - # contains no Corepack string and is invisible to that key. A metric that + # showcase-smoke.yml acquired pnpm through `pnpm/action-setup`, which + # contains no Corepack string and is invisible to that key, until it + # moved onto .github/actions/setup-workspace. A metric that # hits zero while its subject survives is worse than no metric, because it # retires the attention. # @@ -4274,29 +4184,8 @@ jobs: # `fetch-depth: 0`: same reason as the `lint-eslint` checkout above. fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup workspace + uses: ./.github/actions/setup-workspace # ── Gate-family selection (#16496) ─────────────────────────────────── # After #16453 this job is the merge queue's critical path (median 1468 s @@ -4451,29 +4340,8 @@ jobs: # `fetch-depth: 0`: same reason as the `lint-eslint` checkout above. fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup workspace + uses: ./.github/actions/setup-workspace # Gate-family selection (#16496) — the same step, once per lane that carries a # scoped step: a step `if:` reads its OWN job's step outputs, so each such lane @@ -4616,29 +4484,8 @@ jobs: # `fetch-depth: 0`: same reason as the `lint-eslint` checkout above. fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup workspace + uses: ./.github/actions/setup-workspace # Gate-family selection (#16496) — the same step, once per lane that carries a # scoped step: a step `if:` reads its OWN job's step outputs, so each such lane @@ -4825,29 +4672,8 @@ jobs: # `fetch-depth: 0`: same reason as the `lint-eslint` checkout above. fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup workspace + uses: ./.github/actions/setup-workspace # Gate-family selection (#16496) — the same step, once per lane that carries a # scoped step: a step `if:` reads its OWN job's step outputs, so each such lane @@ -5040,26 +4866,8 @@ jobs: # and degrades this gate to a false RED on an innocent PR. fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- + - name: Setup workspace + uses: ./.github/actions/setup-workspace # This job runs the full workspace build below ("Build workspace # packages"); without a restored turbo cache that step rebuilt every @@ -5114,9 +4922,6 @@ jobs: ${{ runner.os }}-turbo-build-core-${{ github.ref_name }}- ${{ runner.os }}-turbo-build-core- - - name: Install dependencies - run: pnpm install --frozen-lockfile - # Coverage meta-gate (#4311): 66 of 77 workspace packages build with tsup # (esbuild transpile, no type check) and `vitest run` does not type-check # either, so a package without a `typecheck` script has NO tsc reading its @@ -5703,26 +5508,8 @@ jobs: # #6356). fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- + - name: Setup workspace + uses: ./.github/actions/setup-workspace # Same note as `typecheck-source-gates`' Restore Turbo cache step (#5401). ⚠️ The # namespace is the literal `typecheck`, never `github.job`. @@ -5737,9 +5524,6 @@ jobs: ${{ runner.os }}-turbo-build-core-${{ github.ref_name }}- ${{ runner.os }}-turbo-build-core- - - name: Install dependencies - run: pnpm install --frozen-lockfile - # Example apps are AI-authoring reference templates; a red typecheck is a # bad signal to copy from. tsup transpiles them without a full typecheck, # so build alone will not catch type drift — typecheck them explicitly. @@ -5968,26 +5752,8 @@ jobs: # #6356). fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- + - name: Setup workspace + uses: ./.github/actions/setup-workspace # Same note as `typecheck-source-gates`' Restore Turbo cache step (#5401). ⚠️ The # namespace is the literal `typecheck`, never `github.job`. @@ -6002,9 +5768,6 @@ jobs: ${{ runner.os }}-turbo-build-core-${{ github.ref_name }}- ${{ runner.os }}-turbo-build-core- - - name: Install dependencies - run: pnpm install --frozen-lockfile - # Example apps are AI-authoring reference templates; a red typecheck is a # bad signal to copy from. tsup transpiles them without a full typecheck, # so build alone will not catch type drift — typecheck them explicitly. @@ -6191,26 +5954,8 @@ jobs: # #6356). fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- + - name: Setup workspace + uses: ./.github/actions/setup-workspace # Same note as `typecheck-source-gates`' Restore Turbo cache step (#5401). ⚠️ The # namespace is the literal `typecheck`, never `github.job`. @@ -6225,9 +5970,6 @@ jobs: ${{ runner.os }}-turbo-build-core-${{ github.ref_name }}- ${{ runner.os }}-turbo-build-core- - - name: Install dependencies - run: pnpm install --frozen-lockfile - # Same build note as `typecheck-debt`'s Build workspace packages step (#3037, # #22077, #21186). - name: Build workspace packages diff --git a/.github/workflows/os-create-smoke.yml b/.github/workflows/os-create-smoke.yml index 062bf2bfbe0..81ea12be51b 100644 --- a/.github/workflows/os-create-smoke.yml +++ b/.github/workflows/os-create-smoke.yml @@ -88,25 +88,8 @@ jobs: - name: Checkout uses: actions/checkout@v7 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- + - name: Setup workspace + uses: ./.github/actions/setup-workspace - name: Setup turbo cache uses: actions/cache@v6 @@ -117,9 +100,6 @@ jobs: ${{ runner.os }}-turbo-${{ github.job }}- ${{ runner.os }}-turbo- - - name: Install dependencies - run: pnpm install --frozen-lockfile - # The smoke's own prerequisite, asserted by the script itself: it refuses # to run without packages/cli/dist and the `os` bin. - name: Build diff --git a/.github/workflows/pack-smoke-optin.yml b/.github/workflows/pack-smoke-optin.yml index 6cb53634df0..ac9f5d49faa 100644 --- a/.github/workflows/pack-smoke-optin.yml +++ b/.github/workflows/pack-smoke-optin.yml @@ -118,25 +118,8 @@ jobs: - name: Checkout the merge preview uses: actions/checkout@v7 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- + - name: Setup workspace + uses: ./.github/actions/setup-workspace - name: Setup turbo cache uses: actions/cache@v6 @@ -147,9 +130,6 @@ jobs: ${{ runner.os }}-turbo-${{ github.job }}- ${{ runner.os }}-turbo- - - name: Install dependencies - run: pnpm install --frozen-lockfile - # The driver's own prerequisite, asserted by the script itself: pack mode # fails fast unless packages/cli/dist and the create-objectstack bin exist. - name: Build diff --git a/.github/workflows/partof-closing-keyword-guard.yml b/.github/workflows/partof-closing-keyword-guard.yml index 8567a86f160..8adf46dd9f2 100644 --- a/.github/workflows/partof-closing-keyword-guard.yml +++ b/.github/workflows/partof-closing-keyword-guard.yml @@ -80,7 +80,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: .nvmrc # The body reaches the script through `env:`, never through `${{ }}` # inside the `run:` script. An expression interpolated into a shell line diff --git a/.github/workflows/platform-checklist-watchdog.yml b/.github/workflows/platform-checklist-watchdog.yml index fa0ede86726..09a5e015708 100644 --- a/.github/workflows/platform-checklist-watchdog.yml +++ b/.github/workflows/platform-checklist-watchdog.yml @@ -192,24 +192,16 @@ jobs: - name: Checkout repository uses: actions/checkout@v7 - # Kept as this job's own step rather than folded into the composite - # below. ⛔ No longer because a gate cannot see it: - # `scripts/check-node-version.mjs` reads `.github/actions/**` too since - # #19229, so its census follows a setup-node step wherever it is written. - # Kept because the pin is already here and moving it buys nothing. - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - # `pnpm` itself is the requirement, NOT the workspace. The gate is invoked # through its PACKAGE SCRIPT (`pnpm check:platform-checklist`), which is a # pinned property of this file — an inlined `node scripts/...` copy would # drift from the script the maintainer's cadence documentation names, and # would silently drop the `checklist-select --self-test` leg the alias runs # first. - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm + - name: Setup workspace + uses: ./.github/actions/setup-workspace + with: + install: 'false' # No `pnpm install`. Both halves of the alias import `node:` builtins and # repo-local modules only — no npm dependency — so installing the diff --git a/.github/workflows/pr-automation.yml b/.github/workflows/pr-automation.yml index 7d355b2ad95..da8e4b2d299 100644 --- a/.github/workflows/pr-automation.yml +++ b/.github/workflows/pr-automation.yml @@ -484,25 +484,9 @@ jobs: # failed without ever compiling anything. It also keeps the failure signature # single: on this path the only red is the adjudication step, never an # install that happened to flake on the same shaky network. - - name: Setup Node.js - if: >- - steps.labels.outputs.skip != 'true' - && steps.diffbase.outputs.base_error == '' - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - if: >- - steps.labels.outputs.skip != 'true' - && steps.diffbase.outputs.base_error == '' - uses: ./.github/actions/setup-pnpm - - - name: Install dependencies - if: >- - steps.labels.outputs.skip != 'true' - && steps.diffbase.outputs.base_error == '' - run: pnpm install --frozen-lockfile + - name: Setup workspace + if: steps.labels.outputs.skip != 'true' && steps.diffbase.outputs.base_error == '' + uses: ./.github/actions/setup-workspace # COUNTING ONLY -- the verdict is two steps down (#6378). The split is not # cosmetic: it is what lets the label window be settled for exactly the PRs diff --git a/.github/workflows/publish-smoke.yml b/.github/workflows/publish-smoke.yml index 569de7ea4ac..59ed5136350 100644 --- a/.github/workflows/publish-smoke.yml +++ b/.github/workflows/publish-smoke.yml @@ -198,25 +198,8 @@ jobs: with: ref: ${{ needs.resolve.outputs.ref }} - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- + - name: Setup workspace + uses: ./.github/actions/setup-workspace - name: Setup turbo cache uses: actions/cache@v6 @@ -227,9 +210,6 @@ jobs: ${{ runner.os }}-turbo-${{ github.job }}- ${{ runner.os }}-turbo- - - name: Install dependencies - run: pnpm install --frozen-lockfile - - name: Build run: pnpm run build @@ -272,7 +252,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: .nvmrc - name: Publish smoke (npm registry) run: SMOKE_MODE=registry bash scripts/publish-smoke.sh diff --git a/.github/workflows/release-coverage-patrol.yml b/.github/workflows/release-coverage-patrol.yml index e30263e34e9..095dccdb4f1 100644 --- a/.github/workflows/release-coverage-patrol.yml +++ b/.github/workflows/release-coverage-patrol.yml @@ -172,7 +172,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: .nvmrc # No `pnpm install`: the gate imports `node:fs` and one local module. # Installing the workspace here would buy nothing and would give a diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 96a87038a69..fb385f671a3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -292,29 +292,8 @@ jobs: - name: Checkout repository uses: actions/checkout@v7 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup workspace + uses: ./.github/actions/setup-workspace # Decides WHAT the version pass will bump, so it belongs on this lane too # (the publish lane runs it again — a gate on one lane is not a gate on @@ -1045,7 +1024,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: .nvmrc # ────────────────────────────────────────────────────────────────────── # R2 — the probe may only ever see the version main ACTUALLY carries. @@ -1504,7 +1483,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: .nvmrc - name: Cancel waiting publish prompts whose version is already on npm env: @@ -1562,7 +1541,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: .nvmrc - name: Read the full battery at or before the version commit env: @@ -1685,7 +1664,7 @@ jobs: # hotcrm's latest release and installs it: pnpm install aborts with # ERR_PNPM_UNSUPPORTED_ENGINE outside that release's engines.node # (v6.0.0, the latest on 2026-10-26: `^22.11 || ^24 || >=26`). - node-version: '22' + node-version-file: .nvmrc # ────────────────────────────────────────────────────────────────────── # The publish lane may only ever ship a commit that is ALREADY on main. @@ -1809,21 +1788,8 @@ jobs: echo " see the run's deployment history for the reviewer and timestamp" } >> "$GITHUB_STEP_SUMMARY" - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- + - name: Setup workspace + uses: ./.github/actions/setup-workspace # Mostly a CONSUMER now, not a seeder: this job used to run on every main # push and warmed the cache for everyone; it now runs only when a human @@ -1849,9 +1815,6 @@ jobs: ${{ runner.os }}-turbo-${{ github.job }}-${{ github.ref_name }}- ${{ runner.os }}-turbo-${{ github.job }}- - - name: Install dependencies - run: pnpm install --frozen-lockfile - - name: Verify Changesets "fixed" group covers every public package run: node scripts/check-changeset-fixed.mjs diff --git a/.github/workflows/required-set-patrol.yml b/.github/workflows/required-set-patrol.yml index 789b90cf6ad..aeda6f7945a 100644 --- a/.github/workflows/required-set-patrol.yml +++ b/.github/workflows/required-set-patrol.yml @@ -132,7 +132,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: .nvmrc # No `pnpm install`: the live mode imports nothing outside node: builtins # and global fetch. Installing the workspace would buy nothing and would diff --git a/.github/workflows/scaffold-e2e.yml b/.github/workflows/scaffold-e2e.yml index 765150291b0..8e0444ca7c2 100644 --- a/.github/workflows/scaffold-e2e.yml +++ b/.github/workflows/scaffold-e2e.yml @@ -55,28 +55,8 @@ jobs: - name: Checkout repository uses: actions/checkout@v7 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup workspace + uses: ./.github/actions/setup-workspace - name: Build the scaffolder run: pnpm --filter create-objectstack build @@ -561,6 +541,10 @@ jobs: # them would gate the release on unmaintained content. template: [blank] steps: + # The one literal Node pin left in the workflows: this job checks nothing + # out (it scaffolds from the published package into RUNNER_TEMP), so + # there is no .nvmrc to read. `pnpm check:node-version` holds it equal to + # .nvmrc. - name: Setup Node.js uses: actions/setup-node@v7 with: diff --git a/.github/workflows/shard-timings-refresh.yml b/.github/workflows/shard-timings-refresh.yml index be27616e480..b0ee7e4875d 100644 --- a/.github/workflows/shard-timings-refresh.yml +++ b/.github/workflows/shard-timings-refresh.yml @@ -270,30 +270,12 @@ jobs: with: persist-credentials: false - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - # Restore-only: scheduled runs read main's store cache; the per-push # workflows own saving it. Same shape as coverage-nightly.yml. - - name: Restore pnpm cache - uses: actions/cache/restore@v6 + - name: Setup workspace + uses: ./.github/actions/setup-workspace with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - - - name: Install dependencies - run: pnpm install --frozen-lockfile + save-store: 'false' # Verify the instruments BEFORE trusting their output. Both scripts carry a # battery floor, so this also catches the case where their assertions diff --git a/.github/workflows/showcase-smoke.yml b/.github/workflows/showcase-smoke.yml index d86e9adc66a..fd1bd3aa1e2 100644 --- a/.github/workflows/showcase-smoke.yml +++ b/.github/workflows/showcase-smoke.yml @@ -19,37 +19,14 @@ jobs: timeout-minutes: 45 steps: - uses: actions/checkout@v7 - # ⚠️ This is the ONLY job in the repo that acquires pnpm through - # `pnpm/action-setup`, and the only one using setup-node's `cache: pnpm`. - # Those are the same fact, and the order below is load-bearing — read this - # before "tidying" it onto `.github/actions/setup-pnpm` like every other - # caller. - # - # `cache: pnpm` makes setup-node shell out to pnpm to locate the store, so - # pnpm must already be on PATH when setup-node runs. That is the OPPOSITE - # ordering from every setup-pnpm caller here (setup-node first, then the - # composite). Getting it backwards does not degrade, it kills the job in - # the setup step with "Unable to locate executable file: pnpm" — the - # failure `partof-closing-keyword-guard.yml` and `single-claim-path-guard.yml` - # both carry receipts for. `pnpm check:pnpm-acquisition` fails on that - # inversion, so the naive conversion is caught rather than shipped. - # - # Converting is therefore not a swap: it means DROPPING `cache: pnpm` and - # adding the house pair (`pnpm store path` → actions/cache, as ci.yml does - # seven times over). Deliberately not done here. This job is nightly and - # non-blocking, its one uncached pnpm download is a single fetch rather - # than the per-queue-build multiplier the composite was built to remove, - # and the restructure trades that for re-plumbing the dependency cache of - # a green job nothing gates on. The site is not hidden by staying: it is - # counted and printed by `pnpm check:pnpm-acquisition`, which is keyed on - # jobs that RUN pnpm rather than on any one spelling — precisely so this - # path cannot vanish from a `corepack enable`-shaped census again. - - uses: pnpm/action-setup@v6 - - uses: actions/setup-node@v7 - with: - node-version: '22' - cache: pnpm - - run: pnpm install --frozen-lockfile + # The house setup, like every other job: no `pnpm/action-setup` and no + # setup-node `cache: pnpm`. That pair needed pnpm on PATH before + # setup-node ran (`cache: pnpm` shells out to pnpm to find the store), the + # opposite order from the composite; the composite drops `cache: pnpm` + # and restores the store itself, so there is no order left to get wrong. + # A nightly run is a `schedule` event, so this job saves the store. + - name: Setup workspace + uses: ./.github/actions/setup-workspace # The smoke's webServer runs `os serve --dev`, which loads the showcase # config + its workspace runtime deps (CLI, connector-rest, …) from their # built `dist/`. Build the whole dependency closure (excluding the diff --git a/.github/workflows/single-claim-path-guard.yml b/.github/workflows/single-claim-path-guard.yml index fc74e254347..bb9010364a7 100644 --- a/.github/workflows/single-claim-path-guard.yml +++ b/.github/workflows/single-claim-path-guard.yml @@ -86,7 +86,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: .nvmrc # Everything reaches the script through `env:`, never through `${{ }}` # inside the `run:` line. Nothing here is attacker-controlled text today diff --git a/.github/workflows/spec-liveness-check.yml b/.github/workflows/spec-liveness-check.yml index 787b1157667..e80e1a6738d 100644 --- a/.github/workflows/spec-liveness-check.yml +++ b/.github/workflows/spec-liveness-check.yml @@ -51,16 +51,8 @@ jobs: - name: Checkout repository uses: actions/checkout@v7 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Install dependencies - run: pnpm install --frozen-lockfile + - name: Setup workspace + uses: ./.github/actions/setup-workspace - name: Check spec liveness run: pnpm --filter @objectstack/spec check:liveness diff --git a/.github/workflows/test-nightly-tiers.yml b/.github/workflows/test-nightly-tiers.yml index d4a69ae0937..871c4be058b 100644 --- a/.github/workflows/test-nightly-tiers.yml +++ b/.github/workflows/test-nightly-tiers.yml @@ -160,33 +160,12 @@ jobs: - name: Checkout repository uses: actions/checkout@v7 - # Kept as this job's own step rather than folded into the composite - # below. ⛔ No longer because a gate cannot see it: - # `scripts/check-node-version.mjs` reads `.github/actions/**` too since - # #19229, so its census follows a setup-node step wherever it is written. - # Kept because the pin is already here and moving it buys nothing. - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - # Restore-only: scheduled runs read main's store cache; the per-push # workflows own saving it. - - name: Restore pnpm cache - uses: actions/cache/restore@v6 + - name: Setup workspace + uses: ./.github/actions/setup-workspace with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- + save-store: 'false' # Restore-only, from Build Core's namespace: that job builds the whole # workspace on every main push, so its entries replay every `build` task @@ -203,9 +182,6 @@ jobs: ${{ runner.os }}-turbo-build-core-${{ github.ref_name }}- ${{ runner.os }}-turbo-build-core- - - name: Install dependencies - run: pnpm install --frozen-lockfile - # Two readings before a single test runs, both loud on failure: # # --check every tier-owning package honours the switch, judged by what @@ -362,7 +338,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v7 with: - node-version: '22' + node-version-file: .nvmrc # No `pnpm install`: `scripts/nightly-tiers.mjs --failing-files` imports # `node:` builtins and two repo-local helpers only, so installing the diff --git a/.github/workflows/ui-coverage-nightly.yml b/.github/workflows/ui-coverage-nightly.yml index de0bab94cb2..cbf83d955de 100644 --- a/.github/workflows/ui-coverage-nightly.yml +++ b/.github/workflows/ui-coverage-nightly.yml @@ -68,28 +68,12 @@ jobs: - name: Checkout repository uses: actions/checkout@v7 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - # Restore-only, here and below: a nightly reads `main`'s caches and the # per-push workflows own saving them. - - name: Restore pnpm cache - uses: actions/cache/restore@v6 + - name: Setup workspace + uses: ./.github/actions/setup-workspace with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- + save-store: 'false' # ci.yml's Build UI Deps saves its turbo entries for exactly the build # below on every `main` push, so the build replays them. @@ -102,9 +86,6 @@ jobs: ${{ runner.os }}-turbo-ui-deps-${{ github.ref_name }}- ${{ runner.os }}-turbo-build-core-${{ github.ref_name }}- - - name: Install dependencies - run: pnpm install --frozen-lockfile - # The same command as the step of this name in ci.yml's ui jobs: # objectui's suite resolves the seven @objectstack packages from their # built dist. @@ -200,36 +181,13 @@ jobs: if: needs.ui-coverage.result == 'success' uses: actions/checkout@v7 - - name: Setup Node.js - if: needs.ui-coverage.result == 'success' - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - if: needs.ui-coverage.result == 'success' - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - if: needs.ui-coverage.result == 'success' - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Restore pnpm cache - if: needs.ui-coverage.result == 'success' - uses: actions/cache/restore@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - # No build: `--merge-reports` runs no test file, and the vitest config # imports nothing from the @objectstack packages. - - name: Install dependencies + - name: Setup workspace if: needs.ui-coverage.result == 'success' - run: pnpm install --frozen-lockfile + uses: ./.github/actions/setup-workspace + with: + save-store: 'false' - name: Download every shard's blob report if: needs.ui-coverage.result == 'success' diff --git a/.github/workflows/upgrade-harness-optin.yml b/.github/workflows/upgrade-harness-optin.yml index 35d015e3491..3d9391184c9 100644 --- a/.github/workflows/upgrade-harness-optin.yml +++ b/.github/workflows/upgrade-harness-optin.yml @@ -84,25 +84,8 @@ jobs: - name: Checkout the merge preview uses: actions/checkout@v7 - - name: Setup Node.js - uses: actions/setup-node@v7 - with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- + - name: Setup workspace + uses: ./.github/actions/setup-workspace # Restore-only: the dogfood job's entries (seeded from `main`) replay this # closure; a PR never writes the cache, as in ci.yml. @@ -115,9 +98,6 @@ jobs: ${{ runner.os }}-turbo-dogfood-1-main- ${{ runner.os }}-turbo-dogfood- - - name: Install dependencies - run: pnpm install --frozen-lockfile - - name: Build the dogfood package's dependency closure run: pnpm turbo run build --filter=@objectstack/dogfood --concurrency=4 diff --git a/.github/workflows/validate-deps.yml b/.github/workflows/validate-deps.yml index 5fc86aab793..d8fab0a44b0 100644 --- a/.github/workflows/validate-deps.yml +++ b/.github/workflows/validate-deps.yml @@ -56,30 +56,10 @@ jobs: - name: Checkout repository uses: actions/checkout@v7 - - name: Setup Node.js - uses: actions/setup-node@v7 + - name: Setup workspace + uses: ./.github/actions/setup-workspace with: - node-version: '22' - - - name: Setup pnpm - uses: ./.github/actions/setup-pnpm - - - name: Get pnpm store directory - shell: bash - run: | - echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - - - name: Setup pnpm cache - uses: actions/cache@v6 - with: - path: ${{ env.STORE_PATH }} - key: ${{ runner.os }}-pnpm-store-v3-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: | - ${{ runner.os }}-pnpm-store-v3- - - - name: Verify lockfile is up to date - run: | - pnpm install --frozen-lockfile --prefer-offline + install-args: --prefer-offline - name: Verify Changesets "fixed" group covers every public package run: node scripts/check-changeset-fixed.mjs diff --git a/scripts/check-pnpm-acquisition.mjs b/scripts/check-pnpm-acquisition.mjs index 2f6087221d8..97d1dae9541 100644 --- a/scripts/check-pnpm-acquisition.mjs +++ b/scripts/check-pnpm-acquisition.mjs @@ -17,8 +17,9 @@ // // Zero is the dangerous number here. It is a TRUE answer about Corepack that // reads as a complete answer about pnpm acquisition, and those are not the same -// question: `showcase-smoke.yml` acquires pnpm through `pnpm/action-setup@v6`, -// which contains no Corepack string and is therefore invisible to that key. +// question: `showcase-smoke.yml` acquired pnpm through `pnpm/action-setup@v6` +// (until it moved onto `.github/actions/setup-workspace`), which contains no +// Corepack string and is therefore invisible to that key. // Anyone greping the key in six months gets 0 and concludes the class is // closed, while a job still downloads pnpm from the registry on every run. // @@ -65,10 +66,10 @@ // headers. It is statically decidable, so it is checked here: a job whose // setup-node asks for the pnpm store cache must acquire pnpm strictly earlier. // -// That invariant is what makes `showcase-smoke.yml` safe to LEAVE on -// `pnpm/action-setup` (see the census note below) and what would catch the -// naive conversion of it -- moving the acquisition after setup-node to match -// the house pattern, while leaving `cache: pnpm` behind. +// That invariant is what made `showcase-smoke.yml` safe to leave on +// `pnpm/action-setup`, and what would have caught the naive conversion of it +// -- moving the acquisition after setup-node to match the house pattern, while +// leaving `cache: pnpm` behind. Its conversion dropped `cache: pnpm`. // // ## Recognised spellings -- published, because an unrecognised one is silent // @@ -76,6 +77,7 @@ // can see is part of the contract, so it is published rather than left inside // the implementation. // +// uses: ./.github/actions/setup-workspace workspace cache-backed (calls setup-pnpm) // uses: ./.github/actions/setup-pnpm composite cache-backed // uses: pnpm/action-setup@ action-setup downloads per job // run: ... corepack enable ... corepack-inline downloads per job @@ -116,6 +118,7 @@ const WORKFLOW_DIR = '.github/workflows'; * remaining exposure is legible without opening any workflow. */ export const MECHANISMS = [ + { id: 'workspace', label: 'uses: ./.github/actions/setup-workspace', cached: true }, { id: 'composite', label: 'uses: ./.github/actions/setup-pnpm', cached: true }, { id: 'action-setup', label: 'uses: pnpm/action-setup@', cached: false }, { id: 'corepack-inline', label: 'run: corepack enable', cached: false }, @@ -135,6 +138,7 @@ const RE_STANDALONE = /get\.pnpm\.io\/install/; // npm's global flag and `pnpm` on the same line, in either order. const RE_NPM_GLOBAL_LINE = /\bnpm\s+(?:i|install|add)\b/; +const RE_WORKSPACE_USES = /^\.\/\.github\/actions\/setup-workspace\/?$/; const RE_COMPOSITE_USES = /^\.\/\.github\/actions\/setup-pnpm\/?$/; const RE_ACTION_SETUP_USES = /^pnpm\/action-setup(?:@|$)/; const RE_SETUP_NODE_USES = /^actions\/setup-node(?:@|$)/; @@ -151,6 +155,7 @@ function isNpmGlobalPnpm(run) { /** The mechanism a step uses to acquire pnpm, or null. */ function classifyStep({ uses, run }) { if (uses) { + if (RE_WORKSPACE_USES.test(uses)) return 'workspace'; if (RE_COMPOSITE_USES.test(uses)) return 'composite'; if (RE_ACTION_SETUP_USES.test(uses)) return 'action-setup'; } @@ -252,7 +257,7 @@ export function scan(root) { where: `${where}:${firstInvoke.line}`, jobId, problem: 'runs pnpm but no step in the job acquires it by any recognised mechanism', - fix: 'add `uses: ./.github/actions/setup-pnpm` -- or, if this job DOES acquire pnpm by a mechanism this gate has never seen, teach the gate: that unseen path is the defect in this script\'s header', + fix: 'add `uses: ./.github/actions/setup-workspace` (or `./.github/actions/setup-pnpm` alone) -- or, if this job DOES acquire pnpm by a mechanism this gate has never seen, teach the gate: that unseen path is the defect in this script\'s header', }); } else if (before.length === 0) { problems.push({ @@ -382,7 +387,7 @@ function withFixture(files, fn) { // must not red. A battery BELOW its floor means cases stopped running; the // remedy is to find what stopped registering. const SELF_TEST_BATTERIES = Object.freeze({ - 'check-pnpm-acquisition self-test': 12, + 'check-pnpm-acquisition self-test': 14, }); // DELETING an entry silences that battery's floor exactly as effectively as @@ -451,6 +456,34 @@ export function selfTest() { }, ); + // The workspace composite: it acquires pnpm (through setup-pnpm, inside it) + // and installs, so a job that calls it and then runs pnpm is the house shape + // too -- and calling it AFTER the first pnpm is the same late acquisition. + withFixture( + { + 'a.yml': wf( + ' j:', + ' runs-on: ubuntu-latest', + ' steps:', + ' - uses: actions/checkout@v7', + ' - uses: ./.github/actions/setup-workspace', + ' - run: pnpm test', + ), + 'b.yml': wf( + ' j:', + ' runs-on: ubuntu-latest', + ' steps:', + ' - run: pnpm test', + ' - uses: ./.github/actions/setup-workspace', + ), + }, + (root) => { + const r = scan(root); + t('the workspace composite is recognised', mechs(r).join() === 'workspace,workspace', JSON.stringify(mechs(r))); + t('the workspace composite before pnpm passes, after it FAILS', kinds(r).join() === 'acquired-late', JSON.stringify(kinds(r))); + }, + ); + // THE case this gate exists for: a job that runs pnpm with no acquisition at // all. This is how an unrecognised sixth mechanism surfaces -- loudly, rather // than as a silent zero in some mechanism-keyed count. diff --git a/scripts/pm/check-governed-queue-guard.mjs b/scripts/pm/check-governed-queue-guard.mjs index 2660f1ceda0..f15eede8587 100644 --- a/scripts/pm/check-governed-queue-guard.mjs +++ b/scripts/pm/check-governed-queue-guard.mjs @@ -4479,7 +4479,7 @@ export async function selfTest() { question?.body ?? '(no toolchain question step)', ); const gate = "steps.toolchain.outputs.needed != 'false'"; - const installChain = stepBodies.filter((s) => /setup-pnpm|pnpm store path|actions\/cache@|pnpm install\b/.test(s.body)); + const installChain = stepBodies.filter((s) => /setup-pnpm|pnpm store path|actions\/cache(?:\/restore)?@|pnpm install\b/.test(s.body)); assert( '⭐ every-install-step-is-gated-on-NOT-FALSE-so-a-skipped-or-failed-question-installs', installChain.length === 4 && installChain.every((s) => s.body.includes(`if: ${gate}`)) && !/outputs\.needed == 'true'/.test(wf),