@schalkneethling/calavera-skill-npm-trusted-publishing-github-workflow 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,6 @@
6
6
  "displayName": "npm trusted publishing GitHub workflow",
7
7
  "payload": "payload/npm-trusted-publishing-github-workflow",
8
8
  "compatibility": {
9
- "calavera": ">=2.2.0 <3"
9
+ "calavera": ">=2.2.0"
10
10
  }
11
11
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schalkneethling/calavera-skill-npm-trusted-publishing-github-workflow",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "npm trusted publishing GitHub workflow artifact for Calavera.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -4,7 +4,7 @@ description: >
4
4
  Generate, repair, or debug the GitHub Actions workflow FILE that performs an OIDC
5
5
  trusted publish of a pnpm package — the concrete publish.yml, its test → build →
6
6
  publish job shape, the package tarball artifact handoff, Node-version inference from
7
- package.json, pnpm and runtime setup via pnpm/setup, the npm-CLI-version upgrade step, and
7
+ package.json, version-aware pnpm and runtime setup, the npm-CLI-version upgrade step, and
8
8
  repository.url/Sigstore provenance matching. Use when the user wants the actual
9
9
  workflow written or fixed, or is debugging a specific CI failure: npm publish
10
10
  E404/E403/422, NODE_AUTH_TOKEN appearing unexpectedly, provenance or id-token errors,
@@ -24,23 +24,23 @@ Implement the same hardened npm trusted publishing pattern every time, without r
24
24
 
25
25
  This skill generates and debugs the publish workflow file. For the surrounding security posture — account and repository 2FA, branch protection, GitHub publish environments, release-strategy choice, and sole-maintainer risk — use the `npm-package-publishing` skill. The two are complementary: `npm-package-publishing` decides how publishing should be set up, this skill writes and fixes the YAML that does it.
26
26
 
27
- One number to keep consistent between the two: both skills use Node 24.8.0 or higher as the publish-step floor. Node 24.8.0 bundles npm 11.6.0, which already exceeds the npm CLI 11.5.1 minimum that trusted publishing requires, so on that floor no manual npm upgrade is needed. If a project must publish on an older Node, it has to upgrade npm to 11.5.1 or later first — the publish job retains a guard step for exactly that case.
27
+ One number to keep consistent between the two: both skills use Node 24.8.0 or higher as the publish-step floor. Node 24.8.0 bundles npm 11.6.0, which already exceeds the npm CLI 11.5.1 minimum that trusted publishing requires, so on that floor no manual npm upgrade is needed. If a project must publish on an older supported Node, reject anything below Node 22.14.0 and upgrade npm only on Node 22.14.0 through 24.7.x.
28
28
 
29
29
  ## Workflow
30
30
 
31
31
  1. Inspect `package.json`, `.npmrc`, lockfiles, and existing `.github/workflows/*.yml`.
32
32
  2. Resolve every workflow dependency to its latest stable version at the moment the file is created, and pin each to the full-length commit SHA of that version. Never leave third-party or GitHub-owned actions pinned to tag-based refs such as `@v4`, `@v6`, or `@v7` in the final workflow; tag refs weaken supply-chain integrity and violate pinned-action policy. The SHAs in this skill's template are placeholders that will be out of date; never copy them verbatim. See "Pinning actions to current SHAs" below for the procedure.
33
33
  3. Preserve pinned action SHAs when they already exist; annotate each with a version comment so Dependabot can bump it.
34
- 4. Drive the test and build jobs' Node version from the project's **existing** target, not from a number invented for this workflow. Read it from the repo's current `devEngines.runtime`, `.nvmrc`, `.node-version`, `volta.node`, or CI config; if none exists, ask the developer rather than guessing. For pnpm projects, prefer `pnpm/setup` and either let it read `devEngines.runtime` from `package.json` or set its `runtime` input to the resolved existing target, such as `node@22`. Do not point runtime selection at `engines.node`, which is the consumer compatibility range — an unbounded range like `>=20` can float CI away from the version developers actually run.
35
- 5. Never raise the project's Node version, create a new `.nvmrc`, or overwrite an existing one to "match" the publish step. The publish step's Node 24.8.0 (step 11) is an isolated requirement of the publish action and must not propagate to `.nvmrc`, to `engines.node`, or to the test and build jobs. A project that targets Node 22 keeps testing and building on Node 22; only the final `npm publish` invocation runs on 24.8.0, and it does not rebuild the artifact. Conflating these two numbers is the most likely way this skill is misapplied — do not do it.
36
- 6. Ensure every job that reads the repo (including any reading `.nvmrc`) runs `actions/checkout` first.
37
- 7. For pnpm workflows, use `pnpm/setup` from the GitHub Marketplace instead of combining `pnpm/action-setup` with `actions/setup-node`. It installs pnpm from `@pnpm/exe` and installs the requested JavaScript runtime through `pnpm runtime set` in one step. Pin it to a full commit SHA like every other action.
38
- 8. Set `install: false` on `pnpm/setup` in release/publish workflows, then run `pnpm install --frozen-lockfile --ignore-scripts` explicitly. The action can auto-install by default, but release workflows should keep install flags visible and hardened.
34
+ 4. Resolve the pnpm major from the exact `packageManager` value before selecting setup actions. For pnpm 11 or newer, use `pnpm/setup`. For pnpm 10 or older, use `pnpm/action-setup` together with `actions/setup-node`; `pnpm/setup` cannot install those versions. Do not guess or float the pnpm major.
35
+ 5. Drive the test and build jobs' Node version from the project's **existing** target, not from a number invented for this workflow. Read it from `devEngines.runtime`, `.nvmrc`, `.node-version`, `volta.node`, or existing CI; if none exists, ask rather than guessing. With pnpm 11 or newer, let `pnpm/setup` read `devEngines.runtime` or pass the resolved target as `runtime`. With pnpm 10 or older, pass the same target to `actions/setup-node`. Never use `engines.node`, which is the consumer compatibility range, as the CI runtime selector.
36
+ 6. Never raise the project's Node version, create a new `.nvmrc`, or overwrite an existing one to "match" the publish step. The publish step's Node 24.8.0 is isolated and must not propagate to development, test, or build targets.
37
+ 7. Ensure every job that reads the repo runs `actions/checkout` first.
38
+ 8. Disable automatic installation in the selected pnpm setup action, then run `pnpm install --frozen-lockfile --ignore-scripts` explicitly so release install flags stay visible and hardened.
39
39
  9. Do not use Corepack in release workflows: it is still marked experimental and downloads the package manager from the network on first use, which is an avoidable failure surface in a release pipeline.
40
40
  10. Set `persist-credentials: false` on every `actions/checkout` step. Never rely on checkout's default credential persistence. If a workflow genuinely must push to git, use an explicit, narrowly scoped credential only for that push step.
41
- 11. Target Node 24.8.0 or higher in the publish step. That floor bundles npm 11.6.0, which already exceeds the npm CLI 11.5.1 minimum trusted publishing requires, so no manual npm upgrade is needed there. Keep a guard step that upgrades npm only when the resolved Node ships an npm below 11.5.1, so the workflow stays correct if a project pins an older Node. An npm that is too old silently falls back to token auth or fails to attempt OIDC at all.
42
- 12. Pack into a dedicated artifact directory, usually `package/*.tgz`.
43
- 13. In the publish job, download the artifact to `package`, find the `.tgz`, and publish its resolved path.
41
+ 11. Target Node 24.8.0 or higher in the publish step. If a lower version is unavoidable, reject Node below 22.14.0 before any npm upgrade, upgrade npm only on Node 22.14.0 through 24.7.x, and require npm 11.5.1 or newer.
42
+ 12. Pack into a unique directory under `RUNNER_TEMP`, keyed by the workflow run and attempt.
43
+ 13. In the publish job, download the artifact to the corresponding `RUNNER_TEMP` directory, find the `.tgz`, and publish its resolved path.
44
44
  14. Use GitHub OIDC trusted publishing, not npm tokens. Provenance is generated automatically under trusted publishing, so the `--provenance` flag is not required.
45
45
  15. Add a `concurrency` group keyed on the release so two tag pushes cannot race into overlapping publishes.
46
46
 
@@ -83,19 +83,57 @@ The publish-step version must never be copied into the other two. Do not raise `
83
83
 
84
84
  The `engines.node` value above is the _consumer_ floor and should reflect what the package actually supports; `>=20` is only an example, and a bounded upper limit is sensible if the package genuinely needs one. Do not raise it to 24.8.0 to satisfy CI — the publish step pins its own Node version, and the test and build jobs read theirs from `devEngines.runtime` or another existing project target, so the trusted-publishing requirement never leaks into the consumer contract.
85
85
 
86
- For pnpm projects, prefer declaring the development runtime in `devEngines.runtime` so `pnpm/setup`
87
- can read the runtime and version from `package.json`. If the repo already uses `.nvmrc`,
88
- `.node-version`, `volta.node`, or existing CI config instead, keep that source of truth and set
89
- `pnpm/setup`'s `runtime` input to the same resolved version. Do not default to the publish step's
90
- 24.8.0.
86
+ For pnpm 11-or-newer projects, prefer declaring the development runtime in `devEngines.runtime` so
87
+ `pnpm/setup` can read it from `package.json`. If the repo already uses `.nvmrc`, `.node-version`,
88
+ `volta.node`, or existing CI instead, keep that source of truth and set `pnpm/setup`'s `runtime`
89
+ input to the same resolved version. For pnpm 10 or older, pass that target to `actions/setup-node`.
90
+ Do not default test or build jobs to the publish step's 24.8.0.
91
91
 
92
92
  The `repository.url` field is not cosmetic. Provenance verification runs through Sigstore, which compares the repository in the OIDC token against `package.json`. A mismatch fails the publish with a 422 error that the user-facing npm docs do not explain. Make sure the owner/name in `repository.url` matches the repository actually running the workflow.
93
93
 
94
- Do not add npm auth tokens for trusted publishing.
94
+ Trusted publishing itself must not receive `NODE_AUTH_TOKEN`. A project that installs private dependencies may expose a read-only token only on the dependency-install step. Keep registry mapping and the environment reference in the checked-in `.npmrc`, never a token value. Do not persist the token through setup, build, pack, or publish steps. Run `npm publish` from an isolated directory under `RUNNER_TEMP` so npm does not discover the repository's private-install `.npmrc`.
95
+
96
+ ```ini
97
+ @OWNER:registry=https://npm.pkg.github.com
98
+ //npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}
99
+ ```
100
+
101
+ ```yaml
102
+ - name: Install dependencies
103
+ env:
104
+ NODE_AUTH_TOKEN: ${{ secrets.READ_ONLY_PACKAGES_TOKEN }}
105
+ run: pnpm install --frozen-lockfile --ignore-scripts
106
+ ```
95
107
 
96
108
  ## Workflow Template
97
109
 
98
- Use this shape for pnpm packages, adapting only names, test commands, and existing pinned action SHAs. The `@<sha>` values below are **placeholders**: before writing the file, resolve each action to its latest stable release and replace the placeholder with that release's full-length commit SHA, keeping the `# vX.Y.Z` comment accurate. Do not copy the example SHAs — see "Pinning actions to current SHAs".
110
+ Use this shape for pnpm 11-or-newer packages, adapting names, test commands, and pinned action SHAs. Verify the exact pnpm major from `packageManager` first. For pnpm 10 or older, replace every test/build `pnpm/setup` step with pinned `pnpm/action-setup` (`run_install: false`) plus pinned `actions/setup-node` using the resolved project Node target. In the publish job, use pinned `actions/setup-node` with Node 24.8.0 or newer; pnpm is not needed after the tarball is downloaded.
111
+
112
+ The `@<sha>` values below are **placeholders**: resolve each action to its latest stable release and replace the placeholder with that release's full-length commit SHA, keeping the version comment accurate. Do not copy the example SHAs.
113
+
114
+ For pnpm 10 or older, use this concrete setup shape in both test and build jobs:
115
+
116
+ ```yaml
117
+ - name: Setup pnpm
118
+ uses: pnpm/action-setup@2222222222222222222222222222222222222222 # PLACEHOLDER SHA, re-resolve before use
119
+ with:
120
+ version: 10 # Replace with the exact project major or version.
121
+ run_install: false
122
+
123
+ - name: Setup Node.js
124
+ uses: actions/setup-node@3333333333333333333333333333333333333333 # PLACEHOLDER SHA, re-resolve before use
125
+ with:
126
+ node-version: "22" # Replace with the resolved existing project target.
127
+ ```
128
+
129
+ In the pnpm-10 publish job, only Node is required:
130
+
131
+ ```yaml
132
+ - name: Setup Node.js for publish
133
+ uses: actions/setup-node@3333333333333333333333333333333333333333 # PLACEHOLDER SHA, re-resolve before use
134
+ with:
135
+ node-version: "24.8.0"
136
+ ```
99
137
 
100
138
  ```yaml
101
139
  # NOTE: every action SHA below is a PLACEHOLDER and is almost certainly out of date.
@@ -123,7 +161,7 @@ jobs:
123
161
  contents: read
124
162
  steps:
125
163
  - name: Checkout
126
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 — PLACEHOLDER SHA, re-resolve before use
164
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 — PLACEHOLDER SHA, re-resolve before use
127
165
  with:
128
166
  persist-credentials: false
129
167
 
@@ -153,7 +191,7 @@ jobs:
153
191
  contents: read
154
192
  steps:
155
193
  - name: Checkout
156
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 — PLACEHOLDER SHA, re-resolve before use
194
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 — PLACEHOLDER SHA, re-resolve before use
157
195
  with:
158
196
  persist-credentials: false
159
197
 
@@ -169,16 +207,19 @@ jobs:
169
207
  run: pnpm install --frozen-lockfile --ignore-scripts
170
208
 
171
209
  - name: Create package directory
172
- run: mkdir package
210
+ run: |
211
+ package_dir="${RUNNER_TEMP}/npm-package-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
212
+ mkdir -p "$package_dir"
213
+ echo "PACKAGE_DIR=$package_dir" >> "$GITHUB_ENV"
173
214
 
174
215
  - name: Create package tarball
175
- run: pnpm pack --pack-destination package
216
+ run: pnpm pack --pack-destination "$PACKAGE_DIR"
176
217
 
177
218
  - name: Upload package tarball
178
219
  uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 — PLACEHOLDER SHA, re-resolve before use
179
220
  with:
180
221
  name: npm-package
181
- path: package/*.tgz
222
+ path: ${{ runner.temp }}/npm-package-${{ github.run_id }}-${{ github.run_attempt }}/*.tgz
182
223
  if-no-files-found: error
183
224
  retention-days: 7
184
225
 
@@ -193,7 +234,7 @@ jobs:
193
234
  id-token: write
194
235
  steps:
195
236
  - name: Checkout
196
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4.2.2 — PLACEHOLDER SHA, re-resolve before use
237
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 — PLACEHOLDER SHA, re-resolve before use
197
238
  with:
198
239
  persist-credentials: false
199
240
 
@@ -206,61 +247,75 @@ jobs:
206
247
  install: false
207
248
 
208
249
  - name: Ensure npm is new enough for trusted publishing
209
- # No-op on Node >= 24.8.0; the guard only matters if Node is pinned lower.
210
250
  run: |
211
- required="11.5.1"
251
+ minimum_node="22.14.0"
252
+ no_upgrade_node="24.8.0"
253
+ required_npm="11.5.1"
212
254
  pinned_npm="11.6.0"
213
- current="$(npm --version)"
214
- if node - "$current" "$required" <<'NODE'
215
- const [current, required] = process.argv.slice(2);
216
- const parse = (version) => version.split(".").map((part) => Number.parseInt(part, 10));
217
- const [currentMajor, currentMinor, currentPatch] = parse(current);
218
- const [requiredMajor, requiredMinor, requiredPatch] = parse(required);
219
- const currentParts = [currentMajor, currentMinor, currentPatch];
220
- const requiredParts = [requiredMajor, requiredMinor, requiredPatch];
221
- let isBelow = false;
222
- for (const [index, part] of currentParts.entries()) {
223
- if (part !== requiredParts[index]) {
224
- isBelow = part < requiredParts[index];
225
- break;
255
+ version_at_least() {
256
+ node - "$1" "$2" <<'NODE'
257
+ const [current, required] = process.argv.slice(2).map((version) =>
258
+ version.split(".").map((part) => Number.parseInt(part, 10)),
259
+ );
260
+ for (let index = 0; index < 3; index += 1) {
261
+ if (current[index] !== required[index]) {
262
+ process.exit(current[index] > required[index] ? 0 : 1);
226
263
  }
227
264
  }
228
- process.exit(isBelow ? 1 : 0);
229
265
  NODE
230
- then
231
- echo "npm $current satisfies $required."
266
+ }
267
+
268
+ node_version="$(node --version | sed 's/^v//')"
269
+ current_npm="$(npm --version)"
270
+ if ! version_at_least "$node_version" "$minimum_node"; then
271
+ echo "Trusted publishing requires Node $minimum_node or newer; found $node_version." >&2
272
+ exit 1
273
+ fi
274
+
275
+ if version_at_least "$current_npm" "$required_npm"; then
276
+ echo "npm $current_npm satisfies $required_npm."
277
+ elif version_at_least "$node_version" "$no_upgrade_node"; then
278
+ echo "Node $node_version unexpectedly provides npm $current_npm; refusing a global npm mutation." >&2
279
+ exit 1
232
280
  else
233
- echo "npm $current is below $required; upgrading to pinned npm $pinned_npm."
281
+ echo "npm $current_npm is below $required_npm; upgrading to pinned npm $pinned_npm."
234
282
  npm install -g "npm@$pinned_npm"
235
283
  fi
236
- npm --version
284
+ version_at_least "$(npm --version)" "$required_npm"
237
285
 
238
286
  - name: Download package tarball
239
287
  uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 — PLACEHOLDER SHA, re-resolve before use
240
288
  with:
241
289
  name: npm-package
242
- path: package
290
+ path: ${{ runner.temp }}/npm-package-${{ github.run_id }}-${{ github.run_attempt }}
243
291
 
244
292
  - name: Publish to npm
245
293
  run: |
246
- tarball="$(find package -type f -name '*.tgz' -print -quit)"
294
+ package_dir="${RUNNER_TEMP}/npm-package-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
295
+ tarball="$(find "$package_dir" -type f -name '*.tgz' -print -quit)"
247
296
 
248
297
  if [ -z "$tarball" ]; then
249
298
  echo "No package tarball found in downloaded artifact."
250
- find package -maxdepth 3 -type f -print
299
+ find "$package_dir" -maxdepth 3 -type f -print
251
300
  exit 1
252
301
  fi
253
302
 
254
- npm publish "$(realpath "$tarball")" --ignore-scripts --access public --registry https://registry.npmjs.org
303
+ tarball="$(realpath "$tarball")"
304
+ publish_dir="${RUNNER_TEMP}/npm-publish-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
305
+ mkdir -p "$publish_dir"
306
+ cd "$publish_dir"
307
+ npm publish "$tarball" --ignore-scripts --registry https://registry.npmjs.org
255
308
  ```
256
309
 
310
+ Do not add `--access public` unconditionally. Prefer explicit package metadata such as `publishConfig.access` when public access is required, and preserve private or restricted package intent.
311
+
257
312
  ## Pinning actions to current SHAs
258
313
 
259
314
  The template's SHAs are stale by design. Action versions and their commit SHAs change over time, so resolve them fresh whenever a `publish.yml` is created or reviewed. Pin to the full-length commit SHA, never a tag or branch, because a tag can be moved to point at malicious code after you have reviewed it. Tag-based refs such as `@v4`, `@v6`, and `@v7` are acceptable only as temporary input to a pinning tool; they must not survive in committed workflow YAML.
260
315
 
261
316
  There are two reliable ways to produce current pins.
262
317
 
263
- The preferred approach is to let tooling resolve and pin for you. Write the workflow first using human-readable tags only in the temporary draft consumed by the tool (for example `actions/checkout@v4`), then run `npx actions-up@1.16.0` in the repository to rewrite every `uses:` reference to the latest stable release pinned to its commit SHA, with a version comment appended. This is the same tool the `npm-package-publishing` skill recommends, and it removes the chance of a hand-typed SHA being wrong. After it runs, confirm each line carries a `@<40-hex-sha> # vX.Y.Z` form.
318
+ The preferred approach is to let tooling resolve and pin for you. Write the workflow first using human-readable tags only in the temporary draft consumed by the tool, then run `npx actions-up@1.16.0 --yes` so the command is non-interactive. Confirm each line carries a `@<40-hex-sha> # vX.Y.Z` form.
264
319
 
265
320
  If resolving manually, for each action find the latest stable release tag, then read the exact commit that tag points to and pin that commit:
266
321
 
@@ -274,7 +329,7 @@ gh api repos/actions/checkout/git/refs/tags/v4.2.2 --jq .object.sha
274
329
 
275
330
  For an annotated tag the first lookup may return a tag object rather than a commit; dereference it with `gh api repos/<owner>/<repo>/git/tags/<sha> --jq .object.sha` to reach the underlying commit. Pin the commit SHA, not the tag SHA.
276
331
 
277
- Keep the pins current after creation by letting Dependabot manage action updates. This is why every `uses:` line carries a `# vX.Y.Z` comment: Dependabot reads the comment to know which version a SHA represents and to raise update PRs. The companion Dependabot configuration should include a `github-actions` ecosystem entry pointing at `/` so the publish workflow is covered. Periodically re-running `npx actions-up@1.16.0` is a reasonable backstop if Dependabot is not enabled.
332
+ Keep the pins current after creation by letting Dependabot manage action updates. This is why every `uses:` line carries a `# vX.Y.Z` comment: Dependabot reads the comment to know which version a SHA represents and to raise update PRs. The companion Dependabot configuration should include a `github-actions` ecosystem entry pointing at `/` so the publish workflow is covered. Periodically re-running `npx actions-up@1.16.0 --yes` is a reasonable backstop if Dependabot is not enabled.
278
333
 
279
334
  ## Checks
280
335
 
@@ -300,25 +355,42 @@ if grep -n "PLACEHOLDER" .github/workflows/publish.yml; then
300
355
  exit 1
301
356
  fi
302
357
 
303
- # Every uses: line must reference a 40-hex SHA, not a tag
304
- if ! grep -qE "uses: [^@]+@" .github/workflows/publish.yml; then
305
- echo "No uses lines found"
306
- exit 1
307
- fi
308
-
309
- bad_refs="$(grep -nE "uses: [^@]+@[^ ]+" .github/workflows/publish.yml \
310
- | grep -vE "@[0-9a-f]{40} " || true)"
311
- if [ -n "$bad_refs" ]; then
312
- echo "$bad_refs"
313
- echo "Unpinned action found" >&2
314
- exit 1
315
- fi
316
- echo "All actions SHA-pinned"
358
+ # Parse the actual YAML uses values so comments and unrelated text cannot satisfy the check.
359
+ ruby <<'RUBY'
360
+ require "yaml"
361
+
362
+ uses = []
363
+ walk = lambda do |value|
364
+ case value
365
+ when Hash
366
+ value.each do |key, child|
367
+ uses << child.to_s if key.to_s == "uses"
368
+ walk.call(child)
369
+ end
370
+ when Array
371
+ value.each { |child| walk.call(child) }
372
+ end
373
+ end
374
+ walk.call(YAML.load_file(".github/workflows/publish.yml"))
375
+
376
+ abort "No uses entries found" if uses.empty?
377
+ bad_refs = uses.reject do |ref|
378
+ next true if ref.start_with?("./", "docker://")
379
+
380
+ sha = ref[/@([0-9a-f]{40})\z/i, 1]
381
+ sha && !sha.match?(/\A(.)\1{39}\z/)
382
+ end
383
+ unless bad_refs.empty?
384
+ warn bad_refs.join("\n")
385
+ abort "Unpinned or placeholder action found"
386
+ end
387
+ puts "All actions SHA-pinned"
388
+ RUBY
317
389
  ```
318
390
 
319
391
  ## Failure Clues
320
392
 
321
- - `NODE_AUTH_TOKEN: ***` appears in the publish log: token auth is being used or injected. Trusted publishing should not need it.
393
+ - `NODE_AUTH_TOKEN: ***` appears in the publish log: token auth is being used or injected into the publish step. A read-only token is acceptable only on a private-dependency install step and must be absent afterward.
322
394
  - `E404 Not Found - PUT ... could not be found or you do not have permission`: often an auth/scope permission problem, especially if local manual publish works.
323
395
  - `422 Unprocessable Entity` during publish with provenance: the repository in the OIDC token does not match `package.json`. Check `repository.url` first.
324
396
  - npm silently publishing with a token despite trusted-publisher config: the runner's npm CLI is older than 11.5.1. This should not happen on the pinned Node 24.8.0 (which bundles npm 11.6.0); if the publish step was moved to an older Node, confirm the guard step actually upgraded npm and reported a version at or above 11.5.1.
@@ -334,7 +406,7 @@ Repo changes cannot create npm's trusted publisher entry. Remind the user to ver
334
406
 
335
407
  - provider: GitHub Actions
336
408
  - repository owner/name matches the repo
337
- - workflow filename matches `.github/workflows/publish.yml`
409
+ - workflow filename matches `publish.yml` (filename only, including the extension)
338
410
  - publish environment matches the workflow if npm is configured with one
339
411
  - at least one allowed action is selected: configurations created after 20 May 2026 require explicitly selecting an allowed action (for example, allow `npm publish`), or the publish will be rejected
340
412