From a07b9eeb2912aa755b373a0836501adc54a0185f Mon Sep 17 00:00:00 2001 From: Stefano Fiorini Date: Sun, 3 May 2026 22:48:45 -0500 Subject: [PATCH] feat(2026-05-03-perform-code-optimization-and-document-cleanup): followup Fix CI pnpm version conflict in check.yml --- .github/workflows/check-online.yml | 2 - .github/workflows/check.yml | 2 - docs/DEVELOPMENT.md | 23 ++++++- package.json | 1 + scripts/lib/assert-no-pnpm-version-pin.mjs | 76 ++++++++++++++++++++++ scripts/lib/run-check.mjs | 1 + 6 files changed, 100 insertions(+), 5 deletions(-) create mode 100644 scripts/lib/assert-no-pnpm-version-pin.mjs diff --git a/.github/workflows/check-online.yml b/.github/workflows/check-online.yml index 3a16286..9a6f824 100644 --- a/.github/workflows/check-online.yml +++ b/.github/workflows/check-online.yml @@ -32,8 +32,6 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4 - with: - version: "10" - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 62d8162..55d45f3 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -38,8 +38,6 @@ jobs: - name: Install pnpm uses: pnpm/action-setup@v4 - with: - version: "10" - name: Install dependencies run: pnpm install --frozen-lockfile diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 791c083..e88d927 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -43,6 +43,7 @@ pnpm run check | `pnpm run verify:docs` | markdownlint + offline link-check + docs-flow verifier | | `pnpm run verify:docs:online` | Same as `verify:docs` but with full external link checking | | `pnpm run verify:generated` | Assert generated output freshness (stub; fleshed out in M3) | +| `pnpm run verify:ci` | Assert CI workflow files carry no pnpm version pins (see [pnpm version pinning](#pnpm-version-pinning)) | | `pnpm run check` | Aggregate: run every gate above and report a summary | ## Quality tooling (added in M1) @@ -167,6 +168,7 @@ PASS verify:pi PASS verify:reviewers PASS verify:docs PASS verify:generated +PASS verify:ci ``` This is the only acceptable state for merge. Any failure on a check not in @@ -415,7 +417,8 @@ Two GitHub Actions workflows live in `.github/workflows/`: 2. Installs `shellcheck` via `apt-get` (Ubuntu) or `brew` (macOS). 3. Installs `ripgrep` via `apt-get` (Ubuntu only; pre-installed on macOS runners). 4. Installs Node.js 22 via `actions/setup-node`. -5. Installs pnpm 10 via `pnpm/action-setup`. +5. Installs pnpm via `pnpm/action-setup@v4` — **no `version:` key is set**; the action reads the version from + `package.json#packageManager` (currently `pnpm@10.18.1+sha512…`), which is the single source of truth. 6. Runs `pnpm install --frozen-lockfile`. 7. Runs `pnpm run check` (the same command contributors run locally). @@ -423,6 +426,24 @@ The matrix runs both `ubuntu-latest` and `macos-latest` to guard against platform-specific regressions. Because M2 made all shell scripts portable across BSD and GNU coreutils, both runners should stay green. +### pnpm version pinning + +The pnpm version is pinned **exclusively** in `package.json#packageManager`: + +```json +"packageManager": "pnpm@10.18.1+sha512.77a884a..." +``` + +This field carries an exact version *and* an integrity hash, giving stronger +reproducibility than a floating major like `version: "10"`. The +`pnpm/action-setup@v4` step in `check.yml` reads this field automatically; +do **not** add a `with.version` key to that step. + +`pnpm run verify:ci` (backed by `scripts/lib/assert-no-pnpm-version-pin.mjs`) +greps every `.github/workflows/*.yml` for `pnpm/action-setup` blocks that +carry a `version:` key and fails if any are found. This prevents +reintroduction of the conflict that caused `pnpm/action-setup@v4` to error. + ### Adding new prerequisites to CI If a new tool is required (e.g. a new binary called by a verify script), diff --git a/package.json b/package.json index 5efe794..29d9ade 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "verify:docs": "markdownlint-cli2 ; R1=$?; node scripts/lib/run-link-check.mjs ; R2=$?; node scripts/verify-docs-flow.mjs ; R3=$?; [ $((R1+R2+R3)) -eq 0 ]", "verify:docs:online": "markdownlint-cli2 ; R1=$?; node scripts/lib/run-link-check.mjs --online ; R2=$?; node scripts/verify-docs-flow.mjs ; R3=$?; [ $((R1+R2+R3)) -eq 0 ]", "verify:generated": "node scripts/verify-generated.mjs", + "verify:ci": "node scripts/lib/assert-no-pnpm-version-pin.mjs", "check": "node scripts/lib/run-check.mjs" }, "pi": { diff --git a/scripts/lib/assert-no-pnpm-version-pin.mjs b/scripts/lib/assert-no-pnpm-version-pin.mjs new file mode 100644 index 0000000..2b95c01 --- /dev/null +++ b/scripts/lib/assert-no-pnpm-version-pin.mjs @@ -0,0 +1,76 @@ +#!/usr/bin/env node +/** + * assert-no-pnpm-version-pin.mjs — CI regression guard (followup: fix pnpm version conflict) + * + * Ensures no .github/workflows/*.yml file pins pnpm via a `version:` key + * under a `pnpm/action-setup` step. The canonical version source is + * `package.json#packageManager`, which carries an exact version + integrity + * hash. Duplicating the version in the workflow creates a conflict that + * pnpm/action-setup@v4 treats as an error. + * + * Usage: + * node scripts/lib/assert-no-pnpm-version-pin.mjs + * pnpm run verify:ci + * + * Exit codes: + * 0 — no version pin found + * 1 — one or more violations found (details on stderr) + */ + +import { readFileSync, readdirSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(__dirname, "../.."); +const WORKFLOWS_DIR = path.join(REPO_ROOT, ".github", "workflows"); + +let violations = 0; + +// Read workflow files — silently pass if directory doesn't exist +let files; +try { + files = readdirSync(WORKFLOWS_DIR).filter( + (f) => f.endsWith(".yml") || f.endsWith(".yaml") + ); +} catch { + process.stdout.write("OK: no .github/workflows directory found; nothing to check.\n"); + process.exit(0); +} + +for (const file of files) { + const fullPath = path.join(WORKFLOWS_DIR, file); + const content = readFileSync(fullPath, "utf8"); + const lines = content.split("\n"); + + for (let i = 0; i < lines.length; i++) { + // Locate a step that uses pnpm/action-setup + if (!lines[i].includes("pnpm/action-setup")) continue; + + // Look ahead up to 10 lines for a `version:` key in the same step + const end = Math.min(i + 10, lines.length); + for (let j = i + 1; j < end; j++) { + const ahead = lines[j]; + // A new step begins at a `- name:` or `- uses:` list item → stop + if (/^\s*-\s+(name|uses)\s*:/.test(ahead)) break; + // `version:` key found inside this step → violation + if (/^\s+version\s*:/.test(ahead)) { + process.stderr.write( + `ERROR: ${file}:${j + 1}: 'version:' key found under pnpm/action-setup step.\n` + + ` Remove 'with.version'; let package.json#packageManager be the single\n` + + ` source of truth for the pnpm version (exact version + integrity hash).\n\n` + ); + violations++; + break; + } + } + } +} + +if (violations > 0) { + process.stderr.write(`${violations} violation(s) found.\n`); + process.exit(1); +} + +process.stdout.write("OK: no pnpm version pins found in workflow files.\n"); +process.exit(0); diff --git a/scripts/lib/run-check.mjs b/scripts/lib/run-check.mjs index 8920016..94dde00 100644 --- a/scripts/lib/run-check.mjs +++ b/scripts/lib/run-check.mjs @@ -33,6 +33,7 @@ const STEPS = [ { label: "verify:reviewers", cmd: "pnpm", args: ["run", "verify:reviewers"] }, { label: "verify:docs", cmd: "pnpm", args: ["run", "verify:docs"] }, { label: "verify:generated", cmd: "pnpm", args: ["run", "verify:generated"] }, + { label: "verify:ci", cmd: "pnpm", args: ["run", "verify:ci"] }, ]; // ── Runner ─────────────────────────────────────────────────────────────────