Skip to content

Commit bf62928

Browse files
ci: publish to npm via trusted publishing instead of a stored token (#549)
* ci: publish to npm via trusted publishing instead of a stored token The crates.io job has been using trusted publishing for a while, but the two npm jobs still authenticated with `secrets.NPM_TOKEN` — they set `//registry.npmjs.org/:_authToken` from it and only used `id-token: write` to attest provenance. So a long-lived npm credential with publish rights to the whole scope sat in the repository's secrets, which is the thing trusted publishing exists to remove. Dropping the token is the whole change: npm 11.5.1+ detects the Actions OIDC token on its own, exchanges it for a short-lived publish credential, and generates provenance automatically on that path. The `npm install -g` pin moves from `npm@11` to `npm@^11.5.1` to state the version that behavior needs. The `--provenance` flags stay: they are redundant under trusted publishing but keep the intent visible, and `publishConfig.provenance` already requests it anyway. The `npm` environment is now load-bearing rather than decorative. A trusted publisher entry names the repository, the workflow file, and the environment, so all three are part of the identity — renaming any of them stops publishing until every package's entry is updated. Both are called out where they appear. Trusted publishing cannot create a package that does not exist yet, because the publisher entry lives on the package's settings page. `release.md` now documents that bootstrap step for npm alongside the equivalent crates.io one, including the version-bump-before-pack detail that decides whether the tarball pins its `@ox-content/*` dependencies to the right release. Setup this needs before the next tag: a trusted publisher entry per package on npmjs.com, and `NPM_TOKEN` can be revoked once they are in place. * ci: pin the npm version installed in the publish jobs Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com> --------- Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
1 parent a653089 commit bf62928

2 files changed

Lines changed: 75 additions & 15 deletions

File tree

.github/workflows/publish.yml

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,16 @@ jobs:
144144
name: Publish @ox-content/napi
145145
runs-on: ubuntu-latest
146146
needs: build-napi
147+
# The npm environment is part of the trusted publisher identity: each
148+
# package's publisher on npmjs.com names this repository, the workflow file,
149+
# and this environment. Renaming any of them stops publishing until the
150+
# publisher entry is updated to match.
147151
environment: npm
148-
env:
149-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
150152
permissions:
151153
contents: read
154+
# Mints the OIDC token npm exchanges for a short-lived publish
155+
# credential. This is the only credential in play — there is no
156+
# long-lived npm token in the repository's secrets.
152157
id-token: write
153158
steps:
154159
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -163,12 +168,15 @@ jobs:
163168
run-install: true
164169

165170
- name: Setup npm registry
166-
run: |
167-
npm config set registry https://registry.npmjs.org
168-
npm config set //registry.npmjs.org/:_authToken "$NODE_AUTH_TOKEN"
171+
run: npm config set registry https://registry.npmjs.org
169172

170-
- name: Install npm 11+ (required for OIDC trusted publishing)
171-
run: npm install -g npm@11
173+
# Trusted publishing needs 11.5.1 at minimum; the runner's bundled npm
174+
# trails that, so pin an exact version rather than a range. No auth
175+
# token is configured: npm detects the Actions OIDC
176+
# token and exchanges it for a publish credential itself, and it also
177+
# generates provenance automatically on that path.
178+
- name: Install npm with trusted publishing support
179+
run: npm install -g npm@11.19.0 # zizmor: ignore[adhoc-packages]
172180

173181
- name: Download all artifacts
174182
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
@@ -276,11 +284,16 @@ jobs:
276284
name: Publish npm packages
277285
runs-on: ubuntu-latest
278286
needs: publish-napi
287+
# The npm environment is part of the trusted publisher identity: each
288+
# package's publisher on npmjs.com names this repository, the workflow file,
289+
# and this environment. Renaming any of them stops publishing until the
290+
# publisher entry is updated to match.
279291
environment: npm
280-
env:
281-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
282292
permissions:
283293
contents: read
294+
# Mints the OIDC token npm exchanges for a short-lived publish
295+
# credential. This is the only credential in play — there is no
296+
# long-lived npm token in the repository's secrets.
284297
id-token: write
285298
steps:
286299
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -295,12 +308,15 @@ jobs:
295308
run-install: true
296309

297310
- name: Setup npm registry
298-
run: |
299-
npm config set registry https://registry.npmjs.org
300-
npm config set //registry.npmjs.org/:_authToken "$NODE_AUTH_TOKEN"
301-
302-
- name: Install npm 11+ (required for OIDC trusted publishing)
303-
run: npm install -g npm@11
311+
run: npm config set registry https://registry.npmjs.org
312+
313+
# Trusted publishing needs 11.5.1 at minimum; the runner's bundled npm
314+
# trails that, so pin an exact version rather than a range. No auth
315+
# token is configured: npm detects the Actions OIDC
316+
# token and exchanges it for a publish credential itself, and it also
317+
# generates provenance automatically on that path.
318+
- name: Install npm with trusted publishing support
319+
run: npm install -g npm@11.19.0 # zizmor: ignore[adhoc-packages]
304320

305321
- name: Setup Rust
306322
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable

docs/content/release.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,50 @@ The release script verifies that every crate listed in
4545
order still matters because crates.io must see each dependency before Cargo can
4646
package a dependent crate.
4747

48+
## npm Authentication
49+
50+
The npm jobs publish through GitHub Actions Trusted Publishing. There is no npm
51+
token in the repository's secrets: `id-token: write` lets the job mint an OIDC
52+
token, npm exchanges it for a short-lived publish credential, and provenance is
53+
attested automatically on that path.
54+
55+
Each package carries its own trusted publisher entry on npmjs.com, naming this
56+
repository, `.github/workflows/publish.yml`, and the `npm` environment. All
57+
three are part of the identity, so renaming the workflow file or the environment
58+
breaks publishing until every entry is updated to match.
59+
60+
Entries are needed for the workspace packages (`@ox-content/napi`,
61+
`@ox-content/islands`, `@ox-content/vite-plugin`, `@ox-content/unplugin`, the
62+
four `@ox-content/vite-plugin-{vue,react,svelte,solid}` integrations, and
63+
`@ox-content/wasm`) and for each `@ox-content/napi-*` platform binding package
64+
the N-API build publishes.
65+
66+
## First-Time npm Publishing
67+
68+
Trusted publishing cannot create a package that does not exist yet: the
69+
publisher entry is configured on the package's settings page, so the package has
70+
to be there first. Same shape as the crates.io restriction below.
71+
72+
A release that introduces a new npm package therefore needs one manual publish
73+
by a maintainer with local npm credentials, before the tag is pushed:
74+
75+
```bash
76+
corepack pnpm --filter @ox-content/vite-plugin-new build
77+
cd npm/vite-plugin-ox-content-new
78+
corepack pnpm pack --pack-destination /tmp
79+
npm publish /tmp/ox-content-vite-plugin-new-<version>.tgz --access public --provenance=false
80+
```
81+
82+
Bump every workspace package to the release version before packing, or the
83+
tarball will pin its `@ox-content/*` dependencies to the previous one.
84+
`--provenance=false` is required because provenance generation needs CI; the
85+
package's `publishConfig` turns it on, and subsequent versions get it from the
86+
workflow.
87+
88+
Then add the trusted publisher entry on npmjs.com and push the tag. The publish
89+
steps skip versions that already exist, so the bootstrap publish is not
90+
republished.
91+
4892
## First-Time Crate Publishing
4993

5094
The crates.io job uses GitHub Actions Trusted Publishing. Trusted Publishing can

0 commit comments

Comments
 (0)