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
163 changes: 82 additions & 81 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,85 +11,19 @@ on:
options: [patch, minor, major]
default: patch

permissions:
contents: write
id-token: write

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

jobs:
tagged:
name: publish tagged release
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7
with:
# Trusted publishing needs npm >= 11.5.1 (newer than Node 20 ships).
node-version: 24
registry-url: "https://registry.npmjs.org"
- run: bun install --frozen-lockfile
- run: bun run test
- run: bun run typecheck
- name: Check whether the version is already published
id: published
run: |
version="${GITHUB_REF_NAME#v}"
pkg_version=$(node -p "require('./package.json').version")
if [ "$version" != "$pkg_version" ]; then
echo "::error::Tag version ($version) does not match package.json version ($pkg_version)."
exit 1
fi
pkg=$(node -p "require('./package.json').name")
if npm view "$pkg@$version" version >/dev/null 2>&1; then
echo "exists=1" >> "$GITHUB_OUTPUT"
echo "::notice::$pkg@$version is already on npm; skipping publish."
else
echo "exists=0" >> "$GITHUB_OUTPUT"
fi
- name: Publish to npm
if: steps.published.outputs.exists == '0'
# npm (not bun) because trusted publishing authenticates with the OIDC
# id-token and attaches provenance automatically (package Trusted
# Publisher settings). No token is stored in the repo.
run: npm publish --provenance --access public
- name: Prepare release notes
id: notes
run: |
tag="${GITHUB_REF_NAME}"
if bash scripts/changelog.sh notes "$tag" > "$RUNNER_TEMP/notes.md"; then
echo "have=1" >> "$GITHUB_OUTPUT"
echo "::notice::using the CHANGELOG.md section for $tag"
else
echo "have=0" >> "$GITHUB_OUTPUT"
echo "::warning::CHANGELOG.md has no usable section for $tag; falling back to generated notes"
fi
- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
tag="${GITHUB_REF_NAME}"
have="${{ steps.notes.outputs.have }}"
if gh release view "$tag" >/dev/null 2>&1; then
if [ "$have" = "1" ]; then
gh release edit "$tag" --title "$tag" --notes-file "$RUNNER_TEMP/notes.md"
else
gh release edit "$tag" --title "$tag"
fi
elif [ "$have" = "1" ]; then
gh release create "$tag" --title "$tag" --notes-file "$RUNNER_TEMP/notes.md" --verify-tag
else
gh release create "$tag" --title "$tag" --generate-notes --verify-tag
fi

cut:
name: cut, tag, publish
name: cut and tag
if: github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
tag: ${{ steps.version.outputs.tag }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
Expand All @@ -99,7 +33,6 @@ jobs:
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7
with:
node-version: 24
registry-url: "https://registry.npmjs.org"
- run: bun install --frozen-lockfile
- run: bun run test
- run: bun run typecheck
Expand All @@ -120,8 +53,10 @@ jobs:
- name: Bump version, write the changelog and tag
if: steps.draft.outputs.ok == '1'
id: version
env:
BUMP: ${{ inputs.bump }}
run: |
version="$(npm version "${{ inputs.bump }}" --no-git-tag-version | tail -n 1)"
version="$(npm version "$BUMP" --no-git-tag-version | tail -n 1)"
case "$version" in
v[0-9]*.[0-9]*.[0-9]*) ;;
*) echo "unexpected npm version output: $version"; exit 1 ;;
Expand All @@ -135,17 +70,83 @@ jobs:
echo "tag=$version" >> "$GITHUB_OUTPUT"
- name: Push commit and tag
if: steps.draft.outputs.ok == '1'
# Tags pushed with GITHUB_TOKEN do not re-trigger this workflow, so the
# publish below is the only one for this release.
run: git push --follow-tags
# Tags pushed with GITHUB_TOKEN do not trigger this workflow again; the
# publish job below picks the tag up from this job's output instead.
run: git push --atomic --follow-tags

publish:
name: publish release
needs: cut
# Runs for a pushed tag (cut is skipped) or after a cut that produced a tag.
# A failed publish is retried with "Re-run failed jobs" without re-cutting.
if: >-
!cancelled() && (github.event_name == 'push' ||
(needs.cut.result == 'success' && needs.cut.outputs.tag != ''))
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write
env:
TAG: ${{ needs.cut.outputs.tag || github.ref_name }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
ref: refs/tags/${{ env.TAG }}
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7
with:
# Trusted publishing needs npm >= 11.5.1 (newer than Node 20 ships).
node-version: 24
registry-url: "https://registry.npmjs.org"
- run: bun install --frozen-lockfile
- run: bun run test
- run: bun run typecheck
- name: Check whether the version is already published
id: published
run: |
version="${TAG#v}"
pkg_version=$(node -p "require('./package.json').version")
if [ "$version" != "$pkg_version" ]; then
echo "::error::Tag version ($version) does not match package.json version ($pkg_version)."
exit 1
fi
pkg=$(node -p "require('./package.json').name")
if npm view "$pkg@$version" version >/dev/null 2>&1; then
echo "exists=1" >> "$GITHUB_OUTPUT"
echo "::notice::$pkg@$version is already on npm; skipping publish."
else
echo "exists=0" >> "$GITHUB_OUTPUT"
fi
- name: Publish to npm
if: steps.draft.outputs.ok == '1'
if: steps.published.outputs.exists == '0'
# npm (not bun) because trusted publishing authenticates with the OIDC
# id-token and attaches provenance automatically (package Trusted
# Publisher settings). No token is stored in the repo.
run: npm publish --provenance --access public
- name: Prepare release notes
id: notes
run: |
if bash scripts/changelog.sh notes "$TAG" > "$RUNNER_TEMP/notes.md"; then
echo "have=1" >> "$GITHUB_OUTPUT"
echo "::notice::using the CHANGELOG.md section for $TAG"
else
echo "have=0" >> "$GITHUB_OUTPUT"
echo "::warning::CHANGELOG.md has no usable section for $TAG; falling back to generated notes"
fi
- name: Create GitHub release
if: steps.draft.outputs.ok == '1'
env:
GH_TOKEN: ${{ github.token }}
HAVE: ${{ steps.notes.outputs.have }}
run: |
tag="${{ steps.version.outputs.tag }}"
bash scripts/changelog.sh notes "$tag" > "$RUNNER_TEMP/notes.md"
gh release create "$tag" --title "$tag" --notes-file "$RUNNER_TEMP/notes.md" --verify-tag
if gh release view "$TAG" >/dev/null 2>&1; then
if [ "$HAVE" = "1" ]; then
gh release edit "$TAG" --title "$TAG" --notes-file "$RUNNER_TEMP/notes.md"
else
gh release edit "$TAG" --title "$TAG"
fi
elif [ "$HAVE" = "1" ]; then
gh release create "$TAG" --title "$TAG" --notes-file "$RUNNER_TEMP/notes.md" --verify-tag
else
gh release create "$TAG" --title "$TAG" --generate-notes --verify-tag
fi
7 changes: 5 additions & 2 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Releasing

[`.github/workflows/release.yml`](.github/workflows/release.yml) does the whole
job: run tests, publish to npm with provenance, and create the GitHub release.
[`.github/workflows/release.yml`](.github/workflows/release.yml) runs in two
jobs: `cut` (manual runs only) bumps, tags and pushes; `publish` runs tests,
publishes to npm with provenance and creates the GitHub release.
There are two ways to trigger it.

Development uses [bun](https://bun.sh) (`bun install`, `bun run test`). Publishing
Expand All @@ -21,6 +22,8 @@ That workflow checks out `main`, drafts notable commits, bumps the version,
writes the dated changelog section, commits and tags it, pushes, publishes to
npm, and cuts the GitHub release using that section as its notes. It does
nothing when there are no notable commits since the last release.
If publishing fails after the tag is pushed, use **Re-run failed jobs**: the
publish job retries the existing tag without cutting a new version.

Do this from a green `main` — it releases whatever is there.

Expand Down
Loading