From 725534f1861a961fd0d86f616ea81fd364001f6c Mon Sep 17 00:00:00 2001 From: "open-swe[bot]" Date: Sun, 9 Aug 2026 10:59:57 -0400 Subject: [PATCH 1/2] docs: make the "Aligning @next" section consistent with the deprecation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #403 deprecated @next but left the older section below it still describing the old world: "alignment is a manual step after every release", "always verify ... must show next == latest", and a straightforward recipe for putting a token back. All three now contradict the policy two sections up. This file has already shipped an internal contradiction twice in two days and both times a reviewer caught it, so the pattern is worth naming: I keep ADDING a correct new section without reconciling what it supersedes. Now: alignment is explicitly optional and never gates a release; the expired secret is documented as not worth fixing rather than as a pending chore; the "make it automatic" recipe is retained but marked deliberately NOT recommended, with the reason (2FA-bypass tokens lose publishing ~Jan 2027, they expire silently, and it reintroduces a long-lived publish credential to an otherwise all-OIDC pipeline — the tag is deprecated, the credential would be real); and verification says `latest` is authoritative with `next` permitted to lag. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4da9bc2391..783e1638e3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -151,14 +151,15 @@ Requirements baked into the workflow: Node ≥ 22.14.0 and npm ≥ 11.5.1 (`npm **Fact 2 — the drift everyone kept chasing was a wiring bug, not the OIDC limit.** 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 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). -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. +2. **The `NPM_TOKEN` secret is expired** (created 2026-02-21; CI returns `E401 — authentication token seems to be invalid`), so the automated step is inert. Given the deprecation above this is **not worth fixing**: alignment is optional, and replacing the secret would mean adopting a credential class npm retires in ~Jan 2027. -**Manual alignment.** Run it yourself in an interactive terminal: +**Manual alignment (optional — `@next` is deprecated, so this never gates a release).** +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. +**Making it automatic again — deliberately NOT recommended.** It is possible: a Granular Access Token scoped to `@nforma.ai/nforma` with Read and write, then `gh secret set NPM_TOKEN --repo nForma-AI/nForma`, verified with `gh workflow run "Align dist-tags"` (#396, dispatchable and idempotent). Don't, unless something changes: 2FA-bypass tokens lose direct publishing on a targeted January 2027, they expire silently in the meantime, and it would put a long-lived publish-capable credential back into a pipeline that is otherwise entirely OIDC. The tag being aligned is deprecated; the credential would be real. -**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. +**Checking:** `npm view @nforma.ai/nforma dist-tags --json`. `latest` is authoritative; `next` may lag and that is expected, not a failure. CI's `Verify @next == @latest` step reports drift as a `::notice::` with the fix command, for anyone who wants to align opportunistically. From 7d162b1b2f0a62d8160e983af899dbdc9722bf74 Mon Sep 17 00:00:00 2001 From: "open-swe[bot]" Date: Sun, 9 Aug 2026 11:10:58 -0400 Subject: [PATCH 2/2] docs: sweep the whole file for stale @next claims, not just the flagged line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third instance of the pattern I named in the previous commit, caught by review again — which makes the lesson the point rather than the line. Line 141 still described CI as aligning @next "per the alias invariant". Rather than patch it and wait to be told about the next one, I grepped every mention of alias / invariant / @next in the file: - publish flow: now says the step ATTEMPTS alignment of a deprecated alias, is inert while NPM_TOKEN is expired, and is not required for a correct release. - the prerelease rule (twice, lines 10 and 106) justified itself by "the alias policy". The rule still holds — a prerelease on @latest installs for everyone — but the justification now stands on "there is only one channel" instead of an invariant that no longer exists. Fixing one flagged line at a time is what produced three rounds of this. The check that actually works is: after changing a policy, grep the file for every term that policy touches. 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 783e1638e3..feb793db43 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -7,7 +7,7 @@ nForma uses milestone-based semver (no separate prerelease channel — see dist- - `0.{milestone}` — milestone release (e.g., 0.40 = 40th milestone) - `0.{milestone}.{patch}` — quick task release within a milestone (e.g., 0.40.1, 0.40.2) -Versions with a prerelease suffix (e.g. `0.40.2-rc.1`) **cannot ship** under the `@next == @latest` alias policy — a prerelease semver string published to `@latest` would silently install `0.40.2-rc.1` for every user doing `npm install @nforma.ai/nforma@latest`. `publish.yml` rejects any version with a `-` suffix. +Versions with a prerelease suffix (e.g. `0.40.2-rc.1`) **cannot ship** — there is only one channel, so a prerelease semver published to `@latest` would silently install `0.40.2-rc.1` for every user doing `npm install @nforma.ai/nforma@latest`. `publish.yml` rejects any version with a `-` suffix. Dist-tag mapping: - `latest` — stable versions (0.44.3). **The only channel.** @@ -103,7 +103,7 @@ Quick check: `node -p "require('./package-lock.json').version"` should match `no - CHANGELOG gate: requires `## [{VERSION}]` in CHANGELOG.md - Asset staleness: `npm run check:assets` — regenerate with `npm run generate-terminal` - Lint isolation: `npm run lint:isolation` — require paths must use `$HOME/.claude/nf-bin/` with CWD fallback -- **No prerelease versions**: `package.json` version must NOT contain a `-` suffix; `publish.yml` rejects prerelease semver (the alias policy means there's no separate prerelease channel to ship them on) +- **No prerelease versions**: `package.json` version must NOT contain a `-` suffix; `publish.yml` rejects prerelease semver (there is no separate prerelease channel to ship them on) ### Troubleshooting CI failures @@ -138,7 +138,7 @@ 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; 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). +- **@latest** — push to main with a non-prerelease `package.json` version (runs tests → publish → tag → GitHub Release). A final step *attempts* to align the deprecated `@next` alias, but it is inert while `NPM_TOKEN` is expired and, per the deprecation above, is not required for the release to be correct. 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`.