diff --git a/.github/publish-git-tag.sh b/.github/publish-git-tag.sh index 1e35385..e7300d0 100755 --- a/.github/publish-git-tag.sh +++ b/.github/publish-git-tag.sh @@ -1,24 +1,43 @@ #!/usr/bin/env bash -# Tag the commit that published this version. +# Tag the commit that published this version, and create its GitHub release. # # Run from the publish job after the version bump has landed on main. Exits -# non-zero if tagging fails, so a broken release is visible rather than silent. +# non-zero if either step fails, so a broken release is visible rather than +# silent. Both steps are independently idempotent: an existing tag does not +# stop the release from being created, so a rerun after a partial failure +# finishes the job rather than passing with nothing done. set -euo pipefail PYTHON=$(command -v python || command -v python3) VERSION=$("$PYTHON" -c "import re, pathlib; print(re.search(r'^version\s*=\s*\"(\d+\.\d+\.\d+)\"', pathlib.Path('pyproject.toml').read_text(), re.M).group(1))") TAG="v${VERSION}" -if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then - echo "Tag ${TAG} already exists locally; nothing to do." - exit 0 +# The remote is the only state that matters here: gh release create +# --verify-tag reads the tag from the remote, so a tag that exists only +# locally must still be pushed. Decide on the remote, then make the local +# tag match and push it. +if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then + echo "Tag ${TAG} already exists on the remote." +else + echo "Tagging ${TAG}" + # Create it only if it is not already here; git tag fails on an existing name. + git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null || git tag "${TAG}" + git push origin "${TAG}" fi -if git ls-remote --exit-code --tags origin "refs/tags/${TAG}" >/dev/null 2>&1; then - echo "Tag ${TAG} already exists on the remote; nothing to do." +# Zenodo archives on the GitHub release, not on the tag, so a tag alone leaves +# the DOI pointing at whatever was last released by hand. +if gh release view "${TAG}" >/dev/null 2>&1; then + echo "Release ${TAG} already exists." exit 0 fi -echo "Tagging ${TAG}" -git tag "${TAG}" -git push origin "${TAG}" +# --verify-tag so gh aborts if the tag is missing from the remote, rather than +# creating one itself at the default branch head. +echo "Creating release ${TAG}" +NOTES=$("$PYTHON" .github/release_notes.py "${VERSION}" 2>/dev/null || true) +if [ -n "${NOTES}" ]; then + printf '%s\n' "${NOTES}" | gh release create "${TAG}" --title "${TAG}" --verify-tag --notes-file - +else + gh release create "${TAG}" --title "${TAG}" --verify-tag --generate-notes +fi diff --git a/.github/release_notes.py b/.github/release_notes.py new file mode 100644 index 0000000..cd72474 --- /dev/null +++ b/.github/release_notes.py @@ -0,0 +1,23 @@ +"""Print the CHANGELOG section for one version, for use as release notes. + +towncrier writes the curated entry; `gh release create --generate-notes` would +write a raw commit list instead. Prints nothing if the section is absent, and +the caller falls back to generated notes. +""" + +import re +import sys +from pathlib import Path + + +def section_for(changelog: str, version: str) -> str: + pattern = rf"^## \[{re.escape(version)}\][^\n]*\n(.*?)(?=^## \[|\Z)" + match = re.search(pattern, changelog, re.M | re.S) + return match.group(1).strip() if match else "" + + +if __name__ == "__main__": + path = Path("CHANGELOG.md") + if not path.exists(): + sys.exit(0) + print(section_for(path.read_text(), sys.argv[1])) diff --git a/.github/workflows/versioning.yaml b/.github/workflows/versioning.yaml index fc66f24..0ca75d3 100644 --- a/.github/workflows/versioning.yaml +++ b/.github/workflows/versioning.yaml @@ -79,5 +79,7 @@ jobs: skip-existing: true # After the upload, so a tag failure can never hold back a release. # skip-existing above keeps a rerun safe. - - name: Publish a git tag + - name: Publish a git tag and GitHub release run: ".github/publish-git-tag.sh" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/changelog.d/auto-release.changed.md b/changelog.d/auto-release.changed.md new file mode 100644 index 0000000..00c5f75 --- /dev/null +++ b/changelog.d/auto-release.changed.md @@ -0,0 +1 @@ +Releases are now created automatically from the tag, so Zenodo archives every version rather than only those released by hand.