Repository navigation
Publish through npm trusted publishing instead of NPM_TOKEN - #78
Merged
Merged
Conversation
release.yml no longer declares, reads or hands on an npm token. The publishing job already held id-token: write; the publish step now runs with no NPM_TOKEN or NODE_AUTH_TOKEN, so changesets/action writes no ~/.npmrc and the publishing tool exchanges the job's OIDC token at npm. setup-node in that job drops registry-url, since nothing authenticates with a file any more. publish-floor.mjs now owes the trusted-publishing floor on every arm, before anything is packed, and prints the tool that makes the exchange with its version: npm 11.5.1 and Node 22.14.0 when npm makes it (pnpm 10 hands its tarball to npm publish), pnpm 11.0.0 when pnpm publishes natively. The staging floors already clear these. The README says how publishing authenticates, what each package's trusted-publisher entry must name (its own repository and release.yml, never cosyte/.github), the owner's one-time steps on npmjs.com, and the residual: id-token: write is job-wide, so any step of the release job can mint a publish token while it runs. test/trusted-publishing.test.mjs pins the workflow half and is shown to fail on a copy of release.yml with each property broken. Harness-Task: 107 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CodeQL flagged js/incomplete-sanitization on the new direct-arm floor test: it built a RegExp from a version string and escaped only dots. A plain substring check says the same thing with nothing to escape. Harness-Task: 107 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t long Harness-Task: 107 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NSchatz
added a commit
to cosyte/config
that referenced
this pull request
Nov 7, 2026
…OKEN note (#131) npm runs `prepare` during `npm pack`, including the `npm pack --dry-run --json` whose stdout the shared attw gate parses. simple-git-hooks prints its `[INFO]` lines on stdout, so every repo scaffolded from the template was born with a `pnpm attw` that refused ("did not print JSON"). The template's `prepare` now runs `simple-git-hooks 1>&2`, as cosyte/hl7 and cosyte/pathways already do. test/attw-scaffold.test.ts runs the real scaffolder, plants a stand-in hook where `pnpm install` puts simple-git-hooks, and runs the gate's own `npm pack --dry-run --json` with `prepare` firing. It asserts the hook ran, its line is on stderr and not stdout, and stdout is one JSON document naming the package. A counterfactual case puts the old `prepare` back and shows the line in stdout and the parse failing. Seen red on the unchanged template first, then green, on npm 10, 11 and 12. The template's release.yml comment and the scaffolder's closing message both still said to set NPM_TOKEN. Publishing moved to npm trusted publishing in cosyte/.github#78, so both now name the trusted-publisher entry and the two secrets the shared workflow reads: RELEASE_PR_TOKEN and DOCS_REPO_DISPATCH_TOKEN. No package ships the template or the scaffolder, so no changeset.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
npm will stop allowing direct publishing with granular access tokens in January 2027. This moves the shared release workflow onto npm trusted publishing (OIDC). Once it merges, no long-lived npm token exists anywhere in the pipeline, and every version still gets provenance.
Merging changes every caller at once. Every caller pins
release.yml@main, so from the merge on, each caller's next release authenticates by OIDC. Each package needs its trusted-publisher entry on npmjs.com before that release, or the publish step fails with npm's own refusal and nothing is published. See "The owner's steps" below.What changed
.github/workflows/release.ymlNPM_TOKENsecret declaration is gone.NPM_TOKENorNODE_AUTH_TOKEN.setup-nodeno longer setsregistry-url.rg -n NPM_TOKEN .github/workflows/release.ymlprints nothing; comments were rewritten too.releasejob is the only job that publishes or stages, and it already grantedid-token: writeand ran onubuntu-latest. Theversionandfinalizejobs hold noid-token: write.changesets/action(v1.9.0,a45c4d59) writes no~/.npmrcand logs that it is "using npm trusted publishing" (src/index.ts:78-116, read 2026-10-19).scripts/publish-floor.mjs(the floor gate) now enforces a trusted-publishing floor on both arms, before anything is packed. It prints the tool that makes the OIDC exchange and that tool's version, and addsauth-tool,auth-tool-versionandtrusted-publishing-floorto its outputs:The staging floors (npm 11.15.0, pnpm 11.3.0) already clear these, and a test pins that.
README: the
release.ymlrow and the caller example now say how publishing authenticates. They also say that each package's trusted-publisher entry names the package's own repository andrelease.yml(the caller), nevercosyte/.github. A new section, "How a release authenticates to npm", lists the sources and the owner's steps.Tests:
test/trusted-publishing.test.mjsis new. It checks that no npm token is named or read anywhere, and that every job that publishes or stages grantsid-token: write, runs on a hosted runner and runs the floor gate first. It also checks the README. Each check is run against a copy ofrelease.ymlwith that one property broken and shown to fail. The existing assertions that pinned the token on the publish step now pin its absence.Does pnpm 10.34.5 authenticate through OIDC? Yes, through the npm it runs
Every caller pins pnpm 10.34.5 or 10.0.0. pnpm 10 has no OIDC code of its own. Its
pnpm publishpacks the tarball and then runsnpm publish --ignore-scripts <tarball>, passing its whole environment through (run-npmcopiesprocess.env). So the npm CLI performs the publish and the OIDC exchange. All sources below were read 2026-10-19:runNpm(opts.npmPath, ['publish', '--ignore-scripts', path.basename(tarballPath), ...args], ...)in releasing/plugin-commands-publishing/src/publish.ts#L278. It is the same in v10.0.0 at L246.npm publishcallsoidc({ packageName, registry, opts, config })before the upload, for a tarball as well as a directory: lib/commands/publish.js#L141.npm stage publishmakes the same call: v11.15.0 L145.releasejob runs Node 24, which bundles npm 11.16.0 or later from Node 24.18.0, and the floor gate checks the real npm at run time.pnpm stage publish(v11.3.0 #L195).Callers
I checked all 14 callers'
.github/workflows/release.yml. Every one already grantsid-token: writeon its calling job and passessecrets: inherit. No caller passesNPM_TOKENby name, so removing the declared secret breaks none of them. All 14 packages already exist on npm, so each one can get an entry now.The owner's steps on npmjs.com, with 2FA
These are not in this PR.
cosyte, the package's own repository, workflow filenamerelease.yml. The environment namereleaseis optional.npm stage publishby default, and callers use the default direct arm.NPM_TOKENsecret and revoke the token.A residual, written down rather than closed
id-token: writeis granted to the whole job.ACTIONS_ID_TOKEN_REQUEST_TOKENis therefore in every step of thereleasejob, and npm now accepts it in exchange for a publish token. Any step can mint one while the job runs: the caller'spnpm installlifecycle scripts, itsVerifyladder and itspack:docs. Before this change the npm token was handed only to the publish step, and left in~/.npmrcuntil the removal step.Four things bound this. The minted token covers one package and is short-lived. The job starts only after the
releaseenvironment approval. A stage-only entry would keep a 2FA promotion between a minted token and the live registry, but the default direct arm gives that up. Every publish carries provenance.Two ways to narrow it are recorded in the README and not done here:
env:on the steps that need neither it nor its URL. I didn't measure this on a run, so I didn't ship it.A known gap in the floor gate
The gate reads the
npmon the step's PATH beforepnpm install. pnpm 10 runsopts.npmPathif one is set. Underpnpm run, the caller'snode_modules/.binalso comes first on PATH. So a caller with annpm-pathsetting or annpmdevDependency would publish with an npm the gate never checked. No caller sampled has either.Checklist
node --test "test/*.test.mjs"(1044 pass, 0 fail) plus actionlint 1.7.12 with.github/actionlint.yaml(clean). Thepnpmline does not apply here..changeset/and noCHANGELOG.md, so there is nothing to record.NPM_TOKENsecret is removed from theworkflow_callcontract, and the README and the workflow header say so.Harness-Task: 107
🤖 Generated with Claude Code