Skip to content

Commit 9a23dca

Browse files
authored
ci: gate combined coverage total against an 85% regression floor (#333)
1 parent fc22a6e commit 9a23dca

1 file changed

Lines changed: 21 additions & 2 deletions

File tree

.ci/coverage_report.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@
1313
1414
Run via ``uv run --no-project --with 'coverage[toml]' python .ci/coverage_report.py``;
1515
coverage settings are read from ``[tool.coverage.*]`` in ``pyproject.toml``.
16+
17+
The script also enforces a regression floor on the *combined* total
18+
(``MIN_TOTAL_COVERAGE``): a dispatch whose total drops below it fails this job.
19+
The threshold lives here rather than in ``[tool.coverage.report] fail_under``
20+
on purpose — pytest-cov reads that key, and each per-job ``--cov`` run measures
21+
only a slice of the package, so a config-level ``fail_under`` would fail every
22+
partial run. Enforcing here gates the combined total only.
1623
"""
1724

1825
from __future__ import annotations
@@ -27,9 +34,14 @@
2734

2835
logger = logging.getLogger("coverage_report")
2936

37+
# Minimum acceptable combined coverage (%). Bump this as coverage improves to
38+
# ratchet the floor up; keep it a few points below the current total so normal
39+
# churn doesn't trip it.
40+
MIN_TOTAL_COVERAGE = 85.0
41+
3042

3143
def main() -> int:
32-
"""Combine coverage data, emit reports, and write the GitHub step summary."""
44+
"""Combine coverage data, emit reports, write the GitHub step summary, gate the total."""
3345
logging.basicConfig(level=logging.INFO, format="%(message)s", stream=sys.stderr)
3446

3547
cov = coverage.Coverage()
@@ -41,15 +53,22 @@ def main() -> int:
4153
cov.html_report()
4254

4355
logger.info("Total coverage: %.2f%%", total)
56+
passed = total >= MIN_TOTAL_COVERAGE
4457

4558
summary_path = os.environ.get("GITHUB_STEP_SUMMARY")
4659
if summary_path:
4760
compact = io.StringIO()
4861
cov.report(file=compact, show_missing=False)
49-
body = f"### Test coverage: {total:.2f}%\n\n```\n{compact.getvalue()}```\n"
62+
gate_icon = "✅" if passed else "❌"
63+
gate_line = f"{gate_icon} Gate: {total:.2f}% vs {MIN_TOTAL_COVERAGE:.2f}% minimum\n"
64+
body = f"### Test coverage: {total:.2f}%\n\n{gate_line}\n```\n{compact.getvalue()}```\n"
5065
with Path(summary_path).open("a", encoding="utf-8") as fh:
5166
fh.write(body)
5267

68+
if not passed:
69+
logger.error("Coverage %.2f%% is below the required minimum of %.2f%%", total, MIN_TOTAL_COVERAGE)
70+
return 1
71+
5372
return 0
5473

5574

0 commit comments

Comments
 (0)