From ef3e0c88720997b16ee5426ce791ae4c4105cf5d Mon Sep 17 00:00:00 2001 From: voorhs Date: Mon, 22 Jun 2026 01:29:25 +0300 Subject: [PATCH] ci: gate combined coverage total against an 85% regression floor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The coverage-report job now fails if the *combined* total drops below MIN_TOTAL_COVERAGE (85%), and writes a pass/fail gate line into the GitHub step summary. The threshold is enforced in the report script rather than via `[tool.coverage.report] fail_under`, because pytest-cov reads that key and each per-job `--cov` run measures only a slice of the package — a config-level fail_under would fail every partial run. Enforcing here gates the combined total only. Co-Authored-By: Claude Opus 4.8 --- .ci/coverage_report.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/.ci/coverage_report.py b/.ci/coverage_report.py index ee49afecb..c95577a86 100644 --- a/.ci/coverage_report.py +++ b/.ci/coverage_report.py @@ -13,6 +13,13 @@ Run via ``uv run --no-project --with 'coverage[toml]' python .ci/coverage_report.py``; coverage settings are read from ``[tool.coverage.*]`` in ``pyproject.toml``. + +The script also enforces a regression floor on the *combined* total +(``MIN_TOTAL_COVERAGE``): a dispatch whose total drops below it fails this job. +The threshold lives here rather than in ``[tool.coverage.report] fail_under`` +on purpose — pytest-cov reads that key, and each per-job ``--cov`` run measures +only a slice of the package, so a config-level ``fail_under`` would fail every +partial run. Enforcing here gates the combined total only. """ from __future__ import annotations @@ -27,9 +34,14 @@ logger = logging.getLogger("coverage_report") +# Minimum acceptable combined coverage (%). Bump this as coverage improves to +# ratchet the floor up; keep it a few points below the current total so normal +# churn doesn't trip it. +MIN_TOTAL_COVERAGE = 85.0 + def main() -> int: - """Combine coverage data, emit reports, and write the GitHub step summary.""" + """Combine coverage data, emit reports, write the GitHub step summary, gate the total.""" logging.basicConfig(level=logging.INFO, format="%(message)s", stream=sys.stderr) cov = coverage.Coverage() @@ -41,15 +53,22 @@ def main() -> int: cov.html_report() logger.info("Total coverage: %.2f%%", total) + passed = total >= MIN_TOTAL_COVERAGE summary_path = os.environ.get("GITHUB_STEP_SUMMARY") if summary_path: compact = io.StringIO() cov.report(file=compact, show_missing=False) - body = f"### Test coverage: {total:.2f}%\n\n```\n{compact.getvalue()}```\n" + gate_icon = "✅" if passed else "❌" + gate_line = f"{gate_icon} Gate: {total:.2f}% vs {MIN_TOTAL_COVERAGE:.2f}% minimum\n" + body = f"### Test coverage: {total:.2f}%\n\n{gate_line}\n```\n{compact.getvalue()}```\n" with Path(summary_path).open("a", encoding="utf-8") as fh: fh.write(body) + if not passed: + logger.error("Coverage %.2f%% is below the required minimum of %.2f%%", total, MIN_TOTAL_COVERAGE) + return 1 + return 0