Skip to main content

npm Package Publishing

How to publish @zaflun/lumio-* packages to npmjs.com.

Packages​

Packagenpm NameDirectory
Protocol@zaflun/lumio-protocolpackages/protocol
Extension Types@zaflun/lumio-extension-typespackages/extension-types
SDK@zaflun/lumio-sdkpackages/sdk
Devkit@zaflun/lumio-devkitpackages/devkit
CLI@zaflun/lumio-clipackages/cli
Agent Skill@zaflun/lumio-agent-skillpackages/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.

  1. npm Organization: the @zaflun org on npmjs.com, with the package maintainer as a member.

  2. Trusted publisher, once per package: on the package page → Settings → Trusted Publisher → GitHub Actions:

    FieldValue
    Organization or userzaflun
    Repositorylumio
    Workflow filenamepublish-npm.yml
    Environment nameempty
    Allowed actionsDirect 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-tag is not needed: publish sets latest itself.

  3. 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:

    1. Publish its workspace dependencies first (an all run publishes them and stops at the new package). pnpm publish rewrites workspace:* to the dependency's version, so a dependency that is not on npm yet makes the new package uninstallable.
    2. On a clean checkout of the release commit: pnpm install --frozen-lockfile, then pnpm --filter "@zaflun/<name>..." build.
    3. npm login with 2FA, then pnpm --filter @zaflun/<name> publish --no-git-checks --access public. Use pnpm, not npm publish: only pnpm rewrites the workspace: ranges. --no-git-checks is needed because releases are cut from next, and pnpm otherwise only publishes from main/master.
    4. Add the trusted publisher as in step 2, then run the workflow again for the packages that come after it.

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​

# 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})
git tag "@zaflun/lumio-protocol@1.0.1"

# 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​

  1. Go to GitHub → Actions → "Publish npm Package"
  2. Click "Run workflow"
  3. Select the package from the dropdown (e.g. protocol)
  4. Optionally tick Dry run (build, test and typecheck only, no publish)
  5. 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:

OrderPackageDepends On
1@zaflun/lumio-protocol-
2@zaflun/lumio-extension-types-
3@zaflun/lumio-agent-skill-
4@zaflun/lumio-sdkprotocol + extension-types
5@zaflun/lumio-devkitprotocol (peer: sdk)
6@zaflun/lumio-cliextension-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 in pnpm-workspace.yaml
  • The "name" field in package.json matches @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, repository lumio (case-sensitive)
  • Workflow filename exactly publish-npm.yml, without .github/workflows/
  • Environment name empty
  • The run comes from zaflun/lumio on 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​

WorkflowFileTrigger
Single package or all.github/workflows/publish-npm.ymlPush 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.