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
40 changes: 40 additions & 0 deletions .github/scripts/changelog_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env python3
"""Print the CHANGELOG.md section for a given version.

Usage: ``python .github/scripts/changelog_notes.py 0.2.0``

Extracts the body under ``## [0.2.0] ...`` up to the next ``## [`` heading.
Falls back to a generic line if the version has no changelog section.
"""
import pathlib
import re
import sys

CHANGELOG = pathlib.Path("CHANGELOG.md")


def notes_for(version: str, text: str) -> str:
pattern = re.compile(
r"^##\s*\[" + re.escape(version) + r"\][^\n]*\n(.*?)(?=^##\s*\[|\Z)",
re.MULTILINE | re.DOTALL,
)
match = pattern.search(text)
if match:
body = match.group(1).strip()
if body:
return body
return f"Release {version}."


def main() -> int:
if len(sys.argv) != 2:
print("usage: changelog_notes.py <version>", file=sys.stderr)
return 2
version = sys.argv[1]
text = CHANGELOG.read_text(encoding="utf-8") if CHANGELOG.exists() else ""
print(notes_for(version, text))
return 0


if __name__ == "__main__":
raise SystemExit(main())
21 changes: 21 additions & 0 deletions .github/scripts/package_version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env python3
"""Print the package version (``__version__`` from the package __init__)."""
import pathlib
import re
import sys

INIT = pathlib.Path("src/PyReprism/__init__.py")


def main() -> int:
match = re.search(r'^__version__\s*=\s*["\']([^"\']+)["\']',
INIT.read_text(encoding="utf-8"), re.MULTILINE)
if not match:
print("could not find __version__ in", INIT, file=sys.stderr)
return 1
print(match.group(1))
return 0


if __name__ == "__main__":
raise SystemExit(main())
38 changes: 38 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ jobs:
with:
python-version: '3.12'

- name: Verify the tag matches the package version
run: |
PKG_VERSION="$(python .github/scripts/package_version.py)"
TAG_VERSION="${GITHUB_REF_NAME#v}"
echo "tag=$TAG_VERSION package=$PKG_VERSION"
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
echo "::error::Tag ($TAG_VERSION) does not match package version ($PKG_VERSION). Bump __version__ in src/PyReprism/__init__.py."
exit 1
fi

- name: Build and validate the distribution
run: |
python -m pip install --upgrade pip build twine
Expand Down Expand Up @@ -51,3 +61,31 @@ jobs:

- name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

github-release:
name: Create GitHub Release
needs: publish
runs-on: ubuntu-latest
permissions:
contents: write # required to create a release
steps:
- name: Check out the code
uses: actions/checkout@v4

- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/

- name: Extract release notes from CHANGELOG
run: python .github/scripts/changelog_notes.py "${GITHUB_REF_NAME#v}" > release-notes.md

- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "$GITHUB_REF_NAME" \
--title "$GITHUB_REF_NAME" \
--notes-file release-notes.md \
dist/*
7 changes: 6 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,9 @@ When you're finished with the changes, create a pull request, also known as a PR
### Your PR is merged!

Congratulations :tada: :tada: <br />
The PyReprism team thanks you :sparkles:.
The PyReprism team thanks you :sparkles:.

## Maintainers: cutting a release

Releases are automated by pushing a `vX.Y.Z` tag. See [RELEASING.md](RELEASING.md)
for the full process.
61 changes: 61 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Releasing PyReprism

Releases are automated: **pushing a `vX.Y.Z` tag** builds the package, publishes it
to PyPI, and creates a GitHub Release. You never edit the version in more than one
place — `src/PyReprism/__init__.py` is the single source of truth and
`pyproject.toml` derives the version from it.

## One-time setup (Trusted Publishing)

Publishing uses PyPI [Trusted Publishing](https://docs.pypi.org/trusted-publishers/)
(OIDC) — no API token. Configure it once:

1. **PyPI** → the `PyReprism` project → *Publishing* → add a GitHub publisher:
- Repository: `unlv-evol/PyReprism`
- Workflow: `publish.yml`
- Environment: `pypi`
2. **GitHub** → repo *Settings → Environments* → create an environment named
`pypi` (optionally require a reviewer to approve each release).

## Cutting a release

1. **Bump the version** in [`src/PyReprism/__init__.py`](src/PyReprism/__init__.py):

```python
__version__ = "0.2.0"
```

2. **Update the changelog** in [`CHANGELOG.md`](CHANGELOG.md): rename the
`## [Unreleased]` heading to `## [0.2.0] - YYYY-MM-DD`, add a fresh empty
`## [Unreleased]` above it, and update the link references at the bottom.

3. **Commit, tag, and push** (the tag must equal the version — CI verifies it):

```shell
git commit -am "release: v0.2.0"
git tag -a v0.2.0 -m "v0.2.0"
git push origin main --follow-tags
```

That's it. Pushing the tag triggers [`publish.yml`](.github/workflows/publish.yml),
which:

1. checks that the tag matches `__version__`,
2. builds the sdist + wheel and runs `twine check`,
3. publishes to PyPI via Trusted Publishing, and
4. creates a GitHub Release titled `v0.2.0` with notes taken from the matching
`CHANGELOG.md` section and the built artifacts attached.

## Versioning

PyReprism follows [Semantic Versioning](https://semver.org): bump **patch** for
fixes, **minor** for backwards-compatible features, **major** for breaking changes.

## Testing a release without publishing

To validate the build locally before tagging:

```shell
python -m build
twine check dist/*
```
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "PyReprism"
version = "0.1.0"
dynamic = ["version"]
authors = [
{ name = "UNLV EVOL LAB", email = "ogenrwot@unlv.nevada.edu" },
]
Expand All @@ -23,6 +23,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
Expand Down Expand Up @@ -52,6 +53,10 @@ Homepage = "https://github.com/unlv-evol/PyReprism"
Documentation = "https://pyreprism.readthedocs.io"
Issues = "https://github.com/unlv-evol/PyReprism/issues"

# Single source of truth for the version: __version__ in the package __init__.
[tool.hatch.version]
path = "src/PyReprism/__init__.py"

[tool.hatch.build.targets.wheel]
packages = ["src/PyReprism"]

Expand Down
Loading