Skip to content

Commit 6711cdf

Browse files
authored
Merge pull request #32 from retailnext/copilot/update-ci-test-coverage-badge
Add CI validation for coverage badge accuracy
2 parents f6d8b47 + 9e2afac commit 6711cdf

4 files changed

Lines changed: 133 additions & 2 deletions

File tree

.github/copilot-instructions.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,40 @@ command to bundle the TypeScript code into JavaScript:
6565
npm run bundle
6666
```
6767

68+
## Coverage Badge
69+
70+
**CRITICAL: Before making ANY commit that includes code changes (changes to
71+
files in the `src` directory), you MUST ensure the coverage badge is up to date.
72+
This is mandatory for EVERY commit that includes code changes.**
73+
74+
The coverage badge in `badges/coverage.svg` must accurately reflect the current
75+
test coverage. The CI workflow includes a check that will fail if the coverage
76+
badge is out of date, making the PR unmergeable.
77+
78+
### Updating the Coverage Badge
79+
80+
After making code changes and running tests:
81+
82+
1. Run `npm run coverage` to update the coverage badge
83+
1. Verify the badge is correct by running
84+
`npx tsx script/check-coverage-badge.ts`
85+
1. Commit the updated badge along with your code changes
86+
87+
### Checking Coverage Badge Accuracy
88+
89+
To verify the coverage badge is up to date:
90+
91+
```bash
92+
npx tsx script/check-coverage-badge.ts
93+
```
94+
95+
This script compares the coverage percentage in `badges/coverage.svg` with the
96+
actual coverage from `coverage/coverage-summary.json`. If they don't match, the
97+
script will fail with an error message.
98+
99+
The `npm run ci-test` script automatically runs this check, so PRs with
100+
inaccurate coverage badges will fail CI and be unmergeable.
101+
68102
## General Coding Guidelines
69103

70104
- Follow standard TypeScript and JavaScript coding conventions and best

badges/coverage.svg

Lines changed: 1 addition & 1 deletion
Loading

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
},
2525
"scripts": {
2626
"bundle": "npm run format:write && npm run package",
27-
"ci-test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest",
27+
"ci-test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest && npx tsx script/check-coverage-badge.ts",
2828
"coverage": "npx make-coverage-badge --output-path ./badges/coverage.svg",
2929
"format:write": "npx prettier --write .",
3030
"format:check": "npx prettier --check .",

script/check-coverage-badge.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/**
2+
* Utility script to verify that the coverage badge is up to date
3+
* This compares the coverage percentage in the badge SVG with the actual coverage from Jest
4+
*/
5+
6+
import { readFileSync, existsSync } from 'fs'
7+
import { dirname, resolve } from 'path'
8+
import { fileURLToPath } from 'url'
9+
10+
/**
11+
* Extracts the coverage percentage from the coverage-summary.json file
12+
* @returns The statement coverage percentage as a number
13+
* @throws Error if coverage file cannot be found or parsed
14+
*/
15+
export function getCoverageFromSummary(): number {
16+
const scriptDir = dirname(fileURLToPath(import.meta.url))
17+
const coveragePath = resolve(scriptDir, '../coverage/coverage-summary.json')
18+
19+
if (!existsSync(coveragePath)) {
20+
throw new Error(
21+
'Coverage summary not found. Run "npm run test" to generate coverage data.'
22+
)
23+
}
24+
25+
const coverageData = JSON.parse(readFileSync(coveragePath, 'utf-8'))
26+
27+
if (!coverageData.total || !coverageData.total.statements) {
28+
throw new Error('Invalid coverage summary format')
29+
}
30+
31+
return coverageData.total.statements.pct
32+
}
33+
34+
/**
35+
* Extracts the coverage percentage from the coverage badge SVG file
36+
* @returns The coverage percentage as a number
37+
* @throws Error if badge file cannot be found or parsed
38+
*/
39+
export function getCoverageFromBadge(): number {
40+
const scriptDir = dirname(fileURLToPath(import.meta.url))
41+
const badgePath = resolve(scriptDir, '../badges/coverage.svg')
42+
43+
if (!existsSync(badgePath)) {
44+
throw new Error(
45+
'Coverage badge not found. Run "npm run coverage" to generate the badge.'
46+
)
47+
}
48+
49+
const badgeContent = readFileSync(badgePath, 'utf-8')
50+
51+
// Extract the coverage percentage from the badge SVG
52+
// The format is: <text ...>XX.XX%</text>
53+
const match = badgeContent.match(/(\d+\.?\d*)%<\/text>/)
54+
55+
if (!match) {
56+
throw new Error('Could not extract coverage percentage from badge')
57+
}
58+
59+
return parseFloat(match[1])
60+
}
61+
62+
/**
63+
* Validates that the coverage badge matches the actual coverage
64+
* @throws Error if badge is out of date
65+
*/
66+
export function validateCoverageBadge(): void {
67+
const actualCoverage = getCoverageFromSummary()
68+
const badgeCoverage = getCoverageFromBadge()
69+
70+
console.log(`Actual coverage: ${actualCoverage}%`)
71+
console.log(`Badge coverage: ${badgeCoverage}%`)
72+
73+
if (actualCoverage !== badgeCoverage) {
74+
throw new Error(
75+
`Coverage badge is out of date!\n` +
76+
` Actual coverage: ${actualCoverage}%\n` +
77+
` Badge coverage: ${badgeCoverage}%\n` +
78+
` Run "npm run coverage" to update the badge.`
79+
)
80+
}
81+
82+
console.log('✓ Coverage badge is up to date')
83+
}
84+
85+
// If run directly as a script, validate the badge
86+
const isMainModule =
87+
fileURLToPath(import.meta.url) === process.argv[1] ||
88+
fileURLToPath(import.meta.url) === resolve(process.argv[1])
89+
90+
if (isMainModule) {
91+
try {
92+
validateCoverageBadge()
93+
} catch (error) {
94+
console.error(error instanceof Error ? error.message : error)
95+
process.exit(1)
96+
}
97+
}

0 commit comments

Comments
 (0)