github-actions

GitHub Actions Publish to npm

GitHub Actions workflow to automatically publish packages to npm on release with provenance, version validation, and scoped access.

Overview

A GitHub Actions workflow that publishes your package to npm when a GitHub release is created. Includes test validation, version consistency checks, npm provenance for supply chain security, and support for scoped packages.

Configuration

# .github/workflows/publish.yml

name: Publish to npm

on:
  release:
    types: [published]                 # Trigger on GitHub release creation

# Only one publish at a time
concurrency:
  group: npm-publish
  cancel-in-progress: false            # Never cancel a publish in progress

jobs:
  validate:
    name: Validate
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Run linting
        run: npm run lint

      - name: Run type checking
        run: npx tsc --noEmit

      - name: Run tests
        run: npm test -- --ci

      - name: Build package
        run: npm run build

      # Verify the version in package.json matches the release tag
      - name: Validate version
        run: |
          PACKAGE_VERSION=$(node -p "require('./package.json').version")
          TAG_VERSION="${{ github.event.release.tag_name }}"
          # Strip 'v' prefix from tag if present
          TAG_VERSION="${TAG_VERSION#v}"
          if [ "$PACKAGE_VERSION" != "$TAG_VERSION" ]; then
            echo "::error::Version mismatch: package.json=$PACKAGE_VERSION, tag=$TAG_VERSION"
            exit 1
          fi
          echo "Version validated: $PACKAGE_VERSION"

  publish:
    name: Publish
    runs-on: ubuntu-latest
    needs: validate

    permissions:
      contents: read
      id-token: write                  # Required for npm provenance

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
          registry-url: https://registry.npmjs.org  # Configure npm registry

      - name: Install dependencies
        run: npm ci

      - name: Build package
        run: npm run build

      # Verify package contents before publishing
      - name: Check package contents
        run: |
          npm pack --dry-run
          echo "---"
          echo "Package size:"
          npm pack 2>&1 | tail -1

      # Publish with provenance (supply chain attestation)
      - name: Publish to npm
        run: npm publish --provenance --access public
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

      # Verify the package was published
      - name: Verify publication
        run: |
          sleep 10
          PACKAGE_NAME=$(node -p "require('./package.json').name")
          PACKAGE_VERSION=$(node -p "require('./package.json').version")
          npm view "$PACKAGE_NAME@$PACKAGE_VERSION" version
          echo "Successfully published $PACKAGE_NAME@$PACKAGE_VERSION"

Key Options Explained

  • release: types: [published] — Triggers only when a GitHub release is published (not drafted). Gives you manual control over when packages ship.
  • cancel-in-progress: false — Never interrupts an ongoing publish. A half-published package could leave the registry in a bad state.
  • id-token: write — Grants permission to request an OIDC token from GitHub, which npm uses to create provenance attestations linking the package to its source.
  • --provenance — Generates a signed build provenance statement (SLSA) proving the package was built from the claimed source repo via GitHub Actions.
  • --access public — Required for scoped packages (@org/package) to be publicly accessible. Unscoped packages are public by default.
  • registry-url — The setup-node action automatically configures .npmrc with auth for this registry using NODE_AUTH_TOKEN.
  • Version validation — Compares package.json version with the Git tag to catch mismatches before publishing.

Common Modifications

  • Scoped packages: For private scoped packages, change --access public to --access restricted.
  • Pre-release tags: Add npm publish --tag beta for pre-release versions. Users install with npm install package@beta.
  • Monorepo: Use npx changeset publish or npx lerna publish from-package for multi-package publishing.
  • GitHub Packages: Change registry-url to https://npm.pkg.github.com and use GITHUB_TOKEN instead of NPM_TOKEN.
  • Changelog automation: Add a step to extract release notes from CHANGELOG.md and include them in the GitHub release body.