esc
Type to search across all notes

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/name is 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.
  • exportstypes must come first before import and require.
  • files — whitelist of files/folders included in the published tarball.
  • repository.urlrequired 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

  1. Go to your package page → Settings
  2. Scroll to Trusted Publisher section
  3. 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)
  4. 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: write permission is required for OIDC
  • No NPM_TOKEN secret needed — authentication is handled via OIDC
  • --provenance flag not needed — provenance is auto-generated with trusted publishing
  • The version check step skips publish if the version already exists on npm
  • repository.url in package.json must match the GitHub repo URL exactly, otherwise publish fails with E422

Release flow

  1. Bump the version: npm version patch (or minor/major) — this is required, npm rejects duplicate versions
  2. git push origin main --tags
  3. 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:

  1. Go to package Settings → Publishing access
  2. Select “Require two-factor authentication and disallow tokens”
  3. Revoke any existing automation tokens

This ensures only your trusted GitHub workflow can publish.

Gotchas

  • Scoped packages are private by default — always pass --access public on first publish
  • exports.types must come first — before import and require, otherwise bundlers may ignore it
  • The npm package name (name field) is exactly what users install — choose it carefully
  • actions/setup-node@v4 is too old for trusted publishing — use @v6
  • If your repo is private, provenance attestation will not be generated (npm limitation)