Skip to content

Commit 9f4c729

Browse files
authored
ci(test): consolidate backend coverage job (#1830)
1 parent 798448f commit 9f4c729

2 files changed

Lines changed: 19 additions & 95 deletions

File tree

.github/workflows/ci.yml

Lines changed: 18 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -105,24 +105,17 @@ jobs:
105105
- name: Typecheck
106106
run: npm run typecheck
107107

108-
# Unit/integration suite + coverage, sharded across parallel runners to cut the
109-
# critical-path wall-clock. Each shard publishes its PARTIAL lcov as a build
110-
# artifact; the `coverage-upload` job merges all 3 and uploads to Codecov ONCE,
111-
# so Codecov computes codecov/patch a single time on the complete report instead
112-
# of flapping FAILURE->SUCCESS as each shard's partial upload arrived. vitest's
113-
# local 90% backstop is disabled here (COVERAGE_NO_THRESHOLDS) because a single
114-
# shard only exercises part of the tree; the backstop still runs on the full
115-
# local `npm run test:coverage`.
108+
# Unit/integration suite + coverage. This used to fan out into 3 GitHub matrix
109+
# jobs because hosted-runner minutes were the bottleneck. On the self-hosted
110+
# runner, the extra checkout/install/artifact fan-in surface has become more
111+
# expensive and fragile than the saved wall-clock, so keep coverage in one job
112+
# and let Vitest use the runner's local worker concurrency.
116113
test:
117114
name: test
118115
needs: changes
119116
if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }}
120117
runs-on: ${{ fromJSON((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) && '["ubuntu-latest"]' || '["self-hosted","gittensory"]') }}
121118
timeout-minutes: 15
122-
strategy:
123-
fail-fast: false
124-
matrix:
125-
shard: [1, 2, 3]
126119
steps:
127120
- name: Checkout
128121
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
@@ -148,104 +141,35 @@ jobs:
148141
exit 1
149142
- name: Prepare test reports dir
150143
run: mkdir -p reports/junit
151-
- name: Test with coverage (shard ${{ matrix.shard }}/3)
144+
- name: Test with coverage
152145
id: coverage
153146
env:
154-
COVERAGE_NO_THRESHOLDS: "1"
155-
VITEST_JUNIT_PATH: reports/junit/vitest-shard-${{ matrix.shard }}.xml
156-
run: npm run test:coverage -- --shard=${{ matrix.shard }}/3
147+
VITEST_JUNIT_PATH: reports/junit/vitest.xml
148+
run: npm run test:coverage -- --maxWorkers=100%
157149
- name: Test failure guidance
158150
if: ${{ failure() && steps.coverage.conclusion == 'failure' }}
159151
run: |
160-
echo "::error title=Tests::A test in shard ${{ matrix.shard }}/3 failed."
161-
echo "Coverage itself is gated by Codecov on changed lines (codecov/patch), computed from the merged shards."
162-
echo "Reproduce locally with the full suite: 'npm run test:coverage' (no sharding)."
163-
- name: Upload partial coverage artifact (shard ${{ matrix.shard }}/3)
164-
# A passing shard writes coverage/lcov.info; a FAILING shard writes none
165-
# (vitest coverage.reportOnFailure defaults to false). Gate on success() so a
166-
# red shard skips this (and fails the job → fails `validate`), and use
167-
# if-no-files-found:error so a green shard always ships exactly one lcov and
168-
# the merge job can assert all 3 arrived. The single Codecov upload happens
169-
# in the coverage-upload job below.
152+
echo "::error title=Tests::The backend test coverage suite failed."
153+
echo "Coverage itself is gated by Codecov on changed lines (codecov/patch), computed from the complete lcov generated by this job."
154+
echo "Reproduce locally with: 'npm run test:coverage'."
155+
- name: Upload coverage to Codecov
170156
if: ${{ success() }}
171-
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
172-
with:
173-
name: coverage-lcov-shard-${{ matrix.shard }}
174-
path: ./coverage/lcov.info
175-
if-no-files-found: error
176-
retention-days: 1
177-
- name: Upload Vitest results to Codecov
178-
if: ${{ !cancelled() }}
179157
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
180158
with:
181159
token: ${{ secrets.CODECOV_TOKEN }}
182-
files: ./reports/junit/vitest-shard-${{ matrix.shard }}.xml
183-
report_type: test_results
160+
files: ./coverage/lcov.info
184161
disable_search: true
185162
override_branch: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref_name }}
186163
override_commit: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
187164
override_pr: ${{ github.event_name == 'pull_request' && github.event.pull_request.number || '' }}
188165
fail_ci_if_error: false
189-
190-
# Merge the 3 shard lcovs into one complete report and upload to Codecov ONCE.
191-
# The shards no longer upload coverage directly: that made Codecov score
192-
# codecov/patch on a partially-merged subset of shards as each upload arrived, so
193-
# the status flapped FAILURE->SUCCESS until all 3 landed. A single upload of the
194-
# merged report means Codecov computes patch exactly once on complete data. This
195-
# job inherits `test`'s path filter and only runs when ALL shards passed, so a
196-
# non-backend PR (which skips `test`) emits no codecov/patch context at all — and
197-
# codecov/patch is intentionally NOT a required branch-protection context, so an
198-
# absent status never strands the review gate at pending.
199-
coverage-upload:
200-
name: coverage-upload
201-
needs: [changes, test]
202-
if: ${{ (github.event_name == 'push' || needs.changes.outputs.backend == 'true') && needs.test.result == 'success' }}
203-
runs-on: ${{ fromJSON((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) && '["ubuntu-latest"]' || '["self-hosted","gittensory"]') }}
204-
timeout-minutes: 10
205-
steps:
206-
- name: Checkout
207-
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
208-
with:
209-
# Full history so Codecov can resolve the merge base for patch coverage.
210-
fetch-depth: 0
211-
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
212-
- name: Download shard coverage artifacts
213-
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
214-
with:
215-
pattern: coverage-lcov-shard-*
216-
path: shard-coverage
217-
- name: Collect the shard lcovs into stable paths
218-
# download-artifact nests each artifact under its own directory, and
219-
# upload-artifact's internal layout for a single file is version-dependent, so
220-
# find the lcovs wherever they landed and copy them to fixed names for an
221-
# explicit Codecov upload. Assert exactly 3 — fewer means an upload/runner
222-
# anomaly (needs.test.result already gated this job to all-shards-green), and a
223-
# hard CI failure (one-shot auto-close, recoverable) beats silently scoring
224-
# codecov/patch on a subset.
225-
run: |
226-
mkdir -p merged-lcov
227-
n=0
228-
for f in $(find shard-coverage -name lcov.info | sort); do
229-
n=$((n + 1))
230-
cp "$f" "merged-lcov/shard-${n}.info"
231-
done
232-
echo "Collected ${n} shard lcov file(s)."
233-
if [ "${n}" -ne 3 ]; then
234-
echo "::error title=Coverage::Expected 3 shard lcov files, found ${n}."
235-
exit 1
236-
fi
237-
- name: Upload all shard coverage to Codecov in one report
238-
# All 3 partial lcovs in a SINGLE codecov-action invocation = ONE upload;
239-
# Codecov merges them server-side before computing codecov/patch (it "always
240-
# merges report data" and holds notifications until merge completes), so the
241-
# status is computed once on the complete report — eliminating the partial
242-
# flap that 3 separate per-shard uploads caused. fail_ci_if_error stays false
243-
# so a Codecov outage cannot fail CI / auto-close an honest PR (codecov/patch
244-
# is simply absent, which the gate treats as not-a-FAILURE).
166+
- name: Upload Vitest results to Codecov
167+
if: ${{ !cancelled() }}
245168
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
246169
with:
247170
token: ${{ secrets.CODECOV_TOKEN }}
248-
files: ./merged-lcov/shard-1.info,./merged-lcov/shard-2.info,./merged-lcov/shard-3.info
171+
files: ./reports/junit/vitest.xml
172+
report_type: test_results
249173
disable_search: true
250174
override_branch: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref_name }}
251175
override_commit: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
@@ -406,7 +330,7 @@ jobs:
406330
# Path-filtered jobs report "skipped", which is treated as success.
407331
validate:
408332
name: validate
409-
needs: [changes, lint, test, coverage-upload, workers, mcp, rees, ui, security]
333+
needs: [changes, lint, test, workers, mcp, rees, ui, security]
410334
if: ${{ always() }}
411335
runs-on: ${{ fromJSON((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) && '["ubuntu-latest"]' || '["self-hosted","gittensory"]') }}
412336
timeout-minutes: 2

test/unit/workflow-runner-labels.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ describe("workflow runner labels", () => {
99
const trustedRunnerExpression =
1010
'${{ fromJSON((github.event_name == \'pull_request\' && github.event.pull_request.head.repo.fork == true) && \'["ubuntu-latest"]\' || \'["self-hosted","gittensory"]\') }}';
1111

12-
expect(workflow.match(new RegExp(escapeRegExp(trustedRunnerExpression), "g")) ?? []).toHaveLength(10);
12+
expect(workflow.match(new RegExp(escapeRegExp(trustedRunnerExpression), "g")) ?? []).toHaveLength(9);
1313
expect(workflow).not.toContain("|| 'self-hosted'");
1414
expect(workflow).not.toContain('"fork-ci"');
1515
});

0 commit comments

Comments
 (0)