From b965a2e1a5034b7e1d7c983fcd5389fcba0b3dc1 Mon Sep 17 00:00:00 2001 From: David Perl Date: Mon, 3 Aug 2026 14:53:35 +0200 Subject: [PATCH] ci: fail diff cover below threshold --- .gitea/workflows/ci.yml | 87 +++++++++++++++++++++++++++++++++++++---- pyproject.toml | 1 + 2 files changed, 81 insertions(+), 7 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 76262d70..32fc0beb 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -109,10 +109,15 @@ jobs: test-with-coverage: runs-on: ubuntu-latest + outputs: + diff-coverage: ${{ steps.diff_cover.outputs.percent }} steps: - name: Checkout code uses: https://github.com/actions/checkout@v5 + with: + # diff-cover needs the base branch present to diff against + fetch-depth: 0 - name: Setup uses: ./.gitea/actions/install @@ -129,21 +134,71 @@ jobs: source .venv/bin/activate pytest --cov=aare --cov-config=./pyproject.toml --cov-branch --cov-report=xml --no-cov-on-fail ./tests/unit - - name: Build coverage summary + # No --fail-under here: the gate lives in coverage-analysis so that the PR + # comment is posted before the build is failed. + - name: Diff coverage + id: diff_cover + env: + BASE_BRANCH: ${{ github.base_ref || 'main' }} run: | source .venv/bin/activate + diff-cover coverage.xml \ + --compare-branch="origin/${BASE_BRANCH}" \ + --format "markdown:diff-cover.md,json:diff-cover.json" + percent=$(python -c "import json; print(json.load(open('diff-cover.json'))['total_percent_covered'])") + echo "percent=${percent}" >> "$GITHUB_OUTPUT" + echo "Diff coverage: ${percent}%" + + # Two artefacts on purpose: the full report is too big to post. The F5 WAF + # in front of gitea.psi.ch rejects large POST bodies with an HTML 403, so + # the comment body is capped and the detail lives in the artifact. + - name: Build coverage summary + env: + BASE_BRANCH: ${{ github.base_ref || 'main' }} + DIFF_COVERAGE: ${{ steps.diff_cover.outputs.percent }} + COMMENT_DIFF_BUDGET: "6000" + run: | + source .venv/bin/activate + total=$(coverage report --format=total) + + { + echo "# Coverage report" + echo + echo "| Metric | Value |" + echo "| --- | --- |" + echo "| Total line + branch coverage | ${total}% |" + echo "| Diff coverage vs \`${BASE_BRANCH}\` | ${DIFF_COVERAGE}% (minimum 80%) |" + echo + cat diff-cover.md + echo + echo "## Per-file breakdown" + echo + coverage report --format=markdown + } > coverage-summary.md + { echo "" echo "### Coverage report" echo - echo "**Total line + branch coverage: $(coverage report --format=total)%**" + echo "| Metric | Value |" + echo "| --- | --- |" + echo "| Total line + branch coverage | ${total}% |" + echo "| Diff coverage vs \`${BASE_BRANCH}\` | ${DIFF_COVERAGE}% (minimum 80%) |" echo - echo "
Per-file breakdown" + echo "
Uncovered lines in this diff" echo - coverage report --format=markdown + if [ "$(wc -c < diff-cover.md)" -gt "${COMMENT_DIFF_BUDGET}" ]; then + head -c "${COMMENT_DIFF_BUDGET}" diff-cover.md + echo + echo "_(truncated)_" + else + cat diff-cover.md + fi echo echo "
" - } > coverage-summary.md + echo + echo "_Full per-file breakdown: \`coverage\` artifact on this run._" + } > coverage-comment.md # Only the rendered summary is uploaded. coverage.xml (~1 MB) is rejected # with an HTML 403 by the proxy in front of gitea.psi.ch. @@ -151,7 +206,9 @@ jobs: uses: https://github.com/actions/upload-artifact@v3 with: name: coverage - path: coverage-summary.md + path: | + coverage-summary.md + coverage-comment.md if-no-files-found: error coverage-analysis: @@ -180,5 +237,21 @@ jobs: with: comment-id: ${{ steps.fc.outputs.comment-id }} issue-number: ${{ github.event.pull_request.number }} - body-path: coverage-summary.md + body-path: coverage-comment.md edit-mode: replace + + # Last, so the report is always visible on the PR even when this fails. + - name: Enforce diff coverage + env: + DIFF_COVERAGE: ${{ needs.test-with-coverage.outputs.diff-coverage }} + MINIMUM: "80" + run: | + if [ -z "${DIFF_COVERAGE}" ]; then + echo "::error::test-with-coverage produced no diff coverage value" + exit 1 + fi + echo "Diff coverage: ${DIFF_COVERAGE}% (minimum ${MINIMUM}%)" + if awk -v p="${DIFF_COVERAGE}" -v m="${MINIMUM}" 'BEGIN { exit !(p + 0 < m + 0) }'; then + echo "::error::Diff coverage ${DIFF_COVERAGE}% is below the required ${MINIMUM}%" + exit 1 + fi diff --git a/pyproject.toml b/pyproject.toml index 005125bc..3408b544 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,6 +43,7 @@ test = [ "pytest-qt", "pytest-asyncio", "pytest-timeout", + "diff-cover", "ruff>=0.15" ]