Publishing npm Packages
Guide for creating, publishing, and automating npm package releases.
Package Setup
package.json essentials
{
"name": "@scope/package-name",
"version": "0.1.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"files": ["dist"],
"repository": {
"type": "git",
"url": "https://github.com/username/repo"
}
}
Key fields:
name— the exact package name on npm.@scope/nameis a scoped package under your npm username or org. Must be globally unique (or unique within your scope).version— must be bumped before each publish. npm rejects duplicate versions.exports—typesmust come first beforeimportandrequire.files— whitelist of files/folders included in the published tarball.repository.url— required for trusted publishing (must match your GitHub repo URL exactly).
Build tooling (tsup)
tsup is a zero-config TypeScript bundler that outputs ESM, CJS, and .d.ts declarations:
// tsup.config.ts
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/index.ts"],
format: ["esm", "cjs"],
dts: true,
clean: true,
sourcemap: true,
});
Publishing
First-time manual publish
npm login # opens browser for auth
npm run build
npm publish --access public # --access public required for scoped packages
Version bumping
npm version patch # 0.1.0 → 0.1.1 (bug fixes)
npm version minor # 0.1.0 → 0.2.0 (new features)
npm version major # 0.1.0 → 1.0.0 (breaking changes)
npm version updates both package.json and package-lock.json, and creates a git tag (e.g. v0.1.1).
Trusted Publishing (OIDC)
Trusted publishing uses OpenID Connect to authenticate CI/CD workflows directly with npm — no npm token needed.
Prerequisite: The package must already exist on npm before you can configure trusted publishing. You need to do a one-time manual publish first (see above), then set up trusted publishing for all subsequent releases.
- Short-lived, cryptographically-signed credentials (not long-lived tokens)
- Provenance attestation is generated automatically (verified badge on npmjs.com)
- Requires npm CLI >= 11.5.1 and Node >= 22.14.0
Setup on npmjs.com
- Go to your package page → Settings
- Scroll to Trusted Publisher section
- Click GitHub Actions and fill in:
- Organization or user: GitHub username
- Repository: repo name
- Workflow filename:
publish.yml(filename only, not full path) - Environment: leave blank (unless using GitHub environments)
- Save
GitHub Actions workflow
name: Publish to npm
on:
push:
branches: [main, master]
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # required for OIDC
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
with:
node-version: 24
registry-url: https://registry.npmjs.org
- run: npm ci
- run: npm test
- run: npm run build
- name: Check if version is already published
id: check
run: |
PACKAGE_NAME=$(node -p "require('./package.json').name")
PACKAGE_VERSION=$(node -p "require('./package.json').version")
if npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version 2>/dev/null; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
- name: Publish
if: steps.check.outputs.exists == 'false'
run: npm publish --access public
Key points:
id-token: writepermission is required for OIDC- No
NPM_TOKENsecret needed — authentication is handled via OIDC --provenanceflag not needed — provenance is auto-generated with trusted publishing- The version check step skips publish if the version already exists on npm
repository.urlin package.json must match the GitHub repo URL exactly, otherwise publish fails with E422
Release flow
- Bump the version:
npm version patch(or minor/major) — this is required, npm rejects duplicate versions git push origin main --tags- GitHub Action runs → tests → builds → publishes automatically
Note: A regular commit without a version bump will still trigger the workflow (tests + build), but the publish step is skipped because the version already exists on npm. Only commits that include a version bump will result in an actual publish.
Optional: maximum security
After verifying trusted publishing works:
- Go to package Settings → Publishing access
- Select “Require two-factor authentication and disallow tokens”
- Revoke any existing automation tokens
This ensures only your trusted GitHub workflow can publish.
Gotchas
- Scoped packages are private by default — always pass
--access publicon first publish exports.typesmust come first — beforeimportandrequire, otherwise bundlers may ignore it- The npm package name (
namefield) is exactly what users install — choose it carefully actions/setup-node@v4is too old for trusted publishing — use@v6- If your repo is private, provenance attestation will not be generated (npm limitation)