Skip to content

Commit a677381

Browse files
dmealingclaude
andcommitted
docs(release): the npm token TYPE is the trap, and OIDC now has a deadline
Records what stalled the 0.24.0 cut for an hour, in both places a release reads. THE TOKEN TYPE IS THE WHOLE GAME, AND A WRONG ONE LOOKS RIGHT. A classic "Publish" token passes every cheap check — `npm whoami` returns the username, `npm access list packages` reports read-write, `bun publish --dry-run` succeeds — and then EOTPs on every real write. `bun publish` defaults to `--auth-type=web`, so it falls back to an interactive browser flow that cannot complete in CI or a non-interactive shell, and `--otp=` is silently ignored (it prompts `Enter OTP:` regardless, so a code has to be piped on stdin). If the account's 2FA is a security key rather than TOTP there is no 6-digit code to give at all. The fix is Classic -> Automation, and the doc now says to verify with a reversible WRITE rather than a read. WITH THE CAVEAT THAT THE PROBE IS ONLY HALF-REVERSIBLE. Since 2026-07-31 a bypass-2FA token may still publish but may not change package access — including deleting a dist-tag. `npm dist-tag add` succeeds and `npm dist-tag rm` returns 403, which also means Phase 9's stale-`next` cleanup now needs an interactive-2FA session. Learned by leaving a stray tag behind doing exactly this probe. OIDC IS NO LONGER A "FUTURE IMPROVEMENT". npm removes direct publishing from 2FA-bypass tokens around January 2027. The skill filed Trusted Publishing under research-backed-not-yet-adopted; it is a dated requirement, `publish-npm.yml` already exists, and `publish-csharp.yml` already does exactly this for NuGet. Changesets stays in the optional bucket, which is where it belongs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
1 parent 5df51c4 commit a677381

2 files changed

Lines changed: 59 additions & 9 deletions

File tree

‎.claude/skills/releasing/SKILL.md‎

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,28 @@ version FAILS by design — skip what's done, never force.
195195
- About to `bun publish` to `latest` without human confirmation → stop.
196196
- About to push the site repo without confirming the live deploy → stop.
197197

198-
## Future improvement (research-backed, not yet adopted)
199-
Best practice for npm/TS monorepos is **Changesets** (per-PR intent files → a
200-
reviewable "Version Packages" PR → automated bump + CHANGELOG) on a GitHub Action,
201-
plus **OIDC Trusted Publishing** (short-lived tokens + automatic provenance, no
202-
long-lived `NPM_TOKEN`). Caveat: OIDC can't drive `npm dist-tag` yet, so RC→promote
203-
still needs a token. Adopting these would replace Phases 1/3/7/9's manual steps;
204-
the preflight gate, smoke test, and stop-and-confirm in this skill stay.
198+
## OIDC Trusted Publishing — now DATED, not optional
199+
npm removes **direct publishing from 2FA-bypass tokens around January 2027**
200+
([changelog](https://github.blog/changelog/2026-07-31-restricting-npm-bypass-2fa-granular-access-tokens/));
201+
since 2026-07-31 such tokens already cannot change package access — including
202+
**deleting a dist-tag**, which breaks Phase 9's `npm dist-tag rm` and needs an
203+
interactive-2FA session. The replacement is **OIDC Trusted Publishing** via
204+
`publish-npm.yml` (which exists), the same mechanism `publish-csharp.yml` already
205+
uses for NuGet; the remaining work is a trusted-publisher registration per package
206+
on npmjs.com. Caveat: OIDC can't drive `npm dist-tag` yet, so RC→promote still needs
207+
a token.
208+
209+
Separately, and still genuinely optional: **Changesets** (per-PR intent files → a
210+
reviewable "Version Packages" PR → automated bump + CHANGELOG). Adopting it would
211+
replace Phases 1/3/7's manual steps; the preflight gate, smoke test, and
212+
stop-and-confirm in this skill stay.
213+
214+
## Phase 0 addendum — check that you CAN publish
215+
`scripts/release.mjs` now runs `npm whoami` in preflight. Do the same by hand if you
216+
are publishing manually, and use an **Automation / bypass-2FA** token: a "Publish"
217+
token passes `whoami`, shows `read-write`, passes `--dry-run`, and then `EOTP`s on
218+
every write, at which point `bun publish` falls back to a browser flow that cannot
219+
complete non-interactively (`--otp=` is ignored; a code must be piped on stdin).
220+
Verify BEFORE the version bump — the bump is committed and pushed well before the
221+
first publish, so a dead credential strands the cut with other registries already
222+
shipped. See `docs/RELEASING.md` → Prerequisites.

‎docs/RELEASING.md‎

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,8 +290,40 @@ diff explained.
290290
resolve uniformly at publish time — don't move it.
291291
- npm auth as an owner of the `metaobjectsdev` org. The account has 2FA **auth-and-writes**, so for
292292
an unattended publish use a **Granular/Automation token with the bypass-2FA option**, scoped
293-
read+write to `@metaobjectsdev`, in `~/.npmrc` (`//registry.npmjs.org/:_authToken=...`). Revoke
294-
it after the release. (Without it, every `bun publish` prompts for an OTP.)
293+
read+write to `@metaobjectsdev`, in `~/.npmrc` (`//registry.npmjs.org/:_authToken=...`).
294+
295+
> **The token TYPE is the whole game, and a wrong one looks right.** A classic **"Publish"**
296+
> token authenticates fine — `npm whoami` returns your username, `npm access list packages`
297+
> shows `read-write`, and `bun publish --dry-run` passes — and then returns **`EOTP`** on
298+
> every actual write. Worse, `bun publish` defaults to `--auth-type=web`, so it falls back to
299+
> an interactive **browser** flow (`https://www.npmjs.com/auth/cli/…`) that cannot complete
300+
> in CI or any non-interactive shell; `--otp=` is silently IGNORED (bun prompts `Enter OTP:`
301+
> regardless, so a code must be piped on **stdin**). And if the account's 2FA is a security
302+
> key rather than TOTP, there is no 6-digit code to supply at all. Use **Classic → Automation**
303+
> (or a granular token with 2FA-bypass enabled). Verify before you need it, with a reversible
304+
> write rather than a read:
305+
> ```bash
306+
> npm dist-tag add @metaobjectsdev/cli@<current-latest> auth-probe # must SUCCEED
307+
> npm dist-tag rm @metaobjectsdev/cli auth-probe
308+
> ```
309+
> **Note the second command now FAILS with a bypass-2FA token** (`403`, "Granular access
310+
> tokens that bypass two-factor authentication may not perform this action"). Since
311+
> 2026-07-31 such tokens may still *publish* but may not change package access — including
312+
> **deleting a dist-tag**. So the probe above is only half-reversible: prefer probing on a
313+
> throwaway package, or accept that removing the tag needs an interactive-2FA session.
314+
> `scripts/release.mjs` now runs `npm whoami` in phase 0 so a dead credential is caught
315+
> BEFORE the version bump is committed, not after other registries have shipped.
316+
317+
Revoking the token after each release is a deliberate trade — it is also why the credential
318+
is reliably dead at the start of the next cut. Whichever you choose, do it knowingly.
319+
320+
> **Deadline: npm removes direct publishing from 2FA-bypass tokens around January 2027**
321+
> ([changelog](https://github.blog/changelog/2026-07-31-restricting-npm-bypass-2fa-granular-access-tokens/)).
322+
> The replacement is **Trusted Publishing (OIDC)** — the same mechanism `publish-csharp.yml`
323+
> already uses for NuGet — via `publish-npm.yml`, which exists and needs a trusted-publisher
324+
> registration per package on npmjs.com. This is a dated requirement, not the "future
325+
> improvement (research-backed, not yet adopted)" that `.claude/skills/releasing/SKILL.md`
326+
> still files it under.
295327
- `bun publish` does **not** apply `publishConfig` field overrides (bin/main/exports) — only
296328
`access`/`tag` (oven-sh/bun#19205). So fields like `bin` must be correct at the top level, not
297329
swapped via `publishConfig`.

0 commit comments

Comments
 (0)