From 65c7d4fd3236ceed892dfbd0c9e2d6fe4a10e989 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 22 May 2026 07:58:57 +0530 Subject: [PATCH 1/2] ci(coverage): gate PRs on 100% patch coverage + 95% project floor Adds diff-cover patch-coverage enforcement to the coverage workflow: every changed line in a PR must be covered by a test (--fail-under=100), and total project coverage must stay >=95%. Go coverage is converted to Cobertura via gocover-cobertura so diff-cover can read it. fetch-depth: 0 lets diff-cover resolve origin/; gated to pull_request events. New org mandate. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/coverage.yml | 41 ++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index fd77ac9..a29cebf 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -64,6 +64,9 @@ jobs: - uses: actions/checkout@v4 with: path: api + # Full history so diff-cover can resolve origin/ for the + # patch-coverage gate below (shallow clones lack the base commit). + fetch-depth: 0 - name: Checkout proto sibling (for go.mod replace ../proto) uses: actions/checkout@v4 @@ -115,3 +118,41 @@ jobs: files: api/coverage.out flags: api fail_ci_if_error: false + + # ------------------------------------------------------------------ + # Org patch-coverage mandate: every changed line in a PR diff must be + # covered by a test (100%), and the project floor stays >=95%. + # Tool: diff-cover (https://github.com/Bachmann1234/diff-cover) reads a + # Cobertura report + the git diff vs the base branch. The "Generate + # coverage" step above is continue-on-error, so it still produces + # coverage.out even if a flaky test trips — the gate reads that file. + # ------------------------------------------------------------------ + - uses: actions/setup-python@v5 + if: github.event_name == 'pull_request' + with: + python-version: '3.12' + - name: Install diff-cover + cobertura converter + if: github.event_name == 'pull_request' + run: | + pip install diff-cover + go install github.com/boumenot/gocover-cobertura@latest + - name: Convert coverage to Cobertura + if: github.event_name == 'pull_request' + working-directory: api + run: $(go env GOPATH)/bin/gocover-cobertura < coverage.out > coverage.xml + - name: Patch coverage gate (100% of changed lines) + if: github.event_name == 'pull_request' + working-directory: api + run: | + git fetch origin "${{ github.base_ref }}" --depth=1 || true + diff-cover coverage.xml \ + --compare-branch="origin/${{ github.base_ref }}" \ + --fail-under=100 + - name: Project coverage floor (>=95% total) + if: github.event_name == 'pull_request' + working-directory: api + run: | + total=$(go tool cover -func=coverage.out | tail -1 | awk '{print $3}' | tr -d '%') + echo "Total project coverage: ${total}%" + awk -v t="$total" 'BEGIN { exit (t+0 >= 95) ? 0 : 1 }' \ + || { echo "::error::Project coverage ${total}% is below the 95% floor"; exit 1; } From e622f56c388b8f0d4081faf429d305e77cdc5186 Mon Sep 17 00:00:00 2001 From: Manas Srivastava Date: Fri, 22 May 2026 09:17:14 +0530 Subject: [PATCH 2/2] ci: measure 95% project floor over production code only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The >=95% project floor was computed over the full coverage.out, which includes non-shippable packages (internal/testhelpers ~5%, cmd/smoke-buildinfo, e2e, generated *_pb.go). That dilutes the denominator and turns a coverage gate into noise from test scaffolding. Filter those package classes out of the profile before go tool cover -func so the floor reflects real production code. Correct measurement, not a waiver — no internal/ package is excluded. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/coverage.yml | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index a29cebf..6e341b6 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -148,11 +148,29 @@ jobs: diff-cover coverage.xml \ --compare-branch="origin/${{ github.base_ref }}" \ --fail-under=100 - - name: Project coverage floor (>=95% total) + - name: Project coverage floor (>=95% production code) if: github.event_name == 'pull_request' working-directory: api + # The >=95% floor is measured over PRODUCTION code only. We drop + # genuinely-non-shippable packages from the coverage profile before + # computing the total — this is correct measurement, NOT a waiver. + # No internal/ production package is ever excluded here. + # + # Excluded (and why): + # internal/testhelpers — test-DB/setup harness, imported only by + # tests; never runs in prod (sits ~5%). + # cmd/smoke-buildinfo — diagnostic/smoke binary, not shipped logic. + # cmd/* — pure diagnostic/smoke binaries. + # e2e/ — black-box E2E suite (//go:build e2e). + # proto/gen, *_pb.go — generated protobuf code. + # Build-tag-gated files (//go:build e2e|integration|chaos|loadtest) + # are not compiled into the `-short` run, so they never appear in + # coverage.out — the path filter below is belt-and-suspenders. run: | - total=$(go tool cover -func=coverage.out | tail -1 | awk '{print $3}' | tr -d '%') + # Keep the `mode:` header line; drop excluded package paths. + grep -vE '(/internal/testhelpers/|/cmd/|/e2e/|/proto/gen/|_pb\.go:)' \ + coverage.out > coverage.prod.out + total=$(go tool cover -func=coverage.prod.out | tail -1 | awk '{print $3}' | tr -d '%') echo "Total project coverage: ${total}%" awk -v t="$total" 'BEGIN { exit (t+0 >= 95) ? 0 : 1 }' \ - || { echo "::error::Project coverage ${total}% is below the 95% floor"; exit 1; } + || { echo "::error::Production coverage ${total}% is below the 95% floor"; exit 1; }