Skip to main content

Automating npm package releases with CI/CD

  • CI/CD
  • GitHub Actions
  • npm

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

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

CI/CI screenshot 2

npm screenshot

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.

Full workflow Official docs for reference

Share

Also published on dev.to .

Comments

Sign in with GitHub to leave one. Threads are public GitHub Discussions — you can read and reply from there instead.