|
10 | 10 | # that failed on something transient; it is safe because a version already on |
11 | 11 | # the registry is skipped rather than attempted. |
12 | 12 | # |
13 | | -# Authenticates with a stored npm automation token, in the repository secret |
14 | | -# `NPM_TOKEN`. Trusted publishing (OIDC) would avoid the stored credential, but |
15 | | -# it needs a trusted publisher registered against this workflow's filename on |
16 | | -# npmjs.com, which can only be done through the web UI — and until that exists |
17 | | -# npm rejects the publish as E404/no-permission, which is how v0.24.1 failed. |
| 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. |
18 | 17 | # |
19 | | -# Worth knowing when this is next revisited: npm is restricting tokens that |
20 | | -# bypass 2FA for direct publishing, so the token path has a horizon. |
21 | | -# https://gh.io/npm-gat-bypass2fa-deprecation |
| 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. |
| 22 | +# |
| 23 | +# What that looks like when it goes wrong, since the error names neither OIDC |
| 24 | +# nor trusted publishing: |
| 25 | +# |
| 26 | +# npm error code E404 |
| 27 | +# npm error 404 Not Found - PUT https://registry.npmjs.org/moshcode |
| 28 | +# npm error 404 ... could not be found or you do not have permission |
| 29 | +# |
| 30 | +# There is no credential at all in that case, and npm reports it as if the |
| 31 | +# package did not exist. If you see it, the publisher is not registered, or is |
| 32 | +# registered against a different workflow filename. |
22 | 33 | name: publish |
23 | 34 |
|
24 | 35 | on: |
|
28 | 39 |
|
29 | 40 | permissions: |
30 | 41 | contents: read |
31 | | - # Still needed with token auth: provenance is signed with a short-lived OIDC |
32 | | - # token even though the publish itself authenticates with NPM_TOKEN. Without |
33 | | - # it `--provenance` fails. |
| 42 | + # The whole basis of the exchange: this is what lets the run mint the OIDC |
| 43 | + # token npm authenticates against. Without it there is no credential at all |
| 44 | + # and publishing fails outright. |
34 | 45 | id-token: write |
35 | 46 |
|
36 | 47 | jobs: |
|
51 | 62 | cache: pnpm |
52 | 63 | registry-url: https://registry.npmjs.org |
53 | 64 |
|
| 65 | + # Node 22 bundles npm 10, which predates trusted publishing and would fall |
| 66 | + # back to looking for a token that no longer exists — an auth failure that |
| 67 | + # reads as a credential problem rather than a version one. 11.5.1 is the |
| 68 | + # floor; the check below says so plainly if that ever regresses. |
| 69 | + - name: Install an npm that understands trusted publishing |
| 70 | + run: | |
| 71 | + npm install -g npm@latest |
| 72 | + VERSION="$(npm --version)" |
| 73 | + MINIMUM=11.5.1 |
| 74 | + echo "npm $VERSION" |
| 75 | + # Lowest of the two must be the minimum, or this npm is older than it. |
| 76 | + if [ "$(printf '%s\n%s\n' "$MINIMUM" "$VERSION" | sort -V | head -n1)" != "$MINIMUM" ]; then |
| 77 | + echo "::error::npm $VERSION cannot use trusted publishing — $MINIMUM or later is required" |
| 78 | + exit 1 |
| 79 | + fi |
| 80 | +
|
54 | 81 | - run: pnpm install --frozen-lockfile |
55 | 82 |
|
56 | 83 | # Publishing is the one action here that cannot be taken back — npm will |
@@ -95,24 +122,11 @@ jobs: |
95 | 122 | echo "already=false" >> "$GITHUB_OUTPUT" |
96 | 123 | fi |
97 | 124 |
|
98 | | - # Said plainly here, rather than as the E404/no-permission npm otherwise |
99 | | - # returns partway through a release — an error that reads as "the package |
100 | | - # does not exist" rather than "there is no credential". |
101 | | - - name: Require an npm token |
102 | | - if: steps.published.outputs.already == 'false' |
103 | | - env: |
104 | | - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} |
105 | | - run: | |
106 | | - if [ -z "$NPM_TOKEN" ]; then |
107 | | - echo "::error::NPM_TOKEN is not set — add an npm automation token as a repository secret named NPM_TOKEN" |
108 | | - exit 1 |
109 | | - fi |
110 | | -
|
| 125 | + # No token, and no `--provenance` either: publishing through trusted |
| 126 | + # publishing generates and attaches the attestation on its own. |
111 | 127 | - name: Publish |
112 | 128 | if: steps.published.outputs.already == 'false' |
113 | | - run: npm publish --access public --provenance |
114 | | - env: |
115 | | - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} |
| 129 | + run: npm publish --access public |
116 | 130 |
|
117 | 131 | - name: Confirm the registry has it |
118 | 132 | if: steps.published.outputs.already == 'false' |
|
0 commit comments