From c49b516ce197a8126790f1161b78fafcf88df747 Mon Sep 17 00:00:00 2001 From: iback Date: Mon, 5 Oct 2026 08:21:19 +0000 Subject: [PATCH] Add Codecov upload and PyPI release workflow The README badges for coverage and released version need something behind them. Two pieces: - tests.yml runs the suite under `coverage` and uploads coverage.xml to Codecov from the 3.11 matrix entry only; the matrix differs in interpreter version, not in which lines execute, so three uploads would be redundant. Coverage scope lives in pyproject.toml and omits the vendored MONAI transforms, matching their existing ruff and mypy exemptions. - python-publish.yml builds an sdist and wheel on a `v*` tag push, which is the release step CONTRIBUTING.md already documents, and publishes via PyPI Trusted Publishing so there is no long-lived API token in the repo secrets. Codecov thresholds are informational: at 32% line coverage a hard gate would only block pull requests that are not the cause. Both badges render as "unknown"/"not found" until the repository is added on Codecov and the first release is published. CONTRIBUTING.md documents both one-time setup steps. Co-Authored-By: Claude Opus 5 --- .github/workflows/python-publish.yml | 61 ++++++++++++++++++++++++++++ .github/workflows/tests.yml | 17 +++++++- CONTRIBUTING.md | 39 +++++++++++++++++- README.md | 2 + codecov.yml | 22 ++++++++++ pyproject.toml | 16 +++++++- 6 files changed, 153 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/python-publish.yml create mode 100644 codecov.yml 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"]