diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..7d0ba6f --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,61 @@ +name: release_to_pypi + +# CONTRIBUTING.md documents releasing as `git tag vX.Y.Z && git push --tags`; +# that tag push is what triggers this workflow. poetry-dynamic-versioning reads +# the version straight off the tag, so nothing in the repo needs bumping. +on: + push: + tags: ["v*"] + +permissions: + contents: read + +jobs: + build: + name: Build distributions + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # poetry-dynamic-versioning shells out to `git describe`, so the tag + # must be in the clone - a clone without tags silently builds 0.0.0. + # actions/checkout does fetch the triggering tag even at depth 1; the + # full history is what keeps the commit-distance part of development + # versions correct, and it is what upstream recommends. + fetch-depth: 0 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install build tooling + run: | + python -m pip install --upgrade pip + pip install build + - name: Build sdist and wheel + run: python -m build + - name: Check metadata renders on PyPI + run: | + pip install twine + twine check dist/* + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + + publish: + name: Publish to PyPI + needs: build + runs-on: ubuntu-latest + # Trusted Publishing: PyPI verifies this workflow's OIDC identity, so there + # is no API token to store or rotate. Configure the publisher once at + # https://pypi.org/manage/account/publishing/ (see CONTRIBUTING.md). + environment: + name: pypi + url: https://pypi.org/p/verpex + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + - uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 46e43bb..eb90eb4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -27,6 +27,19 @@ jobs: run: | python -m pip install --upgrade pip pip install -e . - pip install pytest + pip install pytest coverage - name: Test - run: python -m pytest -v + # Coverage settings (source, omissions) live in pyproject.toml. + run: | + coverage run -m pytest -v + coverage xml + - name: Upload coverage to Codecov + # One upload per commit is enough: the matrix differs only in interpreter + # version, not in which lines run. + if: matrix.python-version == '3.11' + uses: codecov/codecov-action@v5 + with: + files: coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} + # A Codecov outage should not turn the test suite red. + fail_ci_if_error: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bf63331..c0d30e4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -134,7 +134,7 @@ treat the names as a public interface: rename only deliberately, and update ## Releasing -The version is not written in `pyproject.toml` — it is derived from the latest git +The version is not written in `pyproject.toml` - it is derived from the latest git tag at build time by [poetry-dynamic-versioning](https://github.com/mtkennerly/poetry-dynamic-versioning). `version = "0.0.0"` is a placeholder. @@ -146,3 +146,40 @@ git tag v0.1.0 && git push --tags Between tags the version reads as `0.1.0.post.dev0+`. A repository with **no tags at all** builds as `0.0.0.post.dev0+`, so tag once after the initial commit. + +Pushing a `v*` tag triggers `.github/workflows/python-publish.yml`, which builds an +sdist and a wheel, runs `twine check` on them, and uploads to PyPI. The checkout in +that workflow uses `fetch-depth: 0` because poetry-dynamic-versioning calls +`git describe`: a clone that carries no tags builds as `0.0.0` without failing, so the +mistake would only surface on PyPI. + +### One-time PyPI setup + +Publishing uses [Trusted Publishing](https://docs.pypi.org/trusted-publishers/), so +there is no API token in the repository secrets. Before the first release, add a +pending publisher at : + +| Field | Value | +| --- | --- | +| PyPI project name | `verpex` | +| Owner | `Hendrik-code` | +| Repository name | `verpex` | +| Workflow name | `python-publish.yml` | +| Environment name | `pypi` | + +Then create a `pypi` environment under the repository's *Settings -> Environments*. +Restricting it to tag pushes means a release cannot be published from a branch. + +### Coverage + +`tests.yml` runs the suite under `coverage` and uploads `coverage.xml` to Codecov +from the Python 3.11 matrix entry. Add the repository at + and store its upload token as the +`CODECOV_TOKEN` repository secret. Thresholds live in `codecov.yml` and are +informational, so a coverage drop annotates a pull request without blocking it. + +Locally: + +```bash +coverage run -m pytest && coverage report +``` diff --git a/README.md b/README.md index da40ab5..219b30b 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,11 @@
[![Paper](https://img.shields.io/badge/Paper-DT4H%202026-B31B1B)](#citation) +[![Stable Version](https://img.shields.io/pypi/v/verpex?label=stable)](https://pypi.python.org/pypi/verpex/) [![Python Versions](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12-blue)](https://www.python.org/downloads/) [![tests](https://github.com/Hendrik-code/verpex/actions/workflows/tests.yml/badge.svg)](https://github.com/Hendrik-code/verpex/actions/workflows/tests.yml) [![lint](https://github.com/Hendrik-code/verpex/actions/workflows/lint.yml/badge.svg)](https://github.com/Hendrik-code/verpex/actions/workflows/lint.yml) +[![codecov](https://codecov.io/gh/Hendrik-code/verpex/graph/badge.svg)](https://codecov.io/gh/Hendrik-code/verpex) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![Built on TPTBox](https://img.shields.io/badge/built%20on-TPTBox-4c8eda)](https://github.com/Hendrik-code/TPTBox) [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..bd737dc --- /dev/null +++ b/codecov.yml @@ -0,0 +1,22 @@ +# Coverage gates. Both are informational for now: Codecov reports the numbers on +# every PR but does not fail the check, so the badge can go up before the suite +# is broad enough to defend a hard threshold. +coverage: + status: + project: + default: + target: auto + threshold: 1% + informational: true + patch: + default: + target: auto + informational: true + +comment: + layout: "condensed_header, diff, files" + require_changes: true + +ignore: + - "unit_tests/**" + - "verpex/data/transforms.py" # vendored MONAI, kept diffable against upstream diff --git a/pyproject.toml b/pyproject.toml index e0a5ccf..6844a21 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ pytest = ">=8.0" poetry-dynamic-versioning = ">=1.0.0,<2.0.0" ruff = ">=0.11" pre-commit = "*" -coverage = ">=7.0" +coverage = ">=7.2" mypy = ">=1.8" pandas-stubs = "*" types-PyYAML = "*" @@ -120,6 +120,20 @@ max-complexity = 20 [tool.pytest.ini_options] testpaths = ["unit_tests"] +[tool.coverage.run] +source = ["verpex"] +branch = true +# Vendored MONAI transforms - exempted here the same way they are for ruff and mypy. +omit = ["verpex/data/transforms.py"] + +[tool.coverage.report] +exclude_also = [ + "if TYPE_CHECKING:", + "raise NotImplementedError", + "if __name__ == .__main__.:", + "@(abc\\.)?abstractmethod", +] + [tool.mypy] python_version = "3.10" files = ["verpex"]