---
title: "Automating npm package releases with CI/CD"
description: "Automating npm package releases with GitHub Actions: versioning, changelogs, and publishing to npm from CI without manual steps. Includes the full workflow file, the tooling choices, and the trade-offs."
canonical: "https://asmyshlyaev177.dev/blog/automating-npm-package-release-with-cicd"
published: "September 13, 2024"
tags: ["CI/CD", "GitHub Actions", "npm"]
---

# Automating npm package releases with CI/CD

> Automating npm package releases with GitHub Actions: versioning, changelogs, and publishing to npm from CI without manual steps. Includes the full workflow file, the tooling choices, and the trade-offs.

---

## Intro

For quite some time, I wanted to try to automate releasing NPM packages with GitHub Actions.

I already had tests running in CI/CD. If the branch is main and all tests are passed, the desired outcome is to automatically publish NPM package and update changelog.

## Workflow file

```yaml
name: Tests
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  workflow_dispatch:
    branches: [main]
  repository_dispatch:
    types: [semantic-release]

# cancel previous in progress actions
concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

permissions: # important for npm provenance
  contents: read
  pages: write
  id-token: write
  issues: write
  pull-requests: write
jobs:
  # install all packages and run unit tests
  installtest:
    name: installtest
    timeout-minutes: 20
    runs-on: ubuntu-22.04
    container:
      image: mcr.microsoft.com/playwright:v1.45.0-jammy
    permissions:
      contents: write # to be able to publish a GitHub release
      issues: write # to be able to comment on released issues
      pull-requests: write # to be able to comment on released pull requests
      id-token: write # to enable use of OIDC for npm provenance
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - uses: actions/setup-node@v4
        with:
          node-version-file: ".nvmrc"
      - uses: actions/cache/restore@v4
        id: cache-node-modules
        with:
          path: |
            ./node_modules
          key: modules-1-${{ hashFiles('package-lock.json') }}
      - name: Install dependencies
        if: steps.cache-node-modules.outputs.cache-hit != 'true'
        # put playwright executable to node_modules
        run: npm clean-install && npx cross-env HOME=/root PLAYWRIGHT_BROWSERS_PATH=0 npx playwright install chromium firefox webkit

      - name: Unit tests
        run: npm run test:unit
      - name: Run codacy-coverage-reporter
        uses: codacy/codacy-coverage-reporter-action@v1.3.0
        continue-on-error: true
        with:
          project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
          coverage-reports: coverage-reports/lcov.info
      - name: Cache dependencies
        id: cache
        uses: actions/cache/save@v4
        if: steps.cache-node-modules.outputs.cache-hit != 'true'
        with:
          path: |
            ./node_modules
          key: modules-1-${{ hashFiles('package-lock.json') }}

  # build all packages and cache it
  build:
    name: build
    needs: [installtest]
    timeout-minutes: 20
    runs-on: ubuntu-22.04
    container:
      image: mcr.microsoft.com/playwright:v1.45.0-jammy
    permissions:
      contents: write # to be able to publish a GitHub release
      issues: write # to be able to comment on released issues
      pull-requests: write # to be able to comment on released pull requests
      id-token: write # to enable use of OIDC for npm provenance
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - uses: actions/cache/restore@v4
        id: cache-node-modules
        with:
          path: |
            ./node_modules
          key: modules-1-${{ hashFiles('package-lock.json') }}

      - name: Install dependencies
        if: steps.cache-node-modules.outputs.cache-hit != 'true'
        run: npm clean-install && npx cross-env HOME=/root PLAYWRIGHT_BROWSERS_PATH=0 npx playwright install chromium firefox webkit

      - name: build packages
        run: npm run build:packages

      - name: Cache dependencies
        id: cache
        uses: actions/cache/save@v4
        if: always()
        with:
          path: |
            ./node_modules
            ./dist
            ./packages/example-react/dist
            ./packages/example-react/package.json
            ./packages/example-nextjs14/package.json
            ./packages/example-nextjs14/.next
            ./packages/example-nextjs15/package.json
            ./packages/example-nextjs15/.next
          key: modules-2-${{ github.sha }}

  # playwright tests
  testint:
    needs: [build]
    name: testint
    runs-on: ubuntu-22.04
    timeout-minutes: 30
    container:
      image: mcr.microsoft.com/playwright:v1.45.0-jammy
    strategy:
      fail-fast: false
      matrix:
        # using 2 workers
        shardIndex: [1, 2]
        shardTotal: [2]
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - uses: actions/cache/restore@v4
        id: cache
        with:
          # reusing cache from build step
          path: |
            ./node_modules
            ./dist
            ./packages/example-react/dist
            ./packages/example-react/package.json
            ./packages/example-nextjs14/package.json
            ./packages/example-nextjs14/.next
            ./packages/example-nextjs15/package.json
            ./packages/example-nextjs15/.next
          key: modules-2-${{ github.sha }}

      - name: Run Playwright tests
        run: |
          npm run start:ci & \
          npx wait-on http://localhost:3000 && \
          npx wait-on http://localhost:3001 && \
          npm run test:int:ci -- --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: blob-report-${{ matrix.shardIndex }}
          path: blob-report
          retention-days: 5

  # publish to npm
  release:
    name: release
    needs: [testint]
    if: github.ref == 'refs/heads/main'
    timeout-minutes: 20
    runs-on: ubuntu-22.04
    container:
      image: mcr.microsoft.com/playwright:v1.45.0-jammy
    permissions:
      contents: write # to be able to publish a GitHub release
      issues: write # to be able to comment on released issues
      pull-requests: write # to be able to comment on released pull requests
      id-token: write # to enable use of OIDC for npm provenance
    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false
      - uses: actions/setup-node@v4
        with:
          node-version: "20.x"
          registry-url: "https://registry.npmjs.org"
      - uses: actions/cache/restore@v4
        id: cache
        with:
          path: |
            ./node_modules
            ./dist
            ./packages/example-react/dist
            ./packages/example-react/package.json
            ./packages/example-nextjs14/package.json
            ./packages/example-nextjs14/.next
            ./packages/example-nextjs15/package.json
            ./packages/example-nextjs15/.next
          key: modules-2-${{ github.head_ref }}

      - name: Verify the integrity of provenance attestations and registry signatures for installed dependencies
        run: npm audit signatures

      - name: git config
        run: git config --global --add safe.directory /__w/state-in-url/state-in-url
      - name: Initialize Git user
        run: |
          git config --global user.email "github-release-bot@example.com"
          git config --global user.name "Release Workflow"
      - name: Initialise the NPM config
        run: npm config set //registry.npmjs.org/:_authToken $NPM_TOKEN
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Release
        env:
          GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
        run: npx semantic-release
```

Result looks like this

![CI/CI screenshot](https://asmyshlyaev177.dev/_astro/di6napx03xm4uj4frddk.CYZy5mQn.png)

![CI/CI screenshot 2](https://asmyshlyaev177.dev/_astro/fn6e9cldzguv6dbkzep8.kXtjSHCZ.png)

![npm screenshot](https://asmyshlyaev177.devdata:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIoAAABMCAYAAACyNi/NAAAMWklEQVR42u3caVgUV77H8W91gyKiAhFUBNxHR1DQplVQdFDjEtSJu0nGO+MdTZy4XJ0nJjHXJJMxZsgk5sboo5kkd4zjTkyeqKOYuGJQUdxQI7IEF0AgRBFlaaC7/vcFrqNgie1ovOfzPLzpqnNOddWvzzlVnG5NRARFuQvTwz4A5edBBUUxRAVFMUQFRTFEBUUxRAVFMUQFRTFEBUUxRAVFMUQFRTFEBUUxRAVFMUQFRTFEBUUxRAVFMUQFRTFEBUUx5GcbFCk8yPJ33mDesgNcVGv0Hrj7Doqev52PZr/M7IW7KNBv3mLj+Ko3eeXVGDaecTj9wMVuo9RWTlmJDbsKygPncr8VmHyCCGq6jZzcVNIv98HHU6vaUHmG1AwbWiMLQf7me69YBNE0tGrb7cWk17vhMNfB9WfbL/583HdQMPkSFNSEbefPcSq9hHCrBxrgyEojvVSjUY8gAl0AvZjMhE1s2XuS7MuCR7MO9IweRu/WHmh6Hts+WsAOj6FM7n6RuE37OUs3np81hCdO7+DrTYmk5ZWgeTSlbdfeDOgbQpPy/XwW8xWnA4fxygu98NQAKeHMnji27P2e7CId96btsPZ7iqiO3rgAcjmRT2PWUxL5HP1Nh9mRlEZ+WR2adB7MMyOt+N7/2XhsOeGzaMI3KAhfzc6ZUxnYBEAnNzWNIhrSIbgFLpRzZsun/O/mU9hbhjNgYE9a6anELYsl6dKNccORtY3PvzxFvU5RDBgQSlPbMTas+JbvS/2IiI4mqlNDCs/mU36nfkZsnI77hE83HuInjw706BmCb2kK2/7xMbHJRdxoRSdv10rWn25A56j+WJo6OH94I5uTi1EjWPWc8hkyNQkiyHc72zNTOV0ZSkeXAlLTL0ADK8EtXZDCA3yzJx93y2+ZOLI9boDezkTewp0cTbmMtXtVPWKDNs+8wLOhDdAA/adMCsuhbstgIiKsNDL1JOpqm1J+6zHIpUN8uycPh09vxk+KpoUr6N19+OSDf3LsmwR6BUcTULUnpqZRTJg4gOYuUOmdz/HPD5NzLh/d4kEtBsn/F5zT2ZqaEhTkw44dGaSes/PLxhmk5+l4WIJp6QKO3HPk2HVsSUv5U9LNBc34lZTd+CSbWxLUscH1/sL0RAiRIYmsPbKO92KSCLaGExkRQvP6t3eEjtwscuzg0aY9/q5Xy3u1p32TzWTmZpNTLARcq7ieO/WvJsLkXh83DWyVFQ/7WjzSnDQqm2gWFITPzl2kp+ZQXJROtt6A0OBWuAJ2EcCEV9honotoctN4p1HX0wcTBXeuVvMkZOxMAiyH2LNnLwe3r+Hoge8ZO+U5QqsZNLVqXqhpWLlWRg091XPa/YLJL4iOjaEw/QT7T53B7t6e4NZVH21zswD8zEJRVh7i64+//7W/5vh41NTZ27FVmPFuF87Q383kv6JbohWd5FBa6W0X1dzMHz8zXMlMI8de9Zp+KZ20PB2Tlz/+HhpK7Tlvnm/yI7jjE8TH72VXgQP3kGCu5gTNy0K/7oks3bubvy8pontIc+oWZ3My6wl+PWkQgXeMq1CS8jUL15zGNzSUXzStS8GxfHSTF82auN3Wc2ieYQzsmcSnu79j+WeldG3lQl5yEpmOhoQM7EWAmnzcFyc+gTDjF9wRb62SCns92ge3oc61TVo92g2dxITBXWlWkc7erdvZd6oIzxZNqaNX1+Fr1Auw8quujbl8cjeb1n/DsdJm9Bg1nidb3OGqa260GjyJidGheBWdICH+MLlu7ek7fjJjQhqh+pP7o6kvqStGqGeaiiEqKIohKiiKISooiiEqKIohKiiKISooiiEqKIohKiiKISooiiEqKIohKiiKISooiiEqKIohKiiKIeqbLPfIoTuYv20+sQdj0dAYZx3HjH4zMJse7yV0Tlu4pF9M5stPPuPL+GTOFtqp59uWsIH/wYu/709Lt7uVrmDnf/fn5W9uXwuLuTX/+flqXvxlTReijHM7/sHHyzezPy2fMnMjmgdHMmLSi4zt4n3f3aat0kbsoVhyi3JJyEggISOB0ZbRiAjrDq8jsl0kPdv0xK+RH2PCxlDXpa4zTukjxSk9SmnKKl794wL2Fei4evkT4G8j71wKO5a+xpH0Yj7/4Gma13S1xEZJiR1Bw9W9Ae6uNy1cNHvgVlNG9Hzi/zqD179Mp8xUH5/AQDwuZnPmwNd8cPQYeQuXMsNSv9ZLISsdlQxeOJijWUcBCPQOZMlzSxgXNg6Avh36ErMlhu/SvwNg1YFVbJ62GU17zBZfihOUn1kvs4YOlj98sl9+rBARcciFve/K6O5hYrFGy7sHy2uuwJErqyf1EEtYT3llm+3eGtcvSdKC8fLkmLdkY0ax6CIiZT/Imml9xWoJk4g/fCF5jtq/tw3JG8RzhqesPbj2rvuuOrBKPGd4StyJOGec1keKUyazdVoM45016/hwUjd8XAFMeHcfx5AOZtAvkHqqAL3GGkooKRHQPGjU4B4PSWtE2LS/8fXSOQxpc7XncGvN8NGRNDAJlWkn+eE+fkzhaNZRTJqJkV1G3nXfMZYxmDQTh84ecsZpfaQ47a7Hxd39xqr7f93mepcRTi+luFQHrcG9BwVAq4e7ezXlXFxrPb7qorM1ZSuhAaE1TlaXJy4n+1I2ZpOZzv6d2ZqyFV30e2jp0ffAbo/17O+IT3OgubTEEtq45oaklJISQArZ9ZdnGNAznF79hvHbWR+yMfVKLb7BV0LSrgNc0U14hIbRoZZJWbRzEcdzjjM1amq1+7y9+W2mr53OgdMHAJjyqykkZyezeNfiB3VqH4oHExQ9n7iPlnG8QqNR5HiGt73LraPmils9M5qLGZeGLbFEhOLHj5zcuYK5L/yRFRmV99R82bGlLIorQFxbMXJ8HxrWcl65JH4Jz3Z7luGhwwHIKsxieeLy69vf+udbzN86n6lRUxnRZQQAo7qOYmzYWD7e/fEDObUPywN4jlJO2orXeX/3RfCKZNrMQfjcLY4uXZm5ah3DS3xo5Vs1gOkXdzNvwkusz0lmxZokxs6JqHZou5l+IZ733lhBRqUrrX/zKhOCjJSq4d3Yb/xsQmJmItPXTifnUg4iwofbP2Rq1FTmDpt7S5kKewXyuH1dyrlz40rJ3jRbhnQLk7Aew2Xe7gtVdyG1rOvI/KFitVikx+9XSo6BOxf98hFZ/LveYrVYJWrySkkvv3uZmizYvkA8Z3hK7MHY66/NWjdLPGd4iucMT5mzfs5tZfZl7hPPGZ6yaOci557ah8yJQbFL7rY/yfDwMLFYn5TpX2RKddfJYSuT8lsS5BDHbUGwyZ4/PylWi0XCp3wlF/Qb+5aXlcu/7q6XnJC/P3/1lnjMe5JYWPuIXm9Jd0ivv/aS8JjwW15/e9PbErMl5o5l+rzfR3q/31sc+n3ckz+CnBQUu+Rt/7OMDA8Ti7WPTPhbshTf8To5JHvDLHmqh1Uin1kgB4tFRCokc/UUGfn8/0jcqUKpFBGRcslN+EDGR4aJJSxCJiw/WxUM/YLEzxshPa3h8vRbO+Snq23oJd/Lssn9xGoJk25DZktcjt1pJ2juprniPdNb7I6712l32MV7prfM2zzPae0/KpwyR7m8bz5T5mzgTIWgmTTyNr/GiDVXKLPriACunZi2chFjmhVzZEcC+ZU6ZOxiz+kpWDoWkHL0HOcPJzLnN6t5x6sxDblCQWEpDjHRsMtEXhoZWDXrtqewc1sWNl0ne9duUmZH0ct8mi9ems6ipEvoaLiVn2DxxEG8W1yOQwRBw2vo+2x4uVut3luXgC7oorPu8DrGho2tcd/YQ7HoohMaEPrvnT/8GzglKD+lHCO7omryJvoVfsy+cusO9oqrP/HpQfdfD+UXx+K41P5p+rc1g8mPp/6ykrbfrmDZuu0cTM2hwF4Hr1ZWwgeNY8KzfQi89r8il05Ej+rE3jVZ+A0fTGcXoPIMySeKrj7QE2wXz3P+lsZNuFXU/onbwKCBdA3syuSVk5m8cjKB3oG8Nvi166FZnbSamC0xnLt4DoDurbozKGjQg71qD4H6NQMDSipKWJO0hoIrBez5YQ8JGQmM6joKXXS+OvIVke0iiWgdQXPP5o/tPwVVUO6RQ3fw5sY3WRy/GBFhet/pvBH9hlpmoCigVrgpBqmgKIaooCiGqKAohqigKIaooCiGqKAohqigKIaooCiGqKAohqigKIaooCiGqKAohqigKIb8H7NJt91oCmGcAAAAAElFTkSuQmCC)

There are also many small details like adding secrets like `NPM_TOKEN` to repository, scripts in package.json and so on.

## Tools

Had to use multiple tools to achieve it, Github Actions obviously, `wireit` to run npm scripts with dependencies, commits with `commitizen` (needed to update version and to mark breaking changes in Changelog), `husky` for pre-commit hooks, and `semantic-release` package.

## Pro and cons

- The biggest issue is that cache in GitHub actions is pretty slow, thinking about using `Docker` to speed things up.
- Most of the benefits from such complex setup will be visible if you use at least 3 workers for tests. If the project is small, maybe not worth the effort.

## Links

[Full workflow](https://github.com/asmyshlyaev177/state-in-url/blob/main/.github/workflows/tests.yml)
[Official docs](https://docs.github.com/en/actions/use-cases-and-examples/publishing-packages/publishing-nodejs-packages) for reference

---

Author: Aleksandr Smyshliaev — <https://asmyshlyaev177.dev>
More posts: <https://asmyshlyaev177.dev/blog> · Site summary for LLMs: <https://asmyshlyaev177.dev/llms.txt>
