You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
chore(ci): migrate npm publish to trusted publishing via OIDC (#1609)
## Please verify the following:
- [x] `yarn build-and-test:local` passes
- [ ] I have added tests for any new features, if relevant
- [x] `README.md` (or relevant documentation) has been updated with your
changes
## Describe your PR
Migrates the reactotron CI publish pipeline from classic `NPM_TOKEN`
auth to **npm Trusted Publishing via CircleCI OIDC**. npm GA'd CircleCI
support on
[2026-04-06](https://github.blog/changelog/2026-04-06-npm-trusted-publishing-now-supports-circleci/);
this PR wires us up.
The pipeline has been broken since npm revoked classic tokens on
2025-12-09 and #1602 left the renamed `reactotron-npm-context` with an
empty `NPM_TOKEN`. Rather than mint a granular replacement (capped at 90
days) and rotate forever, OIDC eliminates the human-managed token
entirely.
### Changes
- `.circleci/config.yml` (`release_package` job) — replaces the `npm
whoami` + `~/.npmrc` token write with a "Mint npm OIDC token" step using
`circleci run oidc get --claims '{"aud":"npm:registry.npmjs.org"}'`.
- `scripts/release.artifacts.mjs` — accepts either `NPM_TOKEN` or
`NPM_ID_TOKEN`, and performs the npm OIDC token exchange directly (`POST
/-/npm/v1/oidc/token/exchange/package/<ident>`). This in-script exchange
is a workaround for
[yarnpkg/berry#7122](yarnpkg/berry#7122): Yarn
4.14.1's `getOidcToken` helper handles CircleCI, but the `allowOidc`
gate in `publish.ts` only flips on for `GITHUB_ACTIONS` / `GITLAB_CI`.
Once 7122 lands and we bump Yarn, the script-level exchange block can be
deleted.
- Yarn `4.1.1 → 4.14.1` (4.14 brought the CircleCI OIDC support that
7122 finishes wiring up).
- `.yarnrc.yml` — keeps `npmAuthToken: "${NPM_TOKEN-}"` as a soft
fallback during cutover. Empty string is falsy in Yarn's auth chain, so
it's a no-op when OIDC is in play. Removed in a follow-up PR after first
prod publish.
- `docs/contributing/releasing.md` — new "OIDC publish flow" section
covering the CI flow, how to add a trusted publisher when shipping a new
package, and the upstream Yarn workaround.
### Pilot validation
Pilot package: `eslint-plugin-reactotron@0.1.10-beta.0`. Pushed tag from
branch `test/oidc-pilot-eslint`, published under the `beta` dist-tag
(semver prerelease — invisible to `^`/`~`/`*` ranges). Pipeline
succeeded; package published.
The npm registry metadata for the pilot version records:
```json
"_npmUser": {
"name": "CircleCI",
"email": "npm-oidc-no-reply@github.com",
"trustedPublisher": {
"id": "circleci",
"oidcConfigId": "oidc:71dbce82-a25e-4b17-a0e3-78908cc0e805"
}
}
```
This is the smoking-gun proof the publish ran via the trusted publisher
path, not legacy token auth. `_npmVersion: null` and `_nodeVersion:
null` (set when the publisher is OIDC) corroborate.
- npm:
https://www.npmjs.com/package/eslint-plugin-reactotron/v/0.1.10-beta.0
- GitHub release:
https://github.com/infinitered/reactotron/releases/tag/eslint-plugin-reactotron%400.1.10-beta.0
- CircleCI pipeline:
https://app.circleci.com/pipelines/gh/infinitered/reactotron/2762
### Pre-merge prerequisites
- ✅ Trusted publisher configured on all 11 npm packages (same Org /
Project / Pipeline Definition / Context / VCS Origin tuple, validated by
the pilot's successful publish).
- ✅ "Publishing access" tightened to **"Require two-factor
authentication and disallow tokens (recommended)"** on all 11 packages.
Per npm's inline note, this is fully OIDC-compatible — short-lived OIDC
publish tokens are a separate auth path.
Out of scope: `reactotron-app` (Electron app, GitHub release artifacts
only), `reactotron-mcp` (`"private": true`).
### Follow-up PRs
After **first successful production OIDC publish**:
- Remove `npmAuthToken: "${NPM_TOKEN-}"` from `.yarnrc.yml`.
- Tighten `release.artifacts.mjs` to require only `NPM_ID_TOKEN`.
- Clear `NPM_TOKEN` from CircleCI context (currently absent;
belt-and-suspenders).
After **yarnpkg/berry#7122 lands and we bump Yarn**:
- Delete the OIDC exchange block in `scripts/release.artifacts.mjs` —
Yarn handles the exchange directly via `yarn npm publish`.
- Simplify the OIDC docs section.
### Notes
- The chore commit only touches infra files (`.circleci/`,
`.yarnrc.yml`, root `package.json`, `yarn.lock`,
`scripts/release.artifacts.mjs`, `docs/`, `.yarn/releases/`). No `lib/*`
or `apps/*` workspaces affected → 0 release tags created on merge → no
auto-publish triggered. First real OIDC publish happens on the next
legitimate code change inside a `lib/*` workspace.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/contributing/releasing.md
+33Lines changed: 33 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -68,3 +68,36 @@ CircleCi is used to run the release tasks. The `config.yml` file is located in t
68
68
CircleCi is configured to check for whether new release commits and tags are needed on every commit to a release branch: `master`, `beta`, and `alpha`.
69
69
70
70
Once a new release tag is created, CircleCi will run a job to publish the artifacts for the workspace.
71
+
72
+
## npm Authentication (Trusted Publishing via OIDC)
73
+
74
+
npm packages are published using [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers/) — CircleCI mints a short-lived OIDC token that Yarn 4.14+ exchanges for a single-use publish token. There is no long-lived `NPM_TOKEN` to rotate.
75
+
76
+
### How it works in CI
77
+
78
+
The `release_package` job in `.circleci/config.yml` runs two steps:
79
+
80
+
1.**Mint npm OIDC token** — runs `circleci run oidc get --claims '{"aud":"npm:registry.npmjs.org"}'` and exports the result as `NPM_ID_TOKEN`.
81
+
2.**Release to npm and github** — `yarn release:artifacts $CIRCLE_TAG` calls `scripts/release.artifacts.mjs`. The script (not Yarn) exchanges `NPM_ID_TOKEN` for a single-use npm publish token via a direct `POST` to `https://registry.npmjs.org/-/npm/v1/oidc/token/exchange/package/<name>`, then exposes the result as `NPM_TOKEN` so `.yarnrc.yml`'s `npmAuthToken: "${NPM_TOKEN-}"` picks it up at config-load time. Finally it invokes `yarn npm publish`.
82
+
83
+
The reason the script does the exchange rather than Yarn: Yarn 4.14.1's `yarn npm publish` gates the OIDC code path on `GITHUB_ACTIONS || GITLAB_CI`, even though its `getOidcToken` helper already handles `CIRCLECI`. The companion fix is tracked upstream at [yarnpkg/berry#7122](https://github.com/yarnpkg/berry/pull/7122). Once Yarn ships the one-line gate fix and we bump, the exchange block in `release.artifacts.mjs` can be deleted and `yarn npm publish` will pick up `NPM_ID_TOKEN` directly.
84
+
85
+
The job still requires the `reactotron-npm-context` CircleCI context for `$GITHUB_TOKEN` (used to create the GitHub release).
86
+
87
+
### Adding a trusted publisher to a new package
88
+
89
+
When publishing a new `reactotron-*` package, configure its Trusted Publisher on npm before the first release tag, otherwise the publish will fail with a "no trusted publisher configured" error.
90
+
91
+
For each package:
92
+
93
+
1. Navigate to `https://www.npmjs.com/package/<pkg>/access`.
94
+
2. Scroll to "Trusted Publisher" and select **CircleCI**.
95
+
3. Fill in the org/project/context IDs (ask a maintainer for current values; they live in the CircleCI project settings, not in this repo).
96
+
4. Save.
97
+
98
+
`npm trust` (npm v11.10.0+) supports batch configuration after `npm login`. See [npm bulk trusted publishing config](https://github.blog/changelog/2026-02-18-npm-bulk-trusted-publishing-config-and-script-security-now-generally-available/).
99
+
100
+
### Out-of-scope packages
101
+
102
+
-`reactotron-app` is published as GitHub release artifacts, not to npm.
103
+
-`reactotron-mcp` is private (`"private": true`) and intentionally not on npm. An unrelated `reactotron-mcp` package by `steve228uk` exists on npm — that is his own project and is not affiliated with Infinite Red.
0 commit comments