Skip to content

Release

Release #3

Workflow file for this run

name: Release
# Manual, one-button release. Pick a version (a semver bump keyword or an exact
# version), and this workflow: runs the full CI gate, bumps package.json,
# writes the changelog from the commits since the last tag, commits + tags,
# publishes to public npm, and cuts a GitHub Release whose body is that
# changelog. Run `dry_run: true` first to preview without pushing anything.
#
# Requires an NPM_TOKEN repo secret (an npm automation/granular token with
# publish rights on the @copse scope).
on:
workflow_dispatch:
inputs:
bump:
description: 'Semver bump to apply (ignored when "exact version" is set below)'
required: true
default: patch
type: choice
options:
- patch
- minor
- major
- prepatch
- preminor
- premajor
- prerelease
exact_version:
description: 'Optional: exact version to release (e.g. 1.2.0). Overrides the bump dropdown when set.'
required: false
default: ''
type: string
prerelease:
description: 'Mark the GitHub Release as a pre-release'
required: false
default: false
type: boolean
dry_run:
description: 'Build the release notes and version bump but do not commit, tag, publish, or release'
required: false
default: false
type: boolean
# Least privilege: write to the repo (commit + tag + release). Publishing goes
# to public npm and authenticates with the NPM_TOKEN secret, not GITHUB_TOKEN.
# id-token: write lets npm mint a provenance attestation via GitHub OIDC.
permissions:
contents: write
id-token: write
concurrency:
group: release
cancel-in-progress: false
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
# Full history + tags so the changelog can diff against the last tag.
fetch-depth: 0
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
registry-url: https://registry.npmjs.org
scope: '@copse'
- run: npm ci
# Release gate: never cut a release from a red tree.
- name: Typecheck
run: npm run typecheck
- name: Test
run: npm test
- name: Build
run: npm run build
- name: Normalizer parity
run: npm run check:normalizer-parity
- name: Resolve version
id: version
run: |
set -euo pipefail
# Prefer the free-text exact version when provided; otherwise use the
# bump keyword from the dropdown. `npm version` accepts both forms and
# prints the resulting `vX.Y.Z`. --no-git-tag-version edits
# package.json + package-lock.json without tagging (we tag ourselves).
TARGET="${{ inputs.exact_version || inputs.bump }}"
TAG=$(npm version "$TARGET" --no-git-tag-version)
VERSION="${TAG#v}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Releasing $TAG"
- name: Generate changelog
run: npx tsx scripts/gen-changelog.mts "${{ steps.version.outputs.version }}" > RELEASE_NOTES.md
- name: Update CHANGELOG.md
run: |
set -euo pipefail
DATE=$(date -u +%Y-%m-%d)
npx tsx scripts/update-changelog.mts "${{ steps.version.outputs.version }}" "$DATE" RELEASE_NOTES.md
- name: Preview release notes
run: |
{
echo "## Release ${{ steps.version.outputs.tag }}"
echo
cat RELEASE_NOTES.md
} >> "$GITHUB_STEP_SUMMARY"
- name: Commit, tag, and push
if: ${{ !inputs.dry_run }}
run: |
set -euo pipefail
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git add package.json package-lock.json CHANGELOG.md
git commit -m "chore(release): ${{ steps.version.outputs.tag }}"
git tag -a "${{ steps.version.outputs.tag }}" -m "${{ steps.version.outputs.tag }}"
git push origin HEAD
git push origin "${{ steps.version.outputs.tag }}"
- name: Publish to npm
if: ${{ !inputs.dry_run }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
set -euo pipefail
# `private: true` in package.json is a safety net against accidental
# publishes; strip it here (uncommitted) so this deliberate, authed
# publish can proceed. publishConfig in package.json pins the public
# npm registry and `--access public` for the scoped @copse package.
npm pkg delete private
# --provenance attaches a signed attestation linking this release to
# the source commit + build (via GitHub OIDC), shown as a verified
# badge on npm.
npm publish --provenance
- name: Create GitHub Release
if: ${{ !inputs.dry_run }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
gh release create "${{ steps.version.outputs.tag }}" \
--title "${{ steps.version.outputs.tag }}" \
--notes-file RELEASE_NOTES.md \
${{ inputs.prerelease && '--prerelease' || '' }}