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
27 changes: 24 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ on:
push:
tags:
- 'v[0-9]*'
workflow_dispatch:
inputs:
tag:
description: 'Existing tag to create or update a GitHub Release for (e.g. v0.2.2)'
required: true
type: string

permissions:
contents: write
Expand All @@ -12,8 +18,23 @@ jobs:
release:
name: Create GitHub Release
runs-on: ubuntu-latest
env:
RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.tag || github.ref_name }}
steps:
- name: Validate release tag
if: github.event_name == 'workflow_dispatch'
run: |
set -euo pipefail
tag="${{ github.event.inputs.tag }}"
if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "tag must look like vX.Y.Z, got: $tag" >&2
exit 1
fi
git ls-remote --exit-code origin "refs/tags/${tag}" >/dev/null

- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ env.RELEASE_TAG }}

- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
Expand All @@ -23,15 +44,15 @@ jobs:
id: meta
run: |
pkg_name=$(python -c "import tomllib,pathlib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text())['project']['name'])")
pkg_version="${GITHUB_REF_NAME#v}"
pkg_version="${RELEASE_TAG#v}"
echo "name=${pkg_name}" >> "$GITHUB_OUTPUT"
echo "version=${pkg_version}" >> "$GITHUB_OUTPUT"

- name: Extract changelog notes
id: notes
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
version="${RELEASE_TAG#v}"
if [[ -f CHANGELOG.md ]]; then
body="$(python scripts/extract-changelog.py "$version")"
else
Expand All @@ -47,7 +68,7 @@ jobs:
- name: Create GitHub Release
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2
with:
tag_name: ${{ github.ref_name }}
tag_name: ${{ env.RELEASE_TAG }}
name: ${{ steps.meta.outputs.name }} ${{ steps.meta.outputs.version }}
body: ${{ steps.notes.outputs.body }}
generate_release_notes: false
Expand Down
12 changes: 12 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,18 @@ Pushing a `vX.Y.Z` tag triggers two workflows:
| `publish.yml` | Build wheel/sdist and publish to PyPI |
| `release.yml` | Create the GitHub Release with notes from `CHANGELOG.md` |

## Recover a missing GitHub Release

If PyPI publish succeeded but the GitHub Release workflow failed, rerun it from `main`
without retagging:

```bash
gh workflow run "GitHub Release" --ref main -f tag=vX.Y.Z
```

The tag must already exist on the remote. The workflow checks out that tag, extracts the
matching `CHANGELOG.md` section, and creates or updates the GitHub Release.

## Enforcement

- **PR check** (`check-release.yml`): if `pyproject.toml` version changes, `CHANGELOG.md` must contain a matching `## [X.Y.Z]` section.
Expand Down