Skip to content

CI: publish to npm via trusted publishing instead of NPM_TOKEN - #95

Merged
11bit merged 1 commit into
mainfrom
ci/npm-trusted-publishing
Aug 27, 2026
Merged

11bit merged 1 commit into
mainfrom
ci/npm-trusted-publishing

Conversation

@11bit

@11bit 11bit commented Aug 27, 2026 •

Copy link
Copy Markdown
Collaborator

Why

The 1.9.0 release run failed at the publish step:

🦋 error an error occurred while publishing @imgproxy/imgproxy-js-core:
   E404 Not Found - PUT https://registry.npmjs.org/@imgproxy%2fimgproxy-js-core

The package exists (1.8.0 is latest, 17 versions published), so a 404 on a PUT is not "package missing" — npm returns 404 rather than 403 on writes to avoid leaking package existence to unauthorized callers. The secret reached the runner (No user .npmrc file found, creating one with NPM_TOKEN used as auth token), so NPM_TOKEN is set but expired or no longer has write access to the @imgproxy scope.

Rather than rotate a secret that will expire again, this switches to npm trusted publishing: npm exchanges the GitHub-issued OIDC token for a short-lived credential scoped to a single workflow run. Nothing publishable is left at rest in repo secrets.

What changed

  • Drop NPM_TOKEN. changesets/action has supported OIDC since v1.7.0 — with no NPM_TOKEN but the OIDC env present, it skips writing .npmrc and lets npm authenticate itself (source). No migration to changesets/action@v2 is needed, which is good: v2 requires Changesets v3 and we're on v2.28.
  • Node 18 → 24. This is the hard requirement: OIDC publishing needs npm >= 11.5.1, and Node 22 still ships npm 10. Node 24.20.0 (LTS Krypton) ships npm 11.19.0.
  • actions/checkout@v4 → @v7, actions/setup-node@v3 → @v7. Clears the Node 20 runtime deprecation warnings on the run. Neither major's breaking changes apply here (checkout v7 restricts fork checkout under pull_request_target/workflow_run; setup-node v5/v6 changed automatic-cache defaults, and we set cache: "npm" explicitly).
  • Drop the redundant GITHUB_TOKEN env var, which empties the step's env: block entirely. This is tidiness, not a security change: unlike NPM_TOKEN, secrets.GITHUB_TOKEN is not a stored credential — GitHub mints it per run and revokes it at job end. The action still needs a token to open the version PR, push the release commit and tag, and create the release; it just gets it from the github-token input, which defaults to ${{ github.token }} in both v1 and v2 — the same token. The permissions: block is what actually scopes what it can do.
  • id-token: write becomes meaningful. The permission and its "needed for provenance" comment were already there, but nothing enabled provenance — there's no --provenance flag or publishConfig.provenance. Under trusted publishing from a public repo, provenance attestations are generated automatically.

Required before merging

⚠️ A package maintainer must register the trusted publisher first, or the next publish will fail with the same 404.

On npmjs.com → @imgproxy/imgproxy-js-core → Settings → Trusted Publisher → GitHub Actions:

Field Value
Organization or user imgproxy
Repository imgproxy-js-core
Workflow filename publish.yml
Environment name (leave empty)
Allowed actions npm publish

This is one time. It doesn't expire, isn't tied to the person who set it up, and requires nothing per release. It only needs revisiting if the workflow filename, repo name, or org changes — npm matches those claims from the OIDC token. Leave the environment field empty: adding one would gate every release on a manual approval, and npm warns approval delays can cause OIDC timing issues.

Optional hardening afterwards, once a release has gone through: set the package to "Require two-factor authentication and disallow tokens", which disables token-based publishing entirely while trusted publishing keeps working.

Effect on 1.9.0

f0c8031 already landed the version bump — package.json is at 1.9.0, changesets are consumed, CHANGELOG.md is written — but nothing was published and no v1.9.0 tag or GitHub release was created. There's deliberately no changeset in this PR, so merging it pushes to main, and the publish job will find no changesets, notice 1.9.0 is unpublished, and publish it with provenance plus the tag and release. No token rotation needed at all if the trusted publisher is registered first.

Verification

The publish workflow only runs on main, so its build step wouldn't be exercised until merge. I ran it locally against a clean clone on Node 24.20.0 / npm 11.19.0:

$ npm ci && npm run build
vite v6.1.1 building for production...
✓ 110 modules transformed.
dist/imgproxy-js-core.mjs  57.89 kB │ gzip: 11.33 kB
dist/imgproxy-js-core.cjs  43.08 kB │ gzip: 10.42 kB
✓ built in 144ms
Done in 1.45s

npm ci warns that esbuild's and fsevents' install scripts weren't run — that's npm 11's new default script gating, not a config difference, and it's harmless here since Vite 6 resolves esbuild's binary through optional dependencies. The build passes.

Not in scope

.github/workflows/ci.yml still uses actions/checkout@v3 and Node 18 and emits the same deprecation warnings. I left it alone because bumping its Node version changes what the test suite actually runs against — worth a separate PR deciding which versions to support.

🤖 Generated with Claude Code

https://claude.ai/code/session_015jbnNY5DdggoZnmjnA8fmT

The 1.9.0 release failed with `E404 Not Found - PUT` from the registry,
which is npm's response to a write from an unauthorized caller — the
NPM_TOKEN secret is expired or no longer has write access to the scope.

Replace the stored token with npm trusted publishing (OIDC). npm
exchanges the GitHub-issued OIDC token for a short-lived, run-scoped
publish credential, so there is no long-lived secret left to expire.

- Bump Node to 24: npm >= 11.5.1 is required for OIDC publishing and
  Node 22 still ships npm 10. Node 24.20.0 ships npm 11.19.0.
- Bump actions/checkout and actions/setup-node to v7, clearing the
  Node 20 runtime deprecation warnings.
- Drop NPM_TOKEN. changesets/action has supported OIDC since v1.7.0:
  with no NPM_TOKEN and the OIDC env present it skips writing .npmrc
  and lets npm authenticate itself.
- Drop the now-redundant GITHUB_TOKEN env var. The action's
  `github-token` input already defaults to `${{ github.token }}`, the
  same per-run token, in both v1 and v2. The `permissions` block is
  what actually scopes it.

This also makes the pre-existing `id-token: write` permission
meaningful — provenance attestations are now generated automatically,
which the comment claimed but nothing enabled.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015jbnNY5DdggoZnmjnA8fmT
@11bit
11bit force-pushed the ci/npm-trusted-publishing branch from 9bb18c2 to 915a7d2 Compare August 27, 2026 09:47
@11bit
11bit merged commit bc89434 into main Aug 27, 2026
1 check passed
@11bit
11bit deleted the ci/npm-trusted-publishing branch August 27, 2026 09:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant