Skip to content

Commit 3a7bc0e

Browse files
committed
feat: replace codecov with coverage badge
1 parent 469a9e1 commit 3a7bc0e

6 files changed

Lines changed: 116 additions & 3 deletions

File tree

.husky/pre-commit

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
npm run lint
22
npm run format
33
npm run typecheck
4-
npm run test:ci
4+
npm run coverage
5+
npm run coverage:badge
6+
git add README.md resources/coverage-badge.svg
57
npm run build

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Seamless Auth API
22

3-
[![codecov](https://codecov.io/gh/fells-code/seamless-auth-api/graph/badge.svg?token=6PQV8KVY30)](https://codecov.io/gh/fells-code/seamless-auth-api)
3+
![coverage](resources/coverage-badge.svg)
44
[![Publish Docker Image](https://github.com/fells-code/seamless-auth-api/actions/workflows/docker-publish.yml/badge.svg)](https://github.com/fells-code/seamless-auth-api/actions/workflows/docker-publish.yml)
55

66
**Seamless Auth API** is the open-source core authentication server for SeamlessAuth: an exclusively passwordless authentication system designed for modern web applications.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"test": "vitest",
1313
"test:run": "vitest run",
1414
"coverage": "vitest run --coverage",
15+
"coverage:badge": "node ./src/scripts/updateCoverageBadge.mjs",
1516
"test:ci": "CI=true vitest",
1617
"test:e2e": "CI=false vitest",
1718
"lint": "eslint . --ext .ts",

resources/coverage-badge.svg

Lines changed: 23 additions & 0 deletions
Loading
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import fs from 'node:fs';
2+
import path from 'node:path';
3+
4+
const repoRoot = process.cwd();
5+
const coverageSummaryPath = path.join(repoRoot, 'coverage', 'coverage-summary.json');
6+
const badgePath = path.join(repoRoot, 'resources', 'coverage-badge.svg');
7+
const readmePath = path.join(repoRoot, 'README.md');
8+
9+
if (!fs.existsSync(coverageSummaryPath)) {
10+
throw new Error(
11+
'coverage/coverage-summary.json was not found. Run `npm run coverage` before generating the badge.',
12+
);
13+
}
14+
15+
const summary = JSON.parse(fs.readFileSync(coverageSummaryPath, 'utf8'));
16+
const totalCoverage = summary?.total?.lines?.pct;
17+
18+
if (typeof totalCoverage !== 'number' || Number.isNaN(totalCoverage)) {
19+
throw new Error('Unable to read total line coverage from coverage/coverage-summary.json.');
20+
}
21+
22+
const normalizedCoverage = Number(totalCoverage.toFixed(1));
23+
const coverageLabel = `${normalizedCoverage}%`;
24+
const badgeColor = getCoverageColor(normalizedCoverage);
25+
const svg = buildBadgeSvg('coverage', coverageLabel, badgeColor);
26+
27+
fs.mkdirSync(path.dirname(badgePath), { recursive: true });
28+
fs.writeFileSync(badgePath, `${svg}\n`);
29+
30+
const readme = fs.readFileSync(readmePath, 'utf8');
31+
const badgeLine = `![coverage](resources/coverage-badge.svg)`;
32+
33+
if (
34+
!readme.includes('[![codecov](') &&
35+
!readme.includes('![coverage](resources/coverage-badge.svg)')
36+
) {
37+
throw new Error('README.md does not contain the expected coverage badge line.');
38+
}
39+
40+
const nextReadme = readme.replace(/\[!\[(?:codecov|coverage)\][^\n]+\n/, `${badgeLine}\n`);
41+
42+
fs.writeFileSync(readmePath, nextReadme);
43+
44+
function getCoverageColor(coverage) {
45+
if (coverage >= 90) return '#2ea043';
46+
if (coverage >= 80) return '#4c1';
47+
if (coverage >= 70) return '#97ca00';
48+
if (coverage >= 60) return '#dfb317';
49+
if (coverage >= 50) return '#fe7d37';
50+
return '#e05d44';
51+
}
52+
53+
function buildBadgeSvg(label, value, color) {
54+
const labelWidth = getTextWidth(label);
55+
const valueWidth = getTextWidth(value);
56+
const totalWidth = labelWidth + valueWidth;
57+
const valueX = labelWidth + valueWidth / 2;
58+
const labelX = labelWidth / 2;
59+
60+
return `<svg xmlns="http://www.w3.org/2000/svg" width="${totalWidth}" height="20" role="img" aria-label="${label}: ${value}">
61+
<title>${label}: ${value}</title>
62+
<linearGradient id="smooth" x2="0" y2="100%">
63+
<stop offset="0" stop-color="#fff" stop-opacity=".7"/>
64+
<stop offset=".1" stop-color="#aaa" stop-opacity=".1"/>
65+
<stop offset=".9" stop-color="#000" stop-opacity=".3"/>
66+
<stop offset="1" stop-color="#000" stop-opacity=".5"/>
67+
</linearGradient>
68+
<clipPath id="round">
69+
<rect width="${totalWidth}" height="20" rx="3" fill="#fff"/>
70+
</clipPath>
71+
<g clip-path="url(#round)">
72+
<rect width="${labelWidth}" height="20" fill="#555"/>
73+
<rect x="${labelWidth}" width="${valueWidth}" height="20" fill="${color}"/>
74+
<rect width="${totalWidth}" height="20" fill="url(#smooth)"/>
75+
</g>
76+
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">
77+
<text x="${labelX}" y="15" fill="#010101" fill-opacity=".3">${label}</text>
78+
<text x="${labelX}" y="14">${label}</text>
79+
<text x="${valueX}" y="15" fill="#010101" fill-opacity=".3">${value}</text>
80+
<text x="${valueX}" y="14">${value}</text>
81+
</g>
82+
</svg>`;
83+
}
84+
85+
function getTextWidth(text) {
86+
return text.length * 7 + 10;
87+
}

vitest.config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export default defineConfig({
1313
coverage: {
1414
provider: 'v8',
1515

16-
reporter: ['text', 'html', 'lcov'],
16+
reporter: ['text', 'html', 'lcov', 'json-summary'],
1717

1818
reportsDirectory: './coverage',
1919

0 commit comments

Comments
 (0)