diff --git a/README.md b/README.md index a4df7e7..205509e 100644 --- a/README.md +++ b/README.md @@ -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 `## ` 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) diff --git a/template/.github/workflows/release.yml.jinja b/template/.github/workflows/release.yml.jinja index 5b46dfc..af0a938 100644 --- a/template/.github/workflows/release.yml.jinja +++ b/template/.github/workflows/release.yml.jinja @@ -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" diff --git a/template/CHANGELOG.md.jinja b/template/CHANGELOG.md.jinja new file mode 100644 index 0000000..69609bf --- /dev/null +++ b/template/CHANGELOG.md.jinja @@ -0,0 +1,5 @@ +# Changelog + +## 0.0.1.dev0 + +- Initial project scaffold. diff --git a/template/SETUP.md.jinja b/template/SETUP.md.jinja index 8d09a4f..52037b8 100644 --- a/template/SETUP.md.jinja +++ b/template/SETUP.md.jinja @@ -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 `## ` 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