Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
name: Release

# One-click release: bumps package.json, tags, publishes to npm, and cuts the
# GitHub release from a single source of truth. Because the tag is derived from
# `npm version`, the tag can never disagree with package.json.

on:
workflow_dispatch:
inputs:
bump:
description: "Semver bump (ignored if 'version' is set)"
type: choice
options:
- patch
- minor
- major
default: patch
version:
description: "Explicit version, e.g. 0.3.3 (overrides bump). Leave blank to use bump."
type: string
required: false

permissions:
contents: write
id-token: write

concurrency:
group: release
cancel-in-progress: false

jobs:
release:
name: Release ${{ inputs.version || inputs.bump }}
runs-on: ubuntu-latest
environment: npm

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 24.x
registry-url: https://registry.npmjs.org
cache: npm

- name: Install dependencies
run: npm ci

- name: Typecheck
run: npm run typecheck

- name: Test
run: npm test

- name: Build
run: npm run build

- name: Verify package contents
run: npm pack --dry-run

- name: Configure git
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Bump version and tag
id: bump
run: |
if [ -n "${{ inputs.version }}" ]; then
NEW=$(npm version "${{ inputs.version }}" -m "Release v%s")
else
NEW=$(npm version "${{ inputs.bump }}" -m "Release v%s")
fi
echo "tag=$NEW" >> "$GITHUB_OUTPUT"
echo "Bumped to $NEW"

- name: Push commit and tag
run: git push origin HEAD --follow-tags

- name: Publish to npm
run: npm publish --access public --provenance

- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: gh release create "${{ steps.bump.outputs.tag }}" --generate-notes --title "${{ steps.bump.outputs.tag }}"
28 changes: 28 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Releasing

The version in `package.json` is the single source of truth. The published npm
version, the git tag, and the GitHub release must all match it — the `Publish`
workflow enforces this and will fail the release if they drift.

## Recommended: one-click release

Run the **Release** workflow (Actions → Release → Run workflow) on the branch you
want to release from (normally `main`):

- **bump**: `patch` / `minor` / `major`, or
- **version**: an explicit version like `0.3.3` (overrides `bump`).

The workflow bumps `package.json`, commits, tags, pushes, publishes to npm, and
cuts the GitHub release — all from the same version, so the tag can never
disagree with `package.json`.

## Manual release (fallback)

If you create a release/tag by hand in the GitHub UI, you **must** bump
`package.json` to the matching version first and merge that commit into the
branch you tag. Cutting `v0.3.3` while `package.json` still says `0.2.11` is
exactly what the guard rejects.

1. `npm version 0.3.3` on the release branch (bumps + commits + tags).
2. `git push origin HEAD --follow-tags`.
3. Create the GitHub release from tag `v0.3.3` → the `Publish` workflow runs.
Loading