Skip to content

Release

Release #4

Workflow file for this run

name: Release
# Triggered after CI completes on main. The workflow_run trigger guarantees
# we only publish commits that already passed tests on main, while keeping
# release as a separate workflow file so it can be wired to the npm trusted
# publisher (Settings -> Packages -> Trusted Publishers): point npm at this
# workflow filename (`release.yml`) and the `release` environment below.
on:
workflow_run:
workflows: [CI]
branches: [main]
types: [completed]
# Manual trigger - publish from a specific tag, branch or commit SHA.
# Leave `ref` empty to publish from main HEAD.
workflow_dispatch:
inputs:
ref:
description: 'Tag, branch or commit SHA to publish (e.g. v0.1.0). Empty = main HEAD.'
type: string
required: false
default: ''
permissions:
# Needed to push the version tag back to the repository.
contents: write
# Required for npm provenance + npm trusted-publisher OIDC exchange.
id-token: write
jobs:
release:
name: Publish to npm
runs-on: ubuntu-latest
# Skip when CI failed; workflow_dispatch has no workflow_run context, so
# the second clause lets manual runs through.
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success')
environment: release
steps:
- name: Checkout
uses: actions/checkout@v4
with:
# Resolution order:
# 1. workflow_dispatch input (manual republish at a specific ref)
# 2. workflow_run head SHA (the commit that just passed CI)
# 3. github.ref (defensive default; should not be hit)
ref: ${{ inputs.ref != '' && inputs.ref || github.event.workflow_run.head_sha || github.ref }}
# Full history so `git push origin <tag>` succeeds.
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '22'
- name: Install dependencies
run: npm ci
- name: Resolve package metadata
id: meta
run: |
set -euo pipefail
NAME=$(node -p "require('./package.json').name")
VERSION=$(node -p "require('./package.json').version")
TAG="v$VERSION"
{
echo "name=$NAME"
echo "version=$VERSION"
echo "tag=$TAG"
} >>"$GITHUB_OUTPUT"
- name: Skip if version already on npm
id: check
run: |
set -euo pipefail
if npm view "${{ steps.meta.outputs.name }}@${{ steps.meta.outputs.version }}" version >/dev/null 2>&1; then
echo "publish=false" >>"$GITHUB_OUTPUT"
echo "::notice::${{ steps.meta.outputs.name }}@${{ steps.meta.outputs.version }} is already published; skipping."
else
echo "publish=true" >>"$GITHUB_OUTPUT"
echo "::notice::Will publish ${{ steps.meta.outputs.name }}@${{ steps.meta.outputs.version }}."
fi
# `npm publish` runs `prepublishOnly` (typecheck && test && build), so
# we don't pre-build here.
#
# We publish via `npx -y npm@11.5.1` rather than the system npm:
# Trusted Publishing requires npm >= 11.5.1, and Node 22 LTS still
# bundles npm 10.x.
#
# Auth strategy:
# - Preferred: npm Trusted Publishing (OIDC). No secret needed — but it
# only works for a package that ALREADY exists on npm with this repo
# registered as a trusted publisher.
# - Bootstrap / fallback: NPM_TOKEN repo secret. npm only reads a token
# from .npmrc (a NODE_AUTH_TOKEN env var alone is ignored), so we
# write .npmrc explicitly — and only when the secret is set, because
# an empty _authToken line would break the OIDC path.
- name: Publish to npm
if: steps.check.outputs.publish == 'true'
run: |
set -euo pipefail
if [ -n "${NPM_TOKEN:-}" ]; then
echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >>"$HOME/.npmrc"
echo "::notice::Publishing with the NPM_TOKEN secret (bootstrap/fallback)."
else
echo "::notice::No NPM_TOKEN secret; relying on npm Trusted Publishing (OIDC)."
fi
echo "Publishing with:"
npx -y npm@11.5.1 --version
npx -y npm@11.5.1 publish --provenance
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
# `npm publish` (prepublishOnly) already built dist/. Package the npm
# tarball as a release asset for offline installs.
- name: Package release assets
if: steps.check.outputs.publish == 'true'
run: |
set -euo pipefail
mkdir -p release-assets
npm pack --pack-destination release-assets >/dev/null
ls -la release-assets
- name: Create git tag and GitHub release
if: steps.check.outputs.publish == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
TAG="${{ steps.meta.outputs.tag }}"
if gh release view "$TAG" >/dev/null 2>&1; then
echo "::notice::GitHub release $TAG already exists; skipping tag/release creation."
exit 0
fi
git config user.name "Siarhei Dudko"
git config user.email "siarhei@dudko.dev"
if git rev-parse --verify "refs/tags/$TAG" >/dev/null 2>&1; then
echo "::notice::Git tag $TAG already exists locally; reusing without re-tagging."
else
git tag -a "$TAG" -m "Release $TAG"
git push origin "$TAG"
fi
gh release create "$TAG" \
--title "$TAG" \
--generate-notes \
--target "$(git rev-parse HEAD)" \
release-assets/*