Automating npm package release with CI/CD

Alex - Sep 13 - - Dev Community

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
Enter fullscreen mode Exit fullscreen mode

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.

Links

Full workflow
Official docs for reference

. . . . . . . . . . .
Terabox Video Player