Skip to content

Commit 948095e

Browse files
mschmickingclaude
andcommitted
ci(release): publish via npm trusted publishing instead of a token
npm's token dialog warns that bypassing 2FA is a security risk and points at Trusted Publishing. It is right: an automation token is a long-lived credential sitting in repository secrets, and OIDC replaces it with an identity check per run. The workflow no longer sets NODE_AUTH_TOKEN and no NPM_TOKEN secret is needed. --provenance is also dropped because provenance is automatic under OIDC for a public package from a public repository. Node moves to 24 in this job: trusted publishing needs npm >= 11.5.1 and Node 22 still ships npm 10.x. npm is upgraded explicitly as well rather than trusting whatever the runner image happens to bundle. A trusted publisher can only be attached to a package that already exists, so the checklist now has the first publish done by hand with an interactive npm login — which honours 2FA and involves no token at all — and every release after that running from CI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 02e36c7 commit 948095e

2 files changed

Lines changed: 68 additions & 24 deletions

File tree

.github/workflows/release.yml

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,27 @@ jobs:
1616
runs-on: ubuntu-latest
1717
permissions:
1818
contents: read
19-
# Required for npm provenance, which attests that this tarball was built
20-
# from this commit in this repository. Only works on a public repository.
19+
# Required for OIDC — this is what npm exchanges for a short-lived
20+
# publish credential, and what provenance is derived from. Only works on a
21+
# public repository.
2122
id-token: write
2223

2324
steps:
2425
- uses: actions/checkout@v7
2526

2627
- uses: actions/setup-node@v7
2728
with:
28-
node-version: 22
29+
# 24 rather than 22: trusted publishing needs npm >= 11.5.1, and Node 22
30+
# still ships npm 10.x.
31+
node-version: 24
2932
cache: npm
3033
registry-url: https://registry.npmjs.org
3134

35+
- name: Ensure an npm new enough for trusted publishing
36+
run: |
37+
npm install -g npm@^11
38+
npm --version
39+
3240
- run: npm ci
3341

3442
- name: Typecheck
@@ -58,11 +66,17 @@ jobs:
5866
if: inputs.dry_run
5967
run: npm publish --dry-run
6068

69+
# No NODE_AUTH_TOKEN. Publishing uses npm trusted publishing (OIDC): npm
70+
# verifies this workflow's identity against the trusted publisher configured
71+
# on the package, so there is no long-lived token to leak or rotate. npm's own
72+
# UI warns against automation tokens that bypass 2FA, and this is the
73+
# alternative it points at.
74+
#
75+
# Provenance is automatic under OIDC for a public package from a public repo,
76+
# so --provenance is not passed explicitly.
6177
- name: Publish
6278
if: ${{ !inputs.dry_run }}
63-
run: npm publish --provenance --access public
64-
env:
65-
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
79+
run: npm publish --access public
6680

6781
- name: Tag the released commit
6882
if: ${{ !inputs.dry_run }}

docs/GOING-PUBLIC.md

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,15 @@ These change how merges behave, and the release automation depends on them.
5252
## 2. Secrets and tokens
5353

5454
- [ ] Create an npm account if you do not have one, and enable 2FA on it
55-
- [ ] Create an **automation** access token on npm (Access Tokens → Generate → Automation)
56-
— a publish token will prompt for 2FA and hang in CI
57-
- [ ] Add it as **Settings → Secrets and variables → Actions → New repository secret**,
58-
named `NPM_TOKEN`
55+
56+
> **Do not create an automation token.** npm's own dialog warns that bypassing 2FA is a
57+
> security risk and points at Trusted Publishing instead — that warning is right, and
58+
> this repository uses Trusted Publishing (OIDC). There is no `NPM_TOKEN` secret: the
59+
> release workflow proves its identity to npm per run and receives a short-lived
60+
> credential, so there is nothing to leak or rotate.
61+
>
62+
> The catch is that **a package must already exist before a trusted publisher can be
63+
> attached to it**, so the very first publish is done by hand. That is step 4.
5964
6065
- [ ] Create a **fine-grained personal access token** and add it as `RELEASE_PLEASE_TOKEN`
6166
— Settings → Developer settings → Personal access tokens → Fine-grained, scoped to
@@ -107,22 +112,47 @@ release-please then opens a "chore(main): release 1.0.0" PR that bumps
107112

108113
## 4. Publish and flip — in this order
109114

110-
`npm publish --provenance` attaches a signed attestation that the tarball was built from
111-
a specific commit in this repository. **It only works on a public repository**, so the
112-
release workflow fails while private. That forces this sequence:
115+
Provenance and OIDC both require a **public** repository, and a trusted publisher can
116+
only be attached to a package that already exists. That fixes the order:
113117

114118
1. [ ] **Make the repository public** (Settings → General → Danger Zone)
115-
2. [ ] **Wait for CodeQL to finish its first run**, and read the findings. It has been
116-
skipping itself this whole time, so its output is genuinely unknown. Going public
117-
is reversible; `npm publish` is not — so look before you publish.
118-
3. [ ] Remove the `> **Not released yet.**` blockquote from the README quick start, and
119-
the `RELEASE CHECKLIST` HTML comment beside it
120-
4. [ ] Run **Actions → Release to npm** with `dry_run: true` and read the file list
121-
5. [ ] Run it again with `dry_run: false`
122-
6. [ ] Verify: `npm view iobroker-sync`, then in a clean directory
123-
`npm i -g iobroker-sync && iob-sync --help`
124-
125-
> The window between step 1 and step 5 is the only time the README promises a package
119+
120+
2. [ ] **Wait for CodeQL's first run** and read the findings. It has skipped itself the
121+
whole time, so its output is genuinely unknown. Going public is reversible;
122+
`npm publish` is not.
123+
124+
3. [ ] Remove the `> **Not released yet.**` note from the README quick start and the
125+
`RELEASE CHECKLIST` comment beside it.
126+
127+
4. [ ] **Publish once by hand**, from a machine where npm works — no token involved:
128+
129+
npm login # interactive, honours your 2FA
130+
npm publish --access public
131+
132+
This is the only publish that needs a human. It exists purely so the package name
133+
is registered and can be configured.
134+
135+
5. [ ] **Attach the trusted publisher** at
136+
`npmjs.com/package/iobroker-sync/access` → Trusted Publisher → GitHub Actions:
137+
138+
| Field | Value |
139+
| --- | --- |
140+
| Organization or user | `mschmicking` |
141+
| Repository | `iobroker-sync` |
142+
| Workflow filename | `release.yml` |
143+
| Environment | *(leave empty)* |
144+
| Allowed actions | `npm publish` |
145+
146+
All fields are **case-sensitive and exact**.
147+
148+
6. [ ] From here on, releases run themselves: **Actions → Release to npm**, first with
149+
`dry_run: true` to read the file list, then `dry_run: false`.
150+
151+
7. [ ] Verify: `npm view iobroker-sync`, then in a clean directory
152+
`npm i -g iobroker-sync && iob-sync --help`. The npm page should show a
153+
**Provenance** badge — it is generated automatically under OIDC.
154+
155+
> The window between step 1 and step 4 is the only time the README promises a package
126156
> that does not exist. Keep it short.
127157
128158
## 5. After going public

0 commit comments

Comments
 (0)