Skip to content

Commit 5467233

Browse files
authored
fix(ci): treat a passed scoped run with no src coverage as legitimate, not a missing report (#8195)
The third scoped-selection edge case in a week (PR #8192's shards): the selection matched real tests that PASSED but exercised nothing under coverage.include (src/**) — a test-only/docs-drift diff — so --coverage.all=false wrote an empty lcov and 'Verify coverage report exists' failed a green run. no_tests_matched cannot see this case because tests DID match. The scoped branch now derives a second output (no_src_coverage) from the run itself: reachable only on vitest exit 0, detected by the absent/empty lcov, proven by the junit report the same command wrote. Every verify/upload guard carries both escapes, and the codecov-policy drift test pins the full condition strings plus both detector lines so neither can silently vanish. Scoped selection itself stays DISABLED via SCOPED_TEST_SELECTION_ENABLED=false until the re-enable decision documented on the issue. Advances #8194
1 parent 89e2c4e commit 5467233

2 files changed

Lines changed: 35 additions & 12 deletions

File tree

.github/workflows/ci.yml

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1058,6 +1058,16 @@ jobs:
10581058
npm run test:coverage -- --maxWorkers=4 --shard=${{ matrix.shard }}/3 --reporter=default --reporter=blob --reporter=junit --outputFile.blob=blob-report/report-${{ matrix.shard }}.blob --outputFile.junit=reports/junit/vitest.xml "${EXCLUDE_ARGS[@]}" "${SCOPE_ARGS[@]}" 2>&1 | tee vitest-scoped-output.log
10591059
if grep -q "No test files found" vitest-scoped-output.log; then
10601060
echo "no_tests_matched=true" >> "$GITHUB_OUTPUT"
1061+
elif [ ! -s coverage/lcov.info ]; then
1062+
# #8194 (the week's third scoped-selection edge case, PR #8192's shards): the selection
1063+
# matched and PASSED real tests -- set -o pipefail above means this line is only reachable
1064+
# on vitest exit 0 -- but none of them exercise anything under coverage.include (src/**):
1065+
# a test-only/docs-drift diff. --coverage.all=false then writes an empty or absent lcov,
1066+
# which is NOT a failed run; the junit report written above is the proof the suite ran.
1067+
# Without this output, "Verify coverage report exists" fails the green run. The success
1068+
# signal here is the test run itself, never the coverage file's size.
1069+
echo "scoped run passed but exercised no src/** files -- skipping coverage artifacts for this shard"
1070+
echo "no_src_coverage=true" >> "$GITHUB_OUTPUT"
10611071
fi
10621072
else
10631073
# Duration-aware sharding (#ci-duration-aware-sharding), full-suite case only: vitest's own
@@ -1092,10 +1102,12 @@ jobs:
10921102
- name: Verify coverage report exists
10931103
# Skipped when the scoped-selection branch above matched zero test files (steps.coverage.outputs.
10941104
# no_tests_matched) -- a legitimately test-free diff (e.g. docs-only under a scoped path) writes no
1095-
# coverage/lcov.info at all, and that is not a failure. A real failure (tests ran and either failed
1096-
# or the coverage step crashed before writing output) still fails this job via `success()` below,
1097-
# same as before.
1098-
if: ${{ success() && steps.coverage.outputs.no_tests_matched != 'true' }}
1105+
# coverage/lcov.info at all, and that is not a failure. Also skipped when the selection matched
1106+
# tests that PASSED but exercised nothing under src/** (no_src_coverage, #8194) -- same legitimacy,
1107+
# different detector: tests matched, so no_tests_matched can't see it. A real failure (tests ran
1108+
# and either failed or the coverage step crashed before writing output) still fails this job via
1109+
# `success()` below, same as before.
1110+
if: ${{ success() && steps.coverage.outputs.no_tests_matched != 'true' && steps.coverage.outputs.no_src_coverage != 'true' }}
10991111
run: |
11001112
if [ ! -s coverage/lcov.info ]; then
11011113
echo "::error title=Coverage::coverage/lcov.info is missing or empty"
@@ -1104,7 +1116,7 @@ jobs:
11041116
# Consumed by validate-tests-merge to re-check the global coverage threshold against all shards
11051117
# combined -- see this job's own header comment.
11061118
- name: Upload coverage blob report
1107-
if: ${{ success() && steps.coverage.outputs.no_tests_matched != 'true' }}
1119+
if: ${{ success() && steps.coverage.outputs.no_tests_matched != 'true' && steps.coverage.outputs.no_src_coverage != 'true' }}
11081120
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
11091121
with:
11101122
name: coverage-blob-shard-${{ matrix.shard }}
@@ -1115,7 +1127,7 @@ jobs:
11151127
- name: Upload coverage to Codecov
11161128
# Same no_tests_matched skip as the verify step above (#8167 follow-up): with zero matched tests
11171129
# there is no lcov to upload, and fail_ci_if_error would otherwise turn that non-event red.
1118-
if: ${{ success() && steps.coverage.outputs.no_tests_matched != 'true' && github.event.pull_request.head.repo.fork != true }}
1130+
if: ${{ success() && steps.coverage.outputs.no_tests_matched != 'true' && steps.coverage.outputs.no_src_coverage != 'true' && github.event.pull_request.head.repo.fork != true }}
11191131
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
11201132
with:
11211133
token: ${{ secrets.CODECOV_TOKEN }}
@@ -1133,7 +1145,7 @@ jobs:
11331145
# override, etc). Condition dropped the push-or-backend check since this job's own `if:` already
11341146
# covers it.
11351147
- name: Upload coverage to Codecov (fork PR tokenless)
1136-
if: ${{ success() && steps.coverage.outputs.no_tests_matched != 'true' && github.event.pull_request.head.repo.fork == true }}
1148+
if: ${{ success() && steps.coverage.outputs.no_tests_matched != 'true' && steps.coverage.outputs.no_src_coverage != 'true' && github.event.pull_request.head.repo.fork == true }}
11371149
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
11381150
with:
11391151
files: ./coverage/lcov.info
@@ -1147,7 +1159,7 @@ jobs:
11471159
# their upload non-blocking so a JUnit ingestion hiccup does not fail CI
11481160
# after the tests and hard coverage upload have already passed.
11491161
- name: Upload Vitest results to Codecov
1150-
if: ${{ !cancelled() && steps.coverage.outputs.no_tests_matched != 'true' && github.event.pull_request.head.repo.fork != true }}
1162+
if: ${{ !cancelled() && steps.coverage.outputs.no_tests_matched != 'true' && steps.coverage.outputs.no_src_coverage != 'true' && github.event.pull_request.head.repo.fork != true }}
11511163
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
11521164
with:
11531165
token: ${{ secrets.CODECOV_TOKEN }}
@@ -1160,7 +1172,7 @@ jobs:
11601172
override_pr: ${{ github.event_name == 'pull_request' && github.event.pull_request.number || '' }}
11611173
fail_ci_if_error: false
11621174
- name: Upload Vitest results to Codecov (fork PR tokenless)
1163-
if: ${{ !cancelled() && steps.coverage.outputs.no_tests_matched != 'true' && github.event.pull_request.head.repo.fork == true }}
1175+
if: ${{ !cancelled() && steps.coverage.outputs.no_tests_matched != 'true' && steps.coverage.outputs.no_src_coverage != 'true' && github.event.pull_request.head.repo.fork == true }}
11641176
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
11651177
with:
11661178
files: ./reports/junit/vitest.xml

test/unit/codecov-policy.test.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,23 @@ describe("Codecov policy", () => {
6161
// every upload below it — must skip on that output rather than fail closed on a missing lcov. The
6262
// policy stays: absent that explicit escape hatch, a missing report still fails the build, and the
6363
// uploads can never run without the verify guard's own condition.
64-
expect(String(verifyStep.if)).toBe("${{ success() && steps.coverage.outputs.no_tests_matched != 'true' }}");
65-
// EVERY upload below the verify step must carry the same escape: with zero matched tests there is no
66-
// lcov/junit at all, and the coverage uploads' fail_ci_if_error would turn that non-event red.
64+
expect(String(verifyStep.if)).toBe(
65+
"${{ success() && steps.coverage.outputs.no_tests_matched != 'true' && steps.coverage.outputs.no_src_coverage != 'true' }}",
66+
);
67+
// EVERY upload below the verify step must carry the same escapes: with zero matched tests there is no
68+
// lcov/junit at all, and the coverage uploads' fail_ci_if_error would turn that non-event red. The
69+
// second escape (#8194) is the scoped run that matched tests which PASSED but exercised nothing under
70+
// src/** -- an empty lcov there is a legitimate outcome, never a failed suite.
6771
expect(String(coverageUpload.if)).toContain("success()");
6872
expect(String(coverageUpload.if)).toContain("no_tests_matched != 'true'");
73+
expect(String(coverageUpload.if)).toContain("no_src_coverage != 'true'");
6974
expect(String(testResultsUpload.if)).toContain("no_tests_matched != 'true'");
75+
expect(String(testResultsUpload.if)).toContain("no_src_coverage != 'true'");
76+
// The scoped branch must derive BOTH outputs from the run itself (vitest's own stdout + exit status),
77+
// never from re-deriving selection logic -- pin the detector lines so they can't silently vanish.
78+
const coverageStep = steps[stepNames.indexOf("Test with coverage (shard ${{ matrix.shard }}/3)")]!;
79+
expect(String(coverageStep.run)).toContain('grep -q "No test files found" vitest-scoped-output.log');
80+
expect(String(coverageStep.run)).toContain('echo "no_src_coverage=true"');
7081
expect(String(verifyStep.run)).toContain("coverage/lcov.info is missing or empty");
7182
expect(String(verifyStep.run)).toContain("exit 1");
7283

0 commit comments

Comments
 (0)