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
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