From 906977e7a2498ebf5993c452ebbfc3f7d48fb106 Mon Sep 17 00:00:00 2001 From: "open-swe[bot]" Date: Sat, 8 Aug 2026 09:08:53 -0400 Subject: [PATCH 1/2] docs(release): correct the @next alignment story and record the manual step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLAUDE.md carried an explanation that #391 disproved: "OIDC authorizes npm publish but not npm dist-tag add". That framing survived months of recurring drift and sent every investigation (including mine) chasing OIDC. Two separate causes, in order: 1. A wiring bug, now fixed: the align step exported NPM_TOKEN as a bare env var, which npm ignores, so dist-tag add ran unauthenticated and 401'd every release while the warning blamed a healthy token. 2. The NPM_TOKEN secret is expired (created 2026-02-21; CI returns E401), so alignment is a MANUAL step for now. Accepted trade-off, documented as such rather than left as a mystery. Records what actually works, since it is non-obvious and was re-derived twice: the interactive `npm dist-tag add` browser flow, and the fact that --otp only applies to authenticator-app 2FA — with a security key there is no 6-digit code at all, which is why an agent cannot complete this unattended. Also removes a now-contradictory claim two sections up that "there is no NPM_TOKEN secret": the publish uses OIDC and no token, but the alignment step does use one, and the file previously asserted both. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 86d437816d..8c4c99cbba 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -13,7 +13,7 @@ Dist-tag mapping: - `latest` — stable versions (0.40.1) - `next` — alias for `latest` (always points to the same version; see invariant below) -**Invariant: `next` is an alias for `latest`** — both dist-tags point to the same tarball at all times; there is no separate prerelease channel. When `@latest` is updated, `@next` must be moved to the same version. This is automated in `publish.yml` (best-effort under OIDC; see caveat below). Verify after every release: +**Invariant: `next` is an alias for `latest`** — both dist-tags point to the same tarball at all times; there is no separate prerelease channel. When `@latest` is updated, `@next` must be moved to the same version. This is automated in `publish.yml`, but the automation is currently blocked on an expired `NPM_TOKEN` secret, so alignment is a manual step for now — see "Aligning `@next`" below. Verify after every release: ```bash npm view @nforma.ai/nforma dist-tags --json # latest and next must show the same version @@ -101,11 +101,24 @@ Quick check: `node -p "require('./package-lock.json').version"` should match `no ## Publishing (npm OIDC trusted publisher) -The single `publish.yml` workflow publishes the single `@latest` channel via **GitHub OIDC — there is no `NPM_TOKEN` secret**: +The single `publish.yml` workflow publishes the single `@latest` channel via **GitHub OIDC**. The *publish* itself uses no token — but an `NPM_TOKEN` secret **does** exist and is used by the `@next` alignment step alone, because OIDC does not cover `npm dist-tag add`: - **@latest** — push to main with a non-prerelease `package.json` version (runs tests → publish → tag → GitHub Release → align `@next` to match `@latest` per the alias invariant). npm's trusted publisher (npmjs.com → package Settings) must match this file exactly: `Org=nForma-AI · Repo=nForma · Workflow filename=publish.yml · Environment=npm-publish · Allowed=npm publish`. Requirements baked into the workflow: Node ≥ 22.14.0 and npm ≥ 11.5.1 (`npm i -g npm@latest`), `permissions: id-token: write`, **no** `NODE_AUTH_TOKEN` (its presence forces the token path and defeats OIDC). -**Caveat — dist-tag under OIDC:** trusted publishing authorizes `npm publish`, not necessarily `npm dist-tag add`. The `@next` alignment step is therefore best-effort (non-fatal): if it can't move the tag under OIDC, the publish still succeeds and CI emits a warning — align manually with `npm dist-tag add @nforma.ai/nforma@{VERSION} next`. Always verify the invariant after a release: `npm view @nforma.ai/nforma dist-tags` (must show `next == latest`). +**Aligning `@next` — current state (2026-08-08).** The long-standing explanation ("OIDC authorizes `npm publish` but not `npm dist-tag add`") was **wrong** and cost real time. Two separate causes, in order: + +1. **Wiring bug (fixed, #391).** The align step exported `NPM_TOKEN` as a bare `env:` var. npm does not read `NPM_TOKEN` — auth comes from `.npmrc` — so `dist-tag add` ran *unauthenticated* and 401'd every release while the warning blamed a token that was fine. It now writes `//registry.npmjs.org/:_authToken=${NPM_TOKEN}` for that step only (safe: it runs after the OIDC publish, so it cannot force publish onto the token path). +2. **The `NPM_TOKEN` secret is currently expired** (created 2026-02-21; CI returns `E401 — authentication token seems to be invalid`). Until it is replaced, **alignment is a manual step after every release** — an accepted trade-off, not an unknown. + +**Manual alignment.** Run it yourself in an interactive terminal: +```bash +npm dist-tag add @nforma.ai/nforma@{VERSION} next +``` +It prints an `npmjs.com/auth/cli/...` URL; approve in the browser and it completes. Note `--otp=` only works if your npm 2FA is an authenticator app — with a **security key / passkey** there is no 6-digit code and the browser flow is the only path. This also cannot be run unattended by an agent, which is why it needs a human. + +**To make it automatic again:** create a Granular Access Token on npmjs.com scoped to `@nforma.ai/nforma` with **Read and write** (automation tokens are 2FA-exempt), then `gh secret set NPM_TOKEN --repo nForma-AI/nForma`. Verify without waiting for a release: `gh workflow run "Align dist-tags"` — a dispatchable, idempotent workflow (#396) that repairs drift and fails loudly if the tag did not move. + +**Always verify after a release:** `npm view @nforma.ai/nforma dist-tags --json` must show `next == latest`. CI's `Verify @next == @latest` step also reports drift explicitly with the fix command. From f1c3850b0f74955e716fdc4d063b32c1d3e7ccea Mon Sep 17 00:00:00 2001 From: "open-swe[bot]" Date: Sat, 8 Aug 2026 09:19:37 -0400 Subject: [PATCH 2/2] docs(release): describe the OIDC explanation as unverified, not wrong MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review catch, and it is a correctness point rather than wording: I overclaimed. What #391 proved is that the OBSERVED 401s came from the wiring bug — the align step exported NPM_TOKEN as a bare env var, which npm ignores, so the call ran unauthenticated. It never established that OIDC alone *cannot* move a dist-tag, because the step never got far enough to test it. Calling that explanation "wrong" also left the file self-contradictory: two sections up I had just reintroduced the same OIDC-limitation claim as the reason NPM_TOKEN exists. Both reviewers flagged it. Now: the OIDC explanation is described as incomplete and unverified, with the question explicitly left open (it costs nothing — the step uses a token either way), and line 104 states what the step does rather than asserting a capability limit. The same overclaim had already reached the memory store, where it would be recalled as established fact; corrected there too. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8c4c99cbba..d781c99f83 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -101,16 +101,16 @@ Quick check: `node -p "require('./package-lock.json').version"` should match `no ## Publishing (npm OIDC trusted publisher) -The single `publish.yml` workflow publishes the single `@latest` channel via **GitHub OIDC**. The *publish* itself uses no token — but an `NPM_TOKEN` secret **does** exist and is used by the `@next` alignment step alone, because OIDC does not cover `npm dist-tag add`: +The single `publish.yml` workflow publishes the single `@latest` channel via **GitHub OIDC**. The *publish* itself uses no token; the `@next` alignment step is the one place that uses token auth, via a temporary `.npmrc` (see "Aligning `@next`" below for why): - **@latest** — push to main with a non-prerelease `package.json` version (runs tests → publish → tag → GitHub Release → align `@next` to match `@latest` per the alias invariant). npm's trusted publisher (npmjs.com → package Settings) must match this file exactly: `Org=nForma-AI · Repo=nForma · Workflow filename=publish.yml · Environment=npm-publish · Allowed=npm publish`. Requirements baked into the workflow: Node ≥ 22.14.0 and npm ≥ 11.5.1 (`npm i -g npm@latest`), `permissions: id-token: write`, **no** `NODE_AUTH_TOKEN` (its presence forces the token path and defeats OIDC). -**Aligning `@next` — current state (2026-08-08).** The long-standing explanation ("OIDC authorizes `npm publish` but not `npm dist-tag add`") was **wrong** and cost real time. Two separate causes, in order: +**Aligning `@next` — current state (2026-08-08).** The long-standing explanation ("OIDC authorizes `npm publish` but not `npm dist-tag add`") is **incomplete, and was not the operative cause** of the drift everyone kept chasing. It may well be true — it has never been tested here, because the step never got far enough to find out. What was actually happening, in order: -1. **Wiring bug (fixed, #391).** The align step exported `NPM_TOKEN` as a bare `env:` var. npm does not read `NPM_TOKEN` — auth comes from `.npmrc` — so `dist-tag add` ran *unauthenticated* and 401'd every release while the warning blamed a token that was fine. It now writes `//registry.npmjs.org/:_authToken=${NPM_TOKEN}` for that step only (safe: it runs after the OIDC publish, so it cannot force publish onto the token path). +1. **Wiring bug (fixed, #391).** The align step exported `NPM_TOKEN` as a bare `env:` var. npm does not read `NPM_TOKEN` — auth comes from `.npmrc` — so `dist-tag add` ran *unauthenticated* and 401'd every release, while the warning text blamed a token that was configured and healthy. The step now writes `//registry.npmjs.org/:_authToken=${NPM_TOKEN}` for its own duration (safe: it runs after the OIDC publish, so it cannot force publish onto the token path). Whether OIDC alone would suffice is still unverified — the step uses a token, so the question stays open and costs nothing. 2. **The `NPM_TOKEN` secret is currently expired** (created 2026-02-21; CI returns `E401 — authentication token seems to be invalid`). Until it is replaced, **alignment is a manual step after every release** — an accepted trade-off, not an unknown. **Manual alignment.** Run it yourself in an interactive terminal: