Skip to content

Commit 53de0b0

Browse files
committed
Fix Codecov frontend badge by aggregating flags server-side
The merged 'frontend' flag the README badge reads has been erroring on every commit since PR #1322. Root cause: lcov-result-merger@5 strips line/function summary fields (LF, LH, FN, FNDA, FNF, FNH, BRF, BRH) from the merged output, and Vitest v8's relative paths don't dedupe against Istanbul's absolute paths, so the file Codecov receives can't be parsed and lands as state=error. Drop the client-side merge: tag each per-suite upload with 'frontend' as an additional flag so Codecov aggregates the three uploads server-side. Reduce codecov-notify.yml back to its original gate-and- notify role (remove the artifact downloads, the lcov-result-merger invocation, and the merged upload step), delete the per-suite lcov artifact publishes in frontend.yml and frontend-e2e.yml, and prune lcov-result-merger plus its orphan yargs transitive from the frontend lockfile.
1 parent 6d65afa commit 53de0b0

6 files changed

Lines changed: 23 additions & 175 deletions

File tree

.github/workflows/codecov-notify.yml

Lines changed: 5 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,11 @@ name: Codecov Notify
88
# the triggering SHA. Workflows that skipped due to path filters don't block
99
# the notify step.
1010
#
11-
# This workflow also merges the three per-suite frontend lcov reports
12-
# (`frontend-unit-lcov`, `frontend-ct-lcov`, `frontend-e2e-lcov`, each
13-
# published as a GitHub Actions artifact by its producing job) into a
14-
# single lcov and uploads it to Codecov under the `frontend` flag. That
15-
# gives one authoritative Frontend coverage number — the README badge
16-
# reads it, and PR comments show the merged total alongside the per-suite
17-
# flags. The three individual flag uploads still happen in their own
18-
# workflows, so drill-in by suite continues to work.
11+
# The merged `frontend` flag that the README badge reads is produced
12+
# server-side: each per-suite frontend upload (`frontend-unit`,
13+
# `frontend-component`, `frontend-e2e`) is tagged with `frontend` as an
14+
# additional flag in its own workflow, so Codecov aggregates the union
15+
# of all three without any cross-workflow lcov merging here.
1916

2017
on:
2118
workflow_run:
@@ -60,7 +57,6 @@ jobs:
6057
);
6158
6259
let allDone = true;
63-
const runIds = {};
6460
for (const name of expected) {
6561
const matching = runs
6662
.filter((r) => r.name === name)
@@ -77,102 +73,13 @@ jobs:
7773
core.info(
7874
`${name}: status=${latest.status} conclusion=${latest.conclusion} id=${latest.id}`
7975
);
80-
runIds[name] = latest.id;
8176
if (latest.status !== "completed") {
8277
allDone = false;
8378
}
8479
}
8580
8681
core.setOutput("all_done", allDone ? "true" : "false");
8782
core.setOutput("sha", sha);
88-
core.setOutput(
89-
"frontend_ci_run_id",
90-
runIds["Frontend CI"] || ""
91-
);
92-
core.setOutput(
93-
"frontend_e2e_run_id",
94-
runIds["Frontend E2E Integration"] || ""
95-
);
96-
97-
# ────────────────────────────────────────────────────────────────
98-
# Download per-suite lcov artifacts from each producing workflow.
99-
#
100-
# Each download is guarded by the producing workflow's run id (empty
101-
# string when that workflow was path-filtered for this SHA), and
102-
# `continue-on-error: true` tolerates a completed run that did not
103-
# publish the artifact (for example, when the suite failed before
104-
# the upload step ran).
105-
# ────────────────────────────────────────────────────────────────
106-
- name: Download unit lcov artifact
107-
if: steps.check.outputs.all_done == 'true' && steps.check.outputs.frontend_ci_run_id != ''
108-
continue-on-error: true
109-
uses: actions/download-artifact@v7
110-
with:
111-
github-token: ${{ secrets.GITHUB_TOKEN }}
112-
run-id: ${{ steps.check.outputs.frontend_ci_run_id }}
113-
name: frontend-unit-lcov
114-
path: coverage/unit
115-
116-
- name: Download component lcov artifact
117-
if: steps.check.outputs.all_done == 'true' && steps.check.outputs.frontend_ci_run_id != ''
118-
continue-on-error: true
119-
uses: actions/download-artifact@v7
120-
with:
121-
github-token: ${{ secrets.GITHUB_TOKEN }}
122-
run-id: ${{ steps.check.outputs.frontend_ci_run_id }}
123-
name: frontend-ct-lcov
124-
path: coverage/ct
125-
126-
- name: Download e2e lcov artifact
127-
if: steps.check.outputs.all_done == 'true' && steps.check.outputs.frontend_e2e_run_id != ''
128-
continue-on-error: true
129-
uses: actions/download-artifact@v7
130-
with:
131-
github-token: ${{ secrets.GITHUB_TOKEN }}
132-
run-id: ${{ steps.check.outputs.frontend_e2e_run_id }}
133-
name: frontend-e2e-lcov
134-
path: coverage/e2e
135-
136-
- name: Check for downloaded lcov files
137-
id: lcov
138-
if: steps.check.outputs.all_done == 'true'
139-
run: |
140-
shopt -s nullglob
141-
files=(coverage/*/lcov.info)
142-
if [ ${#files[@]} -eq 0 ]; then
143-
echo "No lcov artifacts retrieved; skipping merged frontend upload."
144-
echo "has_lcov=false" >> "$GITHUB_OUTPUT"
145-
else
146-
echo "Found ${#files[@]} lcov file(s):"
147-
printf ' - %s\n' "${files[@]}"
148-
echo "has_lcov=true" >> "$GITHUB_OUTPUT"
149-
fi
150-
151-
- name: Setup Node.js
152-
if: steps.lcov.outputs.has_lcov == 'true'
153-
uses: actions/setup-node@v6
154-
with:
155-
node-version: "20"
156-
157-
- name: Merge frontend lcov reports
158-
if: steps.lcov.outputs.has_lcov == 'true'
159-
run: |
160-
mkdir -p coverage/merged
161-
npx --yes lcov-result-merger@5 'coverage/*/lcov.info' coverage/merged/lcov.info
162-
echo "Merged lcov source-file count:"
163-
grep -c "^SF:" coverage/merged/lcov.info || true
164-
165-
- name: Upload merged frontend coverage to Codecov
166-
if: steps.lcov.outputs.has_lcov == 'true'
167-
uses: codecov/codecov-action@v6
168-
with:
169-
token: ${{ secrets.CODECOV_TOKEN }}
170-
files: coverage/merged/lcov.info
171-
flags: frontend
172-
name: frontend-merged-coverage
173-
fail_ci_if_error: false
174-
disable_search: true
175-
override_commit: ${{ steps.check.outputs.sha }}
17683
17784
- name: Send notifications to Codecov
17885
if: steps.check.outputs.all_done == 'true'

.github/workflows/frontend-e2e.yml

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -208,29 +208,20 @@ jobs:
208208
path: frontend/playwright-report-e2e/
209209
if-no-files-found: ignore
210210

211+
# The `frontend` flag rides along so the same upload feeds both the
212+
# per-suite drill-in and the merged Frontend coverage total that the
213+
# README badge reads. See `frontend.yml` for the companion uploads.
211214
- name: Upload frontend E2E coverage to Codecov
212215
if: success() || failure()
213216
uses: codecov/codecov-action@v6
214217
with:
215218
token: ${{ secrets.CODECOV_TOKEN }}
216219
files: frontend/coverage/e2e/lcov.info
217-
flags: frontend-e2e
220+
flags: frontend-e2e,frontend
218221
name: frontend-e2e-coverage
219222
fail_ci_if_error: false
220223
disable_search: true
221224

222-
# Publish the raw lcov so the cross-workflow merge job in
223-
# `codecov-notify.yml` can pull it via `actions/download-artifact` and
224-
# combine it with the unit + component lcovs into a single `frontend` flag.
225-
- name: Upload frontend E2E lcov artifact
226-
if: success() || failure()
227-
uses: actions/upload-artifact@v7
228-
with:
229-
name: frontend-e2e-lcov
230-
path: frontend/coverage/e2e/lcov.info
231-
if-no-files-found: warn
232-
retention-days: 7
233-
234225
- name: Upload backend E2E coverage to Codecov
235226
if: success() || failure()
236227
uses: codecov/codecov-action@v6

.github/workflows/frontend.yml

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -79,28 +79,22 @@ jobs:
7979
yarn run test:ct --reporter=list tests/*Metadata*.ct.tsx
8080
continue-on-error: true
8181

82+
# The `frontend` flag is intentionally listed alongside
83+
# `frontend-component` so the same upload feeds both the per-suite
84+
# drill-in and the merged Frontend coverage total. Codecov aggregates
85+
# all uploads sharing a flag server-side, so the README `flag=frontend`
86+
# badge reflects the union of unit + component + e2e without any
87+
# cross-workflow lcov merging on our side.
8288
- name: Upload CT Coverage to Codecov
8389
if: success() || failure()
8490
uses: codecov/codecov-action@v6
8591
with:
8692
token: ${{ secrets.CODECOV_TOKEN }}
8793
files: frontend/coverage/ct/lcov.info
88-
flags: frontend-component
94+
flags: frontend-component,frontend
8995
name: frontend-ct-coverage
9096
fail_ci_if_error: false
9197

92-
# Publish the raw lcov so the cross-workflow merge job in
93-
# `codecov-notify.yml` can pull it via `actions/download-artifact` and
94-
# combine it with the unit + e2e lcovs into a single `frontend` flag.
95-
- name: Upload CT lcov artifact
96-
if: success() || failure()
97-
uses: actions/upload-artifact@v7
98-
with:
99-
name: frontend-ct-lcov
100-
path: frontend/coverage/ct/lcov.info
101-
if-no-files-found: warn
102-
retention-days: 7
103-
10498
build:
10599
name: Build
106100
runs-on: ubuntu-latest
@@ -144,28 +138,17 @@ jobs:
144138
run: yarn install --frozen-lockfile
145139
- name: Run Unit Tests with Coverage
146140
run: yarn run test:coverage:unit
141+
# See the CT upload above for why the `frontend` flag is listed here too.
147142
- name: Upload Unit Coverage to Codecov
148143
if: success() || failure()
149144
uses: codecov/codecov-action@v6
150145
with:
151146
token: ${{ secrets.CODECOV_TOKEN }}
152147
files: frontend/coverage/unit/lcov.info
153-
flags: frontend-unit
148+
flags: frontend-unit,frontend
154149
name: frontend-unit-coverage
155150
fail_ci_if_error: false
156151

157-
# Publish the raw lcov so the cross-workflow merge job in
158-
# `codecov-notify.yml` can pull it via `actions/download-artifact` and
159-
# combine it with the component + e2e lcovs into a single `frontend` flag.
160-
- name: Upload Unit lcov artifact
161-
if: success() || failure()
162-
uses: actions/upload-artifact@v7
163-
with:
164-
name: frontend-unit-lcov
165-
path: frontend/coverage/unit/lcov.info
166-
if-no-files-found: warn
167-
retention-days: 7
168-
169152
docker-build:
170153
name: Build Docker Image
171154
runs-on: ubuntu-latest

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- **Frontend coverage badge stuck on "unknown"** (`README.md:12`, `.github/workflows/codecov-notify.yml`, `.github/workflows/frontend.yml`, `.github/workflows/frontend-e2e.yml`, `frontend/package.json`, `frontend/yarn.lock`): PR #1322 pointed the README badge at a new merged `frontend` flag fed by a cross-workflow `lcov-result-merger@5` step inside `codecov-notify.yml`. Every `frontend-merged-coverage` upload since has landed at Codecov with `state: error` / `totals: null`, so the badge rendered "unknown" even though `frontend-unit` (31%), `frontend-component` (61%), and `frontend-e2e` (24%) were all processing correctly. Two defects in the merged lcov confirmed by local repro of the CI merge step: (1) `lcov-result-merger@5` emits a stripped lcov containing only `SF:`, `DA:`, `BRDA:`, `end_of_record` — it drops `TN:`, `FN`, `FNDA`, `FNF`, `FNH`, `LF`, `LH`, `BRF`, `BRH`, so line-summary fields required by Codecov's parser are absent; (2) Vitest v8 emits `src/...` (relative to `frontend/`) while `vite-plugin-istanbul` + `nyc report` emit `/home/runner/work/OpenContracts/OpenContracts/frontend/src/...` (absolute), and the merger keys on the literal path string so the same file appears as two records with conflicting hit counts. `codecov-notify.yml` also ran without `actions/checkout`, which Codecov's action docs explicitly recommend against. Fix: stop merging client-side and let Codecov aggregate server-side, since that is what flags are for. Each per-suite upload now declares two flags — `frontend-unit,frontend`, `frontend-component,frontend`, `frontend-e2e,frontend` — so the `frontend` flag total is the union of the three uploads computed by Codecov. `codecov-notify.yml` is reduced to its original gate-and-notify role (no artifact downloads, no `lcov-result-merger`, no merged upload). Deleted the `frontend-{unit,ct,e2e}-lcov` artifact publishes in `frontend.yml` / `frontend-e2e.yml`, removed the `lcov-result-merger@^5.0.1` devDep and the `coverage:merge` script from `frontend/package.json`, and pruned the orphaned entries from `frontend/yarn.lock`. README badge URL unchanged (`flag=frontend`).
13+
1014
### Changed
1115

1216
- **PR #1297 follow-up — tighten component tests and unit-test coverage** (Issue #1321):

frontend/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,6 @@
101101
"test:e2e:coverage": "COVERAGE=true playwright test; E2E_EXIT=$?; mkdir -p coverage/e2e/.nyc_output && nyc report --all --reporter=lcov --reporter=text --report-dir=coverage/e2e --temp-dir=coverage/e2e/.nyc_output || echo 'No coverage data to report'; exit $E2E_EXIT",
102102
"test:coverage:unit": "vitest run --coverage --watch=false",
103103
"test:coverage:ct": "COVERAGE=true playwright test -c playwright-ct.config.ts --reporter=list && mkdir -p coverage/ct/.nyc_output && nyc report --all --reporter=lcov --reporter=text --report-dir=coverage/ct --temp-dir=coverage/ct/.nyc_output",
104-
"coverage:merge": "mkdir -p coverage/merged && lcov-result-merger 'coverage/*/lcov.info' coverage/merged/lcov.info",
105104
"lint": "prettier . --check --ignore-unknown",
106105
"fix-styles": "prettier . --check --write --ignore-unknown",
107106
"prepare": "cd .. && husky install frontend/.husky",
@@ -156,7 +155,6 @@
156155
"fake-indexeddb": "^6.2.2",
157156
"husky": "^8.0.1",
158157
"jsdom": "^26.1.0",
159-
"lcov-result-merger": "^5.0.1",
160158
"nyc": "^18.0.0",
161159
"prettier": "^2.7.1",
162160
"vite-plugin-istanbul": "^8.0.0",

frontend/yarn.lock

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3479,15 +3479,6 @@ cliui@^6.0.0:
34793479
strip-ansi "^6.0.0"
34803480
wrap-ansi "^6.2.0"
34813481

3482-
cliui@^7.0.2:
3483-
version "7.0.4"
3484-
resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
3485-
integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
3486-
dependencies:
3487-
string-width "^4.2.0"
3488-
strip-ansi "^6.0.0"
3489-
wrap-ansi "^7.0.0"
3490-
34913482
cliui@^8.0.1:
34923483
version "8.0.1"
34933484
resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa"
@@ -4390,7 +4381,7 @@ fast-deep-equal@^3.1.3:
43904381
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
43914382
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
43924383

4393-
fast-glob@^3.2.11, fast-glob@^3.2.9:
4384+
fast-glob@^3.2.9:
43944385
version "3.3.3"
43954386
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818"
43964387
integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
@@ -5512,14 +5503,6 @@ jsonpointer@^5.0.1:
55125503
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559"
55135504
integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==
55145505

5515-
lcov-result-merger@^5.0.1:
5516-
version "5.0.1"
5517-
resolved "https://registry.yarnpkg.com/lcov-result-merger/-/lcov-result-merger-5.0.1.tgz#39e3cfef3f84dc1a73736c139e735b58fbb2eeed"
5518-
integrity sha512-i53RjTYfqbHgerqGtuJjDfARDU340zNxXrJudQZU3o8ak9rrx8FDQUKf38Cjm6MtbqonqiDFmoKuUe++uZbvOg==
5519-
dependencies:
5520-
fast-glob "^3.2.11"
5521-
yargs "^16.2.0"
5522-
55235506
lines-and-columns@^1.1.6:
55245507
version "1.2.4"
55255508
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
@@ -8663,11 +8646,6 @@ yargs-parser@^18.1.2:
86638646
camelcase "^5.0.0"
86648647
decamelize "^1.2.0"
86658648

8666-
yargs-parser@^20.2.2:
8667-
version "20.2.9"
8668-
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
8669-
integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
8670-
86718649
yargs-parser@^21.1.1:
86728650
version "21.1.1"
86738651
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
@@ -8690,19 +8668,6 @@ yargs@^15.0.2:
86908668
y18n "^4.0.0"
86918669
yargs-parser "^18.1.2"
86928670

8693-
yargs@^16.2.0:
8694-
version "16.2.0"
8695-
resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
8696-
integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
8697-
dependencies:
8698-
cliui "^7.0.2"
8699-
escalade "^3.1.1"
8700-
get-caller-file "^2.0.5"
8701-
require-directory "^2.1.1"
8702-
string-width "^4.2.0"
8703-
y18n "^5.0.5"
8704-
yargs-parser "^20.2.2"
8705-
87068671
yargs@^17.0.0:
87078672
version "17.7.2"
87088673
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"

0 commit comments

Comments
 (0)