diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e7f995a --- /dev/null +++ b/.github/workflows/release.yml @@ -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 }}" diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..313c388 --- /dev/null +++ b/RELEASING.md @@ -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.