npm Package Publishing
How to publish @zaflun/lumio-* packages to npmjs.com.
Packages
| Package | npm Name | Directory |
|---|---|---|
| Protocol | @zaflun/lumio-protocol | packages/protocol |
| Extension Types | @zaflun/lumio-extension-types | packages/extension-types |
| SDK | @zaflun/lumio-sdk | packages/sdk |
| Devkit | @zaflun/lumio-devkit | packages/devkit |
| CLI | @zaflun/lumio-cli | packages/cli |
| Agent Skill | @zaflun/lumio-agent-skill | packages/agent-skill |
Each package carries its own independent SemVer (0.x / 1.x …), NOT the Lumio YYYY.M.PATCH product version, and is not in lockstep with the API or the apps. Bump a package's version in the same pull request that changes it - unlike the product version, which the release step owns.
Prerequisites (one-time setup)
Publishing uses npm Trusted Publishing (OIDC). There is no npm token and no NPM_TOKEN secret: the job requests id-token: write, and the npm CLI exchanges the GitHub OIDC token for a short-lived publish token that is valid for one package and one run.
-
npm Organization: the
@zaflunorg on npmjs.com, with the package maintainer as a member. -
Trusted publisher, once per package: on the package page → Settings → Trusted Publisher → GitHub Actions:
Field Value Organization or user zaflunRepository lumioWorkflow filename publish-npm.ymlEnvironment name empty Allowed actions Direct npm publish A new trusted publisher only allows staged publishes by default. Without Direct npm publish the publish step fails with
E403 … OIDC permission denied for this action.npm dist-tagis not needed:publishsetslatestitself. -
A brand-new package cannot get a trusted publisher before it exists on npm. Publish its first version by hand, then add the trusted publisher:
- Publish its workspace dependencies first (an
allrun publishes them and stops at the new package).pnpm publishrewritesworkspace:*to the dependency's version, so a dependency that is not on npm yet makes the new package uninstallable. - On a clean checkout of the release commit:
pnpm install --frozen-lockfile, thenpnpm --filter "@zaflun/<name>..." build. npm loginwith 2FA, thenpnpm --filter @zaflun/<name> publish --no-git-checks --access public. Usepnpm, notnpm publish: only pnpm rewrites theworkspace:ranges.--no-git-checksis needed because releases are cut fromnext, and pnpm otherwise only publishes frommain/master.- Add the trusted publisher as in step 2, then run the workflow again for the packages that come after it.
- Publish its workspace dependencies first (an
npm allows only one workflow file per package as trusted publisher, which is why both single and all-packages releases run from publish-npm.yml. Renaming that file breaks publishing until every package's trusted publisher is updated. The workflow must stay on a GitHub-hosted runner.
Releases carry no provenance attestation: npm only issues provenance for public source repositories, and zaflun/lumio is private. The npm package page still shows the publisher as GitHub Actions via a trusted publisher.
Publishing a Single Package
Via Tag (recommended)
# 1. Bump version in package.json
cd packages/protocol
# Edit "version": "1.0.0" → "1.0.1"
# 2. Commit
git add packages/protocol/package.json
git commit -m "chore(protocol): bump version to 1.0.1"
# 3. Create tag (format: @zaflun/{package-name}@{version})
# 4. Push (triggers publish-npm.yml workflow)
git push origin next --tags
The workflow runs: Install (--frozen-lockfile) → dependency-confusion guard (scripts/supply-chain/check-dependency-confusion.mjs, fail-closed) → Build (the package and its workspace dependencies, via the ... filter suffix) → Test → Typecheck → Publish (--access public).
Via Manual Dispatch
- Go to GitHub → Actions → "Publish npm Package"
- Click "Run workflow"
- Select the package from the dropdown (e.g.
protocol) - Optionally tick Dry run (build, test and typecheck only, no publish)
- Click "Run workflow"
Via gh CLI
gh workflow run "Publish npm Package" --ref next --field package=protocol
Publishing All Packages
Dispatch the same "Publish npm Package" workflow with package = all. One job builds, tests and typechecks every package, then publishes them one after another in dependency order:
protocol → extension-types → agent-skill → sdk → devkit → cli
Nothing is published unless every package passes. A version that is already on npm is skipped with a notice, so a run that failed halfway can simply be started again. (A single-package run still fails on an existing version.)
# Dry run (build + test + typecheck only):
gh workflow run "Publish npm Package" --ref next --field package=all --field dry_run=true
# Actual publish:
gh workflow run "Publish npm Package" --ref next --field package=all
Dependency Order
When publishing for the first time or after breaking changes, publish in this order:
| Order | Package | Depends On |
|---|---|---|
| 1 | @zaflun/lumio-protocol | - |
| 2 | @zaflun/lumio-extension-types | - |
| 3 | @zaflun/lumio-agent-skill | - |
| 4 | @zaflun/lumio-sdk | protocol + extension-types |
| 5 | @zaflun/lumio-devkit | protocol (peer: sdk) |
| 6 | @zaflun/lumio-cli | extension-types |
package: all publishes in exactly this order.
Version Bumping
# Patch (bug fix): 0.6.4 → 0.6.5
# Minor (new feature): 0.6.4 → 0.7.0
# Major (breaking change): 0.6.4 → 1.0.0
Edit "version" in the package's package.json directly. No automated version bump tool. Do not set it to the product YYYY.M.PATCH version - packages/* are versioned independently.
Verifying a Release
After publish, verify on npm:
npm view @zaflun/lumio-protocol
npm view @zaflun/lumio-sdk version
Or visit: https://www.npmjs.com/package/@zaflun/lumio-sdk
Testing Locally Before Publish
# Build
cd packages/sdk && pnpm build
# Test
pnpm test
# Typecheck
pnpm typecheck
# Or dispatch "Publish npm Package" with package=all and dry_run=true
Workspace Dependencies
Packages reference each other via "workspace:*" in package.json:
"dependencies": {
"@zaflun/lumio-protocol": "workspace:*"
}
When pnpm publish runs, it automatically replaces workspace:* with the actual version (e.g. ^1.0.0). This means the referenced package must already be published on npm.
Troubleshooting
"No projects matched the filters"
The package name in pnpm-workspace.yaml doesn't match. Verify:
packages/*is listed inpnpm-workspace.yaml- The
"name"field inpackage.jsonmatches@zaflun/lumio-*
"404 Not Found - PUT …"
npm reports a publish it cannot authenticate as E404, not 401. With Trusted Publishing that means the OIDC exchange did not match a trusted publisher. Check on the package's Settings → Trusted Publisher page:
- Organization
zaflun, repositorylumio(case-sensitive) - Workflow filename exactly
publish-npm.yml, without.github/workflows/ - Environment name empty
- The run comes from
zaflun/lumioon a GitHub-hosted runner
"403 Forbidden - … OIDC permission denied for this action"
The OIDC exchange worked, but the trusted publisher does not allow a direct publish. Edit it and tick Direct npm publish under Allowed actions.
npmjs.com still shows the old version as current
The package page is cached separately from the registry. Check the registry itself: npm view @zaflun/lumio-protocol dist-tags. The page catches up within minutes.
"EPUBLISHCONFLICT" / "Cannot publish over existing version"
The version already exists on npm. Bump the version number and try again.
Workflow Files
| Workflow | File | Trigger |
|---|---|---|
| Single package or all | .github/workflows/publish-npm.yml | Push of a tag matching @zaflun/**@*, or manual dispatch with a package choice input (one package or all) and a dry_run boolean |
It runs on Node 24 with pnpm, on ubuntu-latest (npm Trusted Publishing does not accept self-hosted runners), with contents: read + id-token: write. Runs never overlap (concurrency: publish-npm). See CI runner.