From 71f306195450bdff826f3a24e311f02cd930db38 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Tue, 16 Jun 2026 14:33:59 -0700 Subject: [PATCH] ci(coverage): gate on patch coverage via Codecov, not global threshold The global 97% vitest threshold was non-compositional: each PR was measured against a moving baseline, so the first PR to merge consumed the buffer above 97% and knocked every other open PR below the gate. Branches and functions were sitting at exactly 97.00% (zero buffer), so a single uncovered branch in any merge failed all in-flight PRs and forced everyone to re-sync and re-test. Move the 97% requirement to Codecov's patch (changed-lines) status, which depends only on a PR's own diff and is unaffected by what else merges: - vitest.config.ts: emit lcov; drop the hard 97% global thresholds to a loose 90% backstop (catastrophe net only, far below actual ~97-99%). - codecov.yml: patch target 97% (the real gate); project informational only. - ci.yml: upload coverage to Codecov (SHA-pinned v5.5.5); reword gate guidance. - CONTRIBUTING.md: document patch coverage; drop the "aim for 98% to absorb CI variance" band-aid. The 97% bar is unchanged for new code and arguably stricter: every PR must now test its own changes, instead of hiding undertested code behind a global buffer. --- .github/workflows/ci.yml | 17 ++++++++++++----- CONTRIBUTING.md | 14 ++++++++++---- codecov.yml | 35 +++++++++++++++++++++++++++++++++++ vitest.config.ts | 16 ++++++++++++---- 4 files changed, 69 insertions(+), 13 deletions(-) create mode 100644 codecov.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa3aec142b..7cb2e76172 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,11 +49,18 @@ jobs: - name: Coverage gate guidance if: ${{ failure() && steps.coverage.conclusion == 'failure' }} run: | - echo "::error title=Coverage gate::Tests or the 97% coverage gate failed." - echo "The repo enforces 97% global coverage for lines, statements, functions, and branches." - echo "Review the per-file coverage table printed above to find undercovered files and branch arms." - echo "Run 'npm run test:coverage' locally and add tests for the missing branches, fallback paths, and sanitizer rules." - echo "See CONTRIBUTING.md (Testing & coverage): aim for 98%+ branch coverage locally so small CI variance does not fail near the threshold." + echo "::error title=Coverage gate::Tests failed, or coverage fell below the 90% global backstop." + echo "The 97% requirement is enforced by Codecov on *changed lines* (the codecov/patch check), not here." + echo "This vitest step only fails on a catastrophic global drop below 90%." + echo "Review the per-file coverage table printed above, run 'npm run test:coverage' locally, and add tests for the missing branches, fallback paths, and sanitizer rules." + + - name: Upload coverage to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@0fb7174895f61a3b6b78fc075e0cd60383518dac # v5.5.5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage/lcov.info + fail_ci_if_error: false - name: Worker runtime tests run: npm run test:workers diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1cecf5b414..bc20349837 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,10 +93,16 @@ npm run ui:build npm audit --audit-level=moderate ``` -`npm run test:ci` runs the normal combined gate. Coverage must stay at or above **97%** for -statements, branches, functions, and lines. Run `npm run test:coverage` locally when you change -behavior, and aim for **98%+ branch coverage** locally so small CI variance does not fail near the -threshold. +`npm run test:ci` runs the normal combined gate. The coverage requirement is **patch coverage**: +every line your PR adds or changes must be **97%+ covered** (statements, branches, functions, lines). +This is enforced by Codecov's `codecov/patch` status check, which looks only at your diff — so it +depends solely on your own changes and is unaffected by what else merges. Run `npm run test:coverage` +locally when you change behavior and make sure your new branches, fallback paths, and sanitizer rules +are tested. + +The repo total is tracked by Codecov as a trend (informational, non-blocking). The local vitest run +keeps a loose **90%** global backstop that only trips on a catastrophic drop (e.g. a deleted test +file); it is intentionally well below actual coverage so routine PRs never fail on it. Maintainer/release smoke checks: diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..a70fed5eda --- /dev/null +++ b/codecov.yml @@ -0,0 +1,35 @@ +# Codecov configuration. +# +# The real coverage gate is `patch`: every line a PR changes must be >=97% +# covered. Patch coverage depends only on the PR's own diff, so merging one PR +# can never push another below the bar -- this is what kills the cross-PR churn +# the old global vitest threshold caused. +# +# `project` (whole-repo total) is informational only: it is reported as a trend +# but never blocks a merge. vitest keeps a loose 90% local backstop separately. +coverage: + status: + patch: + default: + target: 97% + threshold: 0% + # Only fail if the PR actually changes coverable lines. + if_ci_failed: error + project: + default: + informational: true + +# Don't post a verdict until the CI run that produced the report has finished. +require_ci_to_pass: true + +comment: + layout: "condensed_header, diff, flags, files" + require_changes: false + +# Coverage is collected by vitest (v8) over src/**; mirror its exclusions so the +# Codecov total matches the local report. +ignore: + - "src/env.d.ts" + - "apps/**" + - "test/**" + - "scripts/**" diff --git a/vitest.config.ts b/vitest.config.ts index 54c08855fe..d57126acf2 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -20,11 +20,19 @@ export default defineConfig({ provider: "v8", include: ["src/**/*.ts"], exclude: ["src/env.d.ts", "apps/**"], + // Emit lcov for Codecov to compute patch (changed-lines) coverage. + reporter: ["text", "lcov"], + // The 97% requirement now lives in codecov.yml as a *patch* gate (changed + // lines only), which is compositional: merging one PR can't drop another + // below the bar. These global thresholds are only a loose catastrophe net + // (e.g. a deleted test file), set well below actual coverage so routine + // PRs never trip them. Do NOT raise these toward the real coverage number + // or the cross-PR churn returns. thresholds: { - lines: 97, - functions: 97, - branches: 97, - statements: 97, + lines: 90, + functions: 90, + branches: 90, + statements: 90, }, }, },