1010# that failed on something transient; it is safe because a version already on
1111# the registry is skipped rather than attempted.
1212#
13- # Authenticates by trusted publishing (OIDC) rather than a stored token: npm
14- # trades the short-lived token GitHub mints for this specific workflow run for
15- # permission to publish, so there is no long-lived credential in the repository
16- # to leak, rotate or forget.
13+ # Authenticates with a stored npm automation token, in the repository secret
14+ # `NPM_TOKEN`. Trusted publishing (OIDC) would avoid the stored credential and
15+ # was tried twice; both attempts failed with npm reporting no credential at all,
16+ # and the registration on npmjs.com — the web-UI half of it, pinned to this
17+ # file's name — is what remains unconfirmed. See #305, #309, #311.
1718#
18- # The other half of that trust lives on npmjs.com, under the package's Trusted
19- # Publisher settings, and it is pinned to the *filename* of this workflow.
20- # Renaming this file silently breaks publishing — npm will refuse the exchange
21- # because the run no longer matches what was configured.
19+ # If you go back to OIDC, the trap is below: `registry-url` is *required* here
20+ # and *fatal* there. setup-node writes
2221#
23- # When it goes wrong the error names neither OIDC nor trusted publishing:
22+ # //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
2423#
25- # npm error code E404
26- # npm error 404 Not Found - PUT https://registry.npmjs.org/moshcode
27- # npm error 404 ... could not be found or you do not have permission
24+ # which is how the token reaches npm — and under OIDC, with no token to fill it,
25+ # resolves to an empty credential that stops npm attempting the exchange.
26+ # Removing it changed the error from E404 to ENEEDAUTH but did not publish.
27+ # actions/setup-node#1551, npm/cli#9088.
2828#
29- # That is an unauthenticated PUT, reported as though the package did not exist.
30- # It has two quite different causes, and it took v0.24.3 to tell them apart:
31- #
32- # 1. anything that leaves an empty auth token in an .npmrc, which stops npm
33- # attempting the exchange at all — see the setup-node note below; or
34- # 2. no trusted publisher registered for this package, or one registered
35- # against a different workflow filename.
36- #
37- # Check 1 first. It is in this file, and it is the one that looks like 2.
29+ # Worth knowing when this is next revisited: npm is restricting tokens that
30+ # bypass 2FA for direct publishing, so the token path has a horizon.
31+ # https://gh.io/npm-gat-bypass2fa-deprecation
3832name : publish
3933
4034on :
4438
4539permissions :
4640 contents : read
47- # The whole basis of the exchange: this is what lets the run mint the OIDC
48- # token npm authenticates against. Without it there is no credential at all
49- # and publishing fails outright .
41+ # Still needed with token auth: provenance is signed with a short-lived OIDC
42+ # token even though the publish itself authenticates with NPM_TOKEN. Without
43+ # it `--provenance` fails.
5044 id-token : write
5145
5246jobs :
@@ -61,38 +55,14 @@ jobs:
6155 # with ERR_PNPM_BAD_PM_VERSION.
6256 - uses : pnpm/action-setup@v4
6357
64- # Deliberately no `registry-url`. With it, setup-node always writes
65- #
66- # //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
67- #
68- # into an .npmrc — correct for token auth, and quietly fatal here. Under
69- # trusted publishing there is no NODE_AUTH_TOKEN, so that line resolves to
70- # an empty token, and npm stops before ever attempting the OIDC exchange:
71- # it believes it already has credentials. The registry then answers the
72- # unauthenticated PUT with E404, which names nothing to do with OIDC and
73- # is what sent us looking at the npmjs.com config instead of at this file.
74- #
75- # actions/setup-node#1551. Without registry-url no .npmrc is written and
76- # npm defaults to registry.npmjs.org anyway, which is where we publish.
58+ # `registry-url` is what makes setup-node write the .npmrc line that feeds
59+ # NODE_AUTH_TOKEN to npm. Required for token auth — and the thing to delete
60+ # first if this ever moves back to OIDC.
7761 - uses : actions/setup-node@v4
7862 with :
7963 node-version : 24
8064 cache : pnpm
81-
82- # Node 24 bundles npm 11, but pin the floor anyway rather than depend on
83- # what a runner image happens to ship: below 11.5.1 there is no trusted
84- # publishing, and the failure would again look like a credential problem.
85- - name : Install an npm that understands trusted publishing
86- run : |
87- npm install -g npm@latest
88- VERSION="$(npm --version)"
89- MINIMUM=11.5.1
90- echo "npm $VERSION"
91- # Lowest of the two must be the minimum, or this npm is older than it.
92- if [ "$(printf '%s\n%s\n' "$MINIMUM" "$VERSION" | sort -V | head -n1)" != "$MINIMUM" ]; then
93- echo "::error::npm $VERSION cannot use trusted publishing — $MINIMUM or later is required"
94- exit 1
95- fi
65+ registry-url : https://registry.npmjs.org
9666
9767 - run : pnpm install --frozen-lockfile
9868
@@ -138,13 +108,24 @@ jobs:
138108 echo "already=false" >> "$GITHUB_OUTPUT"
139109 fi
140110
141- # No token. `--provenance` is passed even though npm documents it as
142- # automatic under trusted publishing: reports differ on whether it really
143- # is, and asking for it explicitly costs nothing and cannot produce a
144- # weaker result. v0.24.2 published with an attestation using this flag.
111+ # Said plainly here, rather than as the E404/no-permission npm otherwise
112+ # returns partway through a release — an error that reads as "the package
113+ # does not exist" rather than "there is no credential".
114+ - name : Require an npm token
115+ if : steps.published.outputs.already == 'false'
116+ env :
117+ NPM_TOKEN : ${{ secrets.NPM_TOKEN }}
118+ run : |
119+ if [ -z "$NPM_TOKEN" ]; then
120+ echo "::error::NPM_TOKEN is not set — add an npm automation token as a repository secret named NPM_TOKEN"
121+ exit 1
122+ fi
123+
145124 - name : Publish
146125 if : steps.published.outputs.already == 'false'
147126 run : npm publish --access public --provenance
127+ env :
128+ NODE_AUTH_TOKEN : ${{ secrets.NPM_TOKEN }}
148129
149130 - name : Confirm the registry has it
150131 if : steps.published.outputs.already == 'false'
0 commit comments