Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,40 @@ command to bundle the TypeScript code into JavaScript:
npm run bundle
```

## Coverage Badge

**CRITICAL: Before making ANY commit that includes code changes (changes to
files in the `src` directory), you MUST ensure the coverage badge is up to date.
This is mandatory for EVERY commit that includes code changes.**

The coverage badge in `badges/coverage.svg` must accurately reflect the current
test coverage. The CI workflow includes a check that will fail if the coverage
badge is out of date, making the PR unmergeable.

### Updating the Coverage Badge

After making code changes and running tests:

1. Run `npm run coverage` to update the coverage badge
1. Verify the badge is correct by running
`npx tsx script/check-coverage-badge.ts`
1. Commit the updated badge along with your code changes

### Checking Coverage Badge Accuracy

To verify the coverage badge is up to date:

```bash
npx tsx script/check-coverage-badge.ts
```

This script compares the coverage percentage in `badges/coverage.svg` with the
actual coverage from `coverage/coverage-summary.json`. If they don't match, the
script will fail with an error message.

The `npm run ci-test` script automatically runs this check, so PRs with
inaccurate coverage badges will fail CI and be unmergeable.

## General Coding Guidelines

- Follow standard TypeScript and JavaScript coding conventions and best
Expand Down
2 changes: 1 addition & 1 deletion badges/coverage.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
},
"scripts": {
"bundle": "npm run format:write && npm run package",
"ci-test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest",
"ci-test": "NODE_OPTIONS=--experimental-vm-modules NODE_NO_WARNINGS=1 npx jest && npx tsx script/check-coverage-badge.ts",
"coverage": "npx make-coverage-badge --output-path ./badges/coverage.svg",
"format:write": "npx prettier --write .",
"format:check": "npx prettier --check .",
Expand Down
97 changes: 97 additions & 0 deletions script/check-coverage-badge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* Utility script to verify that the coverage badge is up to date
* This compares the coverage percentage in the badge SVG with the actual coverage from Jest
*/

import { readFileSync, existsSync } from 'fs'
import { dirname, resolve } from 'path'
import { fileURLToPath } from 'url'

/**
* Extracts the coverage percentage from the coverage-summary.json file
* @returns The statement coverage percentage as a number
* @throws Error if coverage file cannot be found or parsed
*/
export function getCoverageFromSummary(): number {
const scriptDir = dirname(fileURLToPath(import.meta.url))
const coveragePath = resolve(scriptDir, '../coverage/coverage-summary.json')

if (!existsSync(coveragePath)) {
throw new Error(
'Coverage summary not found. Run "npm run test" to generate coverage data.'
)
}

const coverageData = JSON.parse(readFileSync(coveragePath, 'utf-8'))

if (!coverageData.total || !coverageData.total.statements) {
throw new Error('Invalid coverage summary format')
}

return coverageData.total.statements.pct
}

/**
* Extracts the coverage percentage from the coverage badge SVG file
* @returns The coverage percentage as a number
* @throws Error if badge file cannot be found or parsed
*/
export function getCoverageFromBadge(): number {
const scriptDir = dirname(fileURLToPath(import.meta.url))
const badgePath = resolve(scriptDir, '../badges/coverage.svg')

if (!existsSync(badgePath)) {
throw new Error(
'Coverage badge not found. Run "npm run coverage" to generate the badge.'
)
}

const badgeContent = readFileSync(badgePath, 'utf-8')

// Extract the coverage percentage from the badge SVG
// The format is: <text ...>XX.XX%</text>
const match = badgeContent.match(/(\d+\.?\d*)%<\/text>/)

if (!match) {
throw new Error('Could not extract coverage percentage from badge')
}

return parseFloat(match[1])
}

/**
* Validates that the coverage badge matches the actual coverage
* @throws Error if badge is out of date
*/
export function validateCoverageBadge(): void {
const actualCoverage = getCoverageFromSummary()
const badgeCoverage = getCoverageFromBadge()

console.log(`Actual coverage: ${actualCoverage}%`)
console.log(`Badge coverage: ${badgeCoverage}%`)

if (actualCoverage !== badgeCoverage) {
throw new Error(
`Coverage badge is out of date!\n` +
` Actual coverage: ${actualCoverage}%\n` +
` Badge coverage: ${badgeCoverage}%\n` +
` Run "npm run coverage" to update the badge.`
)
}

console.log('✓ Coverage badge is up to date')
}

// If run directly as a script, validate the badge
const isMainModule =
fileURLToPath(import.meta.url) === process.argv[1] ||
fileURLToPath(import.meta.url) === resolve(process.argv[1])

if (isMainModule) {
try {
validateCoverageBadge()
} catch (error) {
console.error(error instanceof Error ? error.message : error)
process.exit(1)
}
}
Loading