From e3be72b715345d061f8872efb3da9f850a7943b8 Mon Sep 17 00:00:00 2001 From: Paul Clark Date: Thu, 20 Aug 2026 19:06:30 -0400 Subject: [PATCH] ci: make the coverage gate actionable and always upload the report Two changes, both aimed at the same problem: when coverage drifts, the failure tells you nothing about where. 1. The gate printed only the aggregate percentages. It now also lists the fifteen least-covered classes when it fails, so the next step is obvious rather than a hunt. 2. `Upload coverage report` ran without `always()`, and the gate above it fails the job -- so the one artifact that would explain the failure was the one thing never uploaded. Confirmed against the last eight main runs: every one has zero artifacts. Now uploads regardless of the gate's outcome. This does not change the thresholds or fix the drift itself, tracked in #25. Coverage is currently 78.25% line / 69.32% branch against 85%/75%, and has been unmeasurable since 2026-06-04 because restore failed on NU1903 before the tests ran. Note this cannot be verified locally: .NET on macOS has no ML-KEM implementation (Apple's crypto stack, not OpenSSL -- confirmed with openssl@3 3.6.3 on DYLD_LIBRARY_PATH and CLR_OPENSSL_VERSION_OVERRIDE, both still report MLKem.IsSupported = False), so 56 of 101 tests skip here and local coverage reads 21.95%. The gate's new reporting logic was exercised against a real cobertura report from that local run; only the numbers differ. Co-Authored-By: Claude Opus 5 --- .github/workflows/ci.yml | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af3481c..1eb9fbd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -132,23 +132,37 @@ jobs: branch=$(python3 -c "import xml.etree.ElementTree as ET; print(ET.parse('$report').getroot().get('branch-rate'))") echo "line-rate=$line" echo "branch-rate=$branch" - python3 - <= {want:.0%}") + if not ok: + rows = [] + for cls in root.iter("class"): + lines = cls.find("lines") + total = len(lines.findall("line")) if lines is not None else 0 + if total: + rows.append((float(cls.get("line-rate")), total, cls.get("name"))) + rows.sort() + print("\nLeast-covered classes (line%, lines, name):") + for rate, total, name in rows[:15]: + print(f" {rate:6.1%} {total:5d} {name}") + sys.exit(0 if ok else 1) EOF - name: Upload coverage report - if: matrix.os == 'ubuntu-latest' + # always(): the gate above fails the job, and without this the coverage report -- + # the only artifact that explains why -- is never uploaded. + if: always() && matrix.os == 'ubuntu-latest' uses: actions/upload-artifact@v7 with: name: coverage