Publishing a New Package
Getting a package onto npm is not done when the workflow goes green. It is done when someone in another repo can install it and it works. Most of the failures below happen after the "publish succeeded" line.
The first publish is different
A brand-new package name will fail the first CI publish unless the token was scoped for it, and the error does not say so. npm returns:
[E404] 404 Not Found - PUT https://registry.npmjs.org/@comfyorg%2fyour-package
A 404 on PUT means the token may publish existing packages in the scope but
may not create a new name — a granular token whose write access is a
hand-picked package list cannot include a package that does not exist yet. It
reads like "the package doesn't exist" — which is true and irrelevant — and
sends you looking for a workflow bug that isn't there. --access public is
already set; the registry URL is already right.
Two ways out, both fine:
- Give the CI token read+write on the whole
@comfyorgscope — the narrowest grant that can create a new name, and preferable to all-packages access — then re-run the workflow. - Publish once by hand, then make sure the CI token covers the new name.
Trusted publishing (OIDC) cannot bootstrap either — npm requires the package to exist before a trusted publisher can be configured (npm/cli#8544). So the ordering is always: first publish by token → configure trusted publisher → switch CI to OIDC.
Never npm publish from this repo
Workspace packages use pnpm catalog specifiers:
"dependencies": { "@iconify/utils": "catalog:" }
pnpm publish rewrites those to real ranges when it packs. npm publish ships
the literal string "catalog:", and every consumer install breaks. The tarball
looks fine locally either way — the damage only shows up in the consumer.
Check before you publish anything:
cd packages/<name>
pnpm pack --pack-destination /tmp
tar -xzOf /tmp/comfyorg-<name>-<version>.tgz package/package.json | jq .dependencies
Every value must be a real range. If you see catalog:, you used the wrong tool.
Scaffolding the workflows
Copy the four-workflow set from an existing package such as design-system:
| Workflow | Role |
|---|---|
publish-<pkg>.yaml |
workflow_call + workflow_dispatch; does the publish |
publish-<pkg>-on-merge.yaml |
fires on merged PR with the Release label |
version-bump-<pkg>.yaml |
dispatch → opens a version PR labelled Release |
ci-<pkg>-pack.yaml |
on PR — typecheck + assert tarball contents |
The pack check must allowlist package.json, LICENSE, and README.md.
npm force-includes all three regardless of the files field, so a guard that
only permits the first two rejects any package that has a readme.
Before the first publish
- Write a README. Without one the npm page renders empty, which defeats publishing for another team to discover.
- Declare peer dependencies. Anything the consumer must supply — Tailwind,
Vue — belongs in
peerDependencies, notdevDependencies. A devDependency tells the consumer nothing. - Export
./package.json. Tooling reads it; anexportsmap that omits it throwsERR_PACKAGE_PATH_NOT_EXPORTED. - Check
filesagainst the exports map. Every path inexportsmust be covered byfiles, or the target is simply absent from the tarball. Nothing catches this at install time — neithernpm packnornpm installresolves export targets — so it surfaces as a consumer resolution error the first time something imports that entry.
Releasing after the first time
Run the version-bump workflow → it opens a PR labelled Release → merge it →
publish-<pkg>-on-merge publishes and posts to Slack. A manual dispatch at an
already-published version is a no-op: the Check if version already on npm
step finds it and skips. If you want to test the pipeline, you need a new
version number.
Prove it is consumable
This is the step people skip, and it is the only one that finds real problems.
In a different repo — ideally one on npm rather than pnpm, since that is the
path where catalog: would explode:
npm install @comfyorg/<name>
Then import it somewhere real, build, and grep the build output to confirm the
thing you imported actually reached the bundle. Import every entry in the
exports map while you are there — a subpath whose target never made it into
the tarball fails only here. A green build proves the import resolved; it does
not prove the values landed. For CSS, point the check at the consumer's own
build output — the path below is Nuxt's, so substitute whatever your consumer
emits:
grep -o -- "--your-token:[^;]*" .output/public/_nuxt/*.css
Open that consumer change as a PR and keep the preview link — it is the evidence that the publish worked end to end.
Trusted publishing
Once the package exists, configure it on npmjs.com under package settings:
- Organization / repository / workflow filename — use the reusable workflow
that actually runs the publish (
publish-<pkg>.yaml), not the on-merge wrapper. - Environment name — leave blank unless the publish job declares
environment:. A mismatch fails every publish. - Allow
npm publishonly.npm stage publishpublishes unlisted pending manual approval; we do not use it.
Then grant OIDC at both workflow layers — the caller job that does
uses: ./.github/workflows/publish-<pkg>.yaml, and the publish job inside the
reusable workflow. A called workflow can never hold more than the calling job
does, so setting this on the inner job alone leaves it with no token and the
publish quietly falls back to NPM_TOKEN:
permissions:
contents: read
id-token: write
Keep NODE_AUTH_TOKEN in place until an OIDC publish has actually succeeded.
[WARN] Skipped OIDC in the log means it silently fell back to the token —
treat that as a failure to chase down, not a warning to scroll past. Suspect
pnpm/action-setup first: every workflow here still pins v4.4.0
(fc06bc1257f339d1d5d8b3a19a8cae5388b55320), the version that broke pnpm's OIDC
publish in pnpm#11513 — closed once
the reporter bumped the action, not by a pnpm release. Only after a real OIDC
publish should you tighten the org to require 2FA and disallow tokens; doing it
earlier removes the only working path.
Announce it
Post the npm link, the install line, and the consumer PR preview link. "It's published" is not actionable; "here is the import and here is it working" is.