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
4 changes: 3 additions & 1 deletion README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,7 +10,9 @@ Includes:
- **GitHub Actions CI** — lint / types / docs / test matrix (`python_min` through 3.14)
- **Release pipeline** — PyPI via Trusted Publishing; projects with a Dockerfile (web services,
currently) also push a multi-arch image to GHCR. No tokens in either path — Trusted Publishing
and `GITHUB_TOKEN` respectively.
and `GITHUB_TOKEN` respectively. Once the publish jobs succeed, a tag push creates a GitHub
Release with notes from the matching `## <version>` section of `CHANGELOG.md`; the release fails
if that section is missing.
- **Dependabot** for actions and dependencies
- A generated **SETUP.md** — the go-public checklist (branch protection, RTD import, Trusted
Publisher registration)
Expand Down
31 changes: 31 additions & 0 deletions template/.github/workflows/release.yml.jinja
Original file line numberDiff line numberDiff line change
Expand Up@@ -123,3 +123,34 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
[% endif %]
release:
needs: [ [% if native_extension %]wheels, sdist[% else %]build[% endif %], check-version[% if publish_to_pypi %], publish[% endif %][% if web_service %], image[% endif %] ]
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
permissions:
contents: write
env:
GH_TOKEN: ${{ github.token }}
steps:
- uses: actions/checkout@v7
- name: Create GitHub release from CHANGELOG
run: |
set -eu
version="${GITHUB_REF_NAME#v}"
if [ ! -f CHANGELOG.md ]; then
echo "::error::CHANGELOG.md not found"
exit 1
fi
notes="$(mktemp)"
awk -v want="$version" '
/^## / {
if (found) { exit }
if ($2 == want) { found = 1; next }
}
found { print }
END { if (!found) { exit 1 } }
' CHANGELOG.md > "$notes" || {
echo "::error::CHANGELOG.md has no section for $version"
exit 1
}
gh release create "$GITHUB_REF_NAME" --verify-tag --title "$GITHUB_REF_NAME" --notes-file "$notes"
5 changes: 5 additions & 0 deletions template/CHANGELOG.md.jinja
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
# Changelog

## 0.0.1.dev0

- Initial project scaffold.
11 changes: 7 additions & 4 deletions template/SETUP.md.jinja
Original file line numberDiff line numberDiff line change
Expand Up@@ -51,15 +51,18 @@ Then create the GitHub environment: `gh api -X PUT repos/[[ github_org ]]/[[ rep
## 4. Release

```
# bump pyproject version, THEN regenerate the lock (uv sync --locked fails otherwise):
# bump pyproject version and add a matching `## <version>` section to CHANGELOG.md,
# THEN regenerate the lock (uv sync --locked fails otherwise):
uv lock
git commit -am "Release 0.1.0" && git push # via PR if main is protected
git tag -a v0.1.0 -m "0.1.0" && git push origin v0.1.0
```

`release.yml` runs check-version → build[% if native_extension %]/wheels[% endif %] → publish → smoke. A version is
permanent on PyPI once uploaded; re-pushing the same tag only works for *pre-upload* failures
(`skip-existing` covers a partial upload). README/example edits never need a re-release.
`release.yml` runs check-version → build[% if native_extension %]/wheels[% endif %] → publish → smoke → release. The release
job extracts the `## 0.1.0` section from CHANGELOG.md for the GitHub Release notes and fails if
that section is missing. A version is permanent on PyPI once uploaded; re-pushing the same tag
only works for *pre-upload* failures (`skip-existing` covers a partial upload). README/example
edits never need a re-release.

[% endif %]## [[ '5' if publish_to_pypi else '3' ]]. Read the Docs

Expand Down