Hook
I hit one of the most frustrating CI/CD problems while publishing a package from a modern pnpm monorepo.
Everything looked correct.
The workflow passed. OIDC was detected. Changesets worked. The package already existed on npm.
And yet publishing kept failing with this:
E404 Not Found - PUT https://registry.npmjs.org/@pycolors%2ftokens - Not foundBefore that, I was also hitting:
EOTP This operation requires a one-time password from your authenticator.The worst part?
The errors were misleading.
The actual issue was not the package itself.
It was the migration from legacy NPM_TOKEN publishing to npm Trusted Publishing (OIDC) with GitHub Actions.
Here’s the full production-ready setup that finally fixed it.
Context
This happened while publishing packages inside the PyColors ecosystem:
- Next.js 15
- pnpm workspaces
- Turborepo
- Changesets
- GitHub Actions
- npm Trusted Publishing
- TypeScript
- Node.js 22
The monorepo contains multiple packages:
@pycolors/ui
@pycolors/tokens
@pycolors/eslint-config
@pycolors/typescript-configThe release pipeline automatically publishes unpublished packages to npm using Changesets.
The Problem
The original workflow used a classic npm token:
env:
NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}"This worked for a while.
Then npm started failing with:
EOTP This operation requires a one-time password from your authenticator.I switched to npm Trusted Publishing with OIDC.
GitHub Actions then detected OIDC correctly:
No NPM_TOKEN found, but OIDC is available - using npm trusted publishingBut publishing still failed:
E404 Not Found - PUT https://registry.npmjs.org/@pycolors%2ftokens - Not foundThe package already existed on npm:
npm view @pycolors/tokens version
# 1.1.0Local version:
{
"name": "@pycolors/tokens",
"version": "1.2.0"
}So why was npm saying the package was "not found"?
Why This Error Is So Confusing
This is not a normal 404.
With npm Trusted Publishing, npm sometimes returns:
E404when the OIDC authentication chain is invalid.
That means:
- wrong workflow filename
- unsupported Node/npm version
- Trusted Publisher mismatch
- workflow not authorized
- package not linked correctly
The error message hides the real cause.
This makes debugging painful.
Root Cause
I actually had multiple problems at the same time.
1. Wrong Trusted Publisher Workflow Filename
I initially configured:
.github/workflows/release.ymlBut npm expects:
release.ymlOnly the filename.
Not the full path.
2. Node.js / npm Version Too Old
Trusted Publishing requires recent tooling.
I was using:
node-version: 20npm Trusted Publishing behaves inconsistently with older npm versions.
After upgrading:
node-version: 22.14.0and:
- run: npm install -g npm@latesteverything started working correctly.
3. GitHub Workflow Was Ignored
My .gitignore contained this:
.github/
**/.github/Which meant:
- workflows were not versioned
- CI changes were not tracked
- workflows were inconsistently deployed
This is a terrible idea for modern SaaS infrastructure.
4. Trusted Publisher Was Configured on the Wrong Repository
This one is subtle.
My package repository metadata pointed to:
"repository": {
"url": "https://github.com/pycolors-io/pycolors-tokens"
}But the actual publishing workflow runs from:
pycolors-io/pycolorsTrusted Publishing must point to the repository that executes the workflow.
Not the mirror repository.
The Solution
Here’s the final production-ready setup.
Step 1 — Configure npm Trusted Publishing
On npm:
@pycolors/tokens
→ Settings
→ Trusted PublisherAdd:
Organization or user: pycolors-io
Repository: pycolors
Workflow filename: release.yml
Environment: leave empty
Allowed actions: Allow npm publishImportant:
Workflow filename = release.ymlNOT:
.github/workflows/release.ymlStep 2 — Remove Legacy NPM_TOKEN Publishing
Delete this entirely:
NODE_AUTH_TOKEN: "${{ secrets.NPM_TOKEN }}"Also remove:
- name: "Test npm auth"
run: npm whoamiOIDC does not use npm tokens.
Step 3 — Enable OIDC Permissions
Your workflow must contain:
permissions:
contents: write
pull-requests: write
id-token: writeThe critical permission is:
id-token: writeWithout it, OIDC cannot work.
Step 4 — Upgrade Node.js and npm
This part matters more than most developers realize.
Use Node.js 22+:
- name: "Setup Node"
uses: actions/setup-node@v4
with:
node-version: 22.14.0
registry-url: "https://registry.npmjs.org"
cache: "pnpm"Then immediately upgrade npm:
- name: "Upgrade npm"
run: npm install -g npm@latestStep 5 — Use Changesets with OIDC
Final publishing step:
- name: "Version or Publish (Changesets)"
uses: changesets/action@v1
with:
version: "pnpm -w changeset version"
publish: "pnpm -w changeset publish"
title: "chore(release): version packages"
commit: "chore(release): version packages"
env:
GITHUB_TOKEN: "${{ steps.app-token.outputs.token }}"No npm token needed.
Final Working Workflow
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
run_install: false
- uses: actions/setup-node@v4
with:
node-version: 22.14.0
registry-url: "https://registry.npmjs.org"
cache: "pnpm"
- run: npm install -g npm@latest
- run: pnpm install --frozen-lockfile
- run: pnpm -w build
- uses: changesets/action@v1
with:
version: "pnpm -w changeset version"
publish: "pnpm -w changeset publish"Before / After
Before
- OTP failures
- Broken CI publishing
- Manual npm tokens
- Misleading E404 errors
- Security risks
- Inconsistent workflow tracking
After
- Zero npm tokens
- Fully automated publishing
- Modern OIDC authentication
- Secure GitHub Actions pipeline
- Production-ready monorepo releases
- Cleaner infrastructure
Lessons Learned
1. npm Errors Can Be Extremely Misleading
This:
E404 Not Founddid not actually mean the package was missing.
It meant the OIDC authorization chain was invalid.
2. Modern CI/CD Requires Modern Node/npm Versions
A lot of developers upgrade frameworks but forget infrastructure tooling.
npm Trusted Publishing is evolving quickly.
Old npm versions can fail in strange ways.
3. .github/ Should Usually Be Versioned
Ignoring GitHub workflows is an anti-pattern for modern SaaS teams.
Your CI/CD pipeline is part of your product infrastructure.
Treat it like production code.
4. Monorepo Mirrors Add Complexity
When using public mirrors:
pycolors-tokens
pycolors-uiyou must clearly separate:
- repository metadata
- actual publishing repository
- CI execution source
Otherwise authentication becomes confusing.
SaaS / Builder Insight
This problem is exactly why infrastructure maturity matters in SaaS products.
Most developers think shipping faster only means:
- UI speed
- coding faster
- AI tooling
But operational reliability matters just as much.
Broken release pipelines destroy momentum.
A modern SaaS stack is not just:
- Next.js
- Tailwind
- Prisma
- Stripe
It’s also:
- reproducible CI/CD
- automated publishing
- secure package distribution
- scalable monorepo workflows
The teams that ship consistently are the teams that reduce infrastructure friction early.
Upgrade Path
As your SaaS ecosystem grows:
- multiple packages
- design systems
- starters
- templates
- shared configs
release automation becomes critical.
This is exactly why production-ready SaaS starters matter.
The difference between:
"it works locally"and:
"this scales operationally"is where real engineering leverage happens.
PyColors focuses heavily on this layer:
- production-ready monorepos
- modern CI/CD
- scalable package architecture
- SaaS infrastructure defaults
- premium developer workflows
Because shipping is not enough.
You need to keep shipping reliably.