# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
name: Publish to npm
on:
push:
branches:
- main
# Manual trigger with version bump type
workflow_dispatch:
inputs:
release_type:
description: 'Semver bump type'
required: false
default: 'patch'
type: choice
options:
- patch
- minor
- major
registry:
description: 'Registry URL for publish'
required: false
default: 'https://registry.npmjs.org/'
type: string
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
publish-npm:
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
env:
RELEASE_TYPE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.release_type || 'patch' }}
REGISTRY: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.registry || 'https://registry.npmjs.org/' }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.repository.default_branch }}
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: ${{ env.REGISTRY }}
- name: Install dependencies
run: |
npm ci
- name: Configure git user
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Bump version and create tag (unique)
id: versioning
run: |
set -euo pipefail
# Ensure we have all tags locally
git fetch --tags --force
# Initial bump without creating tag
npm version --no-git-tag-version "$RELEASE_TYPE"
# Loop until we find a version whose tag doesn't already exist
NEXT="v$(node -p "require('./package.json').version")"
while git rev-parse -q --verify "refs/tags/${NEXT}" >/dev/null; do
echo "Tag ${NEXT} already exists; bumping patch again..."
npm version --no-git-tag-version patch
NEXT="v$(node -p "require('./package.json').version")"
done
# Also ensure version is not already published in the target registry
PACKAGE_NAME="$(node -p "require('./package.json').name")"
NEXT_NO_V="${NEXT#v}"
# Function to check if version exists on registry
version_published() {
node -e "const v=process.argv[1]; const name=process.argv[2]; const reg=process.argv[3];
import('node:child_process').then(({execSync})=>{
let out='[]';
try { out = execSync(`npm view ${name} versions --json --registry ${reg}`, {stdio:['ignore','pipe','ignore']}).toString(); } catch {}
const arr = Array.isArray(JSON.parse(out||'[]')) ? JSON.parse(out||'[]') : [];
process.exit(Array.isArray(arr) && arr.includes(v) ? 0 : 1);
});" "$1" "$2" "$3"
}
while version_published "$NEXT_NO_V" "$PACKAGE_NAME" "$REGISTRY"; do
echo "Registry already has ${PACKAGE_NAME}@${NEXT_NO_V}; bumping patch again..."
npm version --no-git-tag-version patch
NEXT="v$(node -p "require('./package.json').version")"
NEXT_NO_V="${NEXT#v}"
done
echo "Final version: ${NEXT}"
git add package.json package-lock.json || true
git commit -m "chore(release): ${NEXT} [skip ci]" || echo "No changes to commit"
git tag -a "${NEXT}" -m "chore(release): ${NEXT#v} [skip ci]"
echo "tag=${NEXT}" >> $GITHUB_OUTPUT
echo "version=${NEXT#v}" >> $GITHUB_OUTPUT
- name: Push commit and tags
run: |
set -euo pipefail
git push origin HEAD:${{ github.event.repository.default_branch }}
git push origin --tags
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ steps.versioning.outputs.tag }}
name: Release v${{ steps.versioning.outputs.version }}
generate_release_notes: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Set up .npmrc for authentication
run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish to npm
env:
# Map common secret names to NODE_AUTH_TOKEN and also export NPM_TOKEN for .npmrc usage
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN || secrets.NPMJS_TOKEN || secrets.NODE_AUTH_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN || secrets.NPMJS_TOKEN || secrets.NODE_AUTH_TOKEN }}
run: |
set -euo pipefail
if [ -z "${NODE_AUTH_TOKEN:-}" ]; then
echo "Missing NPM token. Configure one of these repository secrets: NPM_TOKEN (preferred), NPMJS_TOKEN or NODE_AUTH_TOKEN." >&2
echo "Create an Automation token at https://www.npmjs.com/settings/<your-user>/tokens and add it as a secret." >&2
exit 1
fi
npm whoami --registry "$REGISTRY"
npm publish --access public --registry "$REGISTRY"