mirror of
https://github.com/grafana/grafana.git
synced 2026-02-03 20:49:50 -05:00
* chore(git): ignore coverage summary from CI workflow * feat(script): check if files changed for a codeowner * feat(ci): clean up PR comments when break glass label applied * feat(ci): add change detection to coverage workflow * feat(ci): conditionally run coverage checks only for teams changed files * feat(ci): compare code coverage reports by codeowner * feat(ci): improve PR comment message * feat(ci): add skip warning with opted-in teams list * fix(scripts): avoid rounding errors in test coverage summaries * fix(script): fix GHA linting errors * fix(script): multiple space delimited filenames in change detection * fix(scripts): round to two decimal points of precision * feat(script): collector job to fan-in coverage comparison matrix * fix(script): display correct git SHA * fix(script): serial execution in each test suite for deterministism * fix(script): use base branch SHA, not ref for main * fix(script): ignore CI scripts written in Node.js and Jest config file * fix(script): post failure message when coverage drops * fix(script): use correct SHAs in PR comment message * fix(script): fail when any one of the coverage comparisons fail * fix(script): use the same PR comment bot for all messages * fix(script): use the same token so comments are cleared when re-run etc. * feat(script): make PR message more concise
184 lines
5.6 KiB
JavaScript
Executable file
184 lines
5.6 KiB
JavaScript
Executable file
#!/usr/bin/env node
|
|
|
|
const fs = require('fs');
|
|
|
|
const COVERAGE_MAIN_PATH = './coverage-main/coverage-summary.json';
|
|
const COVERAGE_PR_PATH = './coverage-pr/coverage-summary.json';
|
|
const COMPARISON_OUTPUT_PATH = './coverage-comparison.md';
|
|
|
|
/**
|
|
* Reads and parses a coverage summary JSON file
|
|
* @param {string} filePath - Path to coverage summary file
|
|
* @returns {Object} Parsed coverage data
|
|
*/
|
|
function readCoverageFile(filePath) {
|
|
try {
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
return JSON.parse(content);
|
|
} catch (err) {
|
|
console.error(`Error reading coverage file ${filePath}: ${err.message}`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Formats a number as a percentage string
|
|
* @param {number} value - Percentage value
|
|
* @returns {string} Formatted percentage (e.g., "85.34%")
|
|
*/
|
|
function formatPercentage(value) {
|
|
return `${value.toFixed(2)}%`;
|
|
}
|
|
|
|
/**
|
|
* Returns status icon based on coverage comparison
|
|
* @param {number} mainValue - Main branch coverage percentage
|
|
* @param {number} prValue - PR branch coverage percentage
|
|
* @returns {string} Status icon and text
|
|
*/
|
|
function getStatusIcon(mainValue, prValue) {
|
|
// Round to 2 decimal places for comparison to match display precision
|
|
const prPct = Math.round(prValue * 100) / 100;
|
|
const mainPct = Math.round(mainValue * 100) / 100;
|
|
|
|
if (prPct >= mainPct) {
|
|
return '✅ Pass';
|
|
}
|
|
return '❌ Fail';
|
|
}
|
|
|
|
/**
|
|
* Determines overall pass/fail status for all coverage metrics
|
|
* @param {Object} mainSummary - Main branch coverage summary
|
|
* @param {Object} prSummary - PR branch coverage summary
|
|
* @returns {boolean} True if all metrics maintained or improved
|
|
*/
|
|
function getOverallStatus(mainSummary, prSummary) {
|
|
const metrics = ['lines', 'statements', 'functions', 'branches'];
|
|
const allPass = metrics.every((metric) => {
|
|
// Round to 2 decimal places for comparison to match display precision
|
|
const prPct = Math.round(prSummary[metric].pct * 100) / 100;
|
|
const mainPct = Math.round(mainSummary[metric].pct * 100) / 100;
|
|
return prPct >= mainPct;
|
|
});
|
|
return allPass;
|
|
}
|
|
|
|
/**
|
|
* Calculates the difference between PR and main coverage
|
|
* @param {number} prValue - PR coverage percentage
|
|
* @param {number} mainValue - Main coverage percentage
|
|
* @returns {string} Formatted delta (e.g., "+1.2%" or "-0.5%")
|
|
*/
|
|
function formatDelta(prValue, mainValue) {
|
|
const delta = prValue - mainValue;
|
|
if (delta > 0) {
|
|
return `+${delta.toFixed(2)}%`;
|
|
} else if (delta < 0) {
|
|
return `${delta.toFixed(2)}%`;
|
|
}
|
|
return '—';
|
|
}
|
|
|
|
/**
|
|
* Generates markdown report comparing main and PR coverage
|
|
* @param {Object} mainCoverage - Main branch coverage data
|
|
* @param {Object} prCoverage - PR branch coverage data
|
|
* @returns {string} Markdown formatted report
|
|
*/
|
|
function generateMarkdown(mainCoverage, prCoverage) {
|
|
const teamName = prCoverage.team;
|
|
const mainSummary = mainCoverage.summary;
|
|
const prSummary = prCoverage.summary;
|
|
|
|
const overallPass = getOverallStatus(mainSummary, prSummary);
|
|
|
|
const rows = [
|
|
{
|
|
metric: 'Lines',
|
|
main: mainSummary.lines.pct,
|
|
pr: prSummary.lines.pct,
|
|
},
|
|
{
|
|
metric: 'Statements',
|
|
main: mainSummary.statements.pct,
|
|
pr: prSummary.statements.pct,
|
|
},
|
|
{
|
|
metric: 'Functions',
|
|
main: mainSummary.functions.pct,
|
|
pr: prSummary.functions.pct,
|
|
},
|
|
{
|
|
metric: 'Branches',
|
|
main: mainSummary.branches.pct,
|
|
pr: prSummary.branches.pct,
|
|
},
|
|
];
|
|
|
|
const tableRows = rows
|
|
.map((row) => {
|
|
const status = getStatusIcon(row.main, row.pr);
|
|
const delta = formatDelta(row.pr, row.main);
|
|
return `| ${row.metric} | ${formatPercentage(row.main)} | ${formatPercentage(row.pr)} | ${delta} | ${status} |`;
|
|
})
|
|
.join('\n');
|
|
|
|
const overallStatus = overallPass ? '✅ Passed' : '❌ Failed';
|
|
|
|
return `## Test Coverage Checks ${overallStatus} for ${teamName}
|
|
|
|
| Metric | Main | PR | Change | Status |
|
|
|--------|------|----|----|--------|
|
|
${tableRows}
|
|
|
|
**Run locally:** 💻 \`yarn test:coverage:by-codeowner ${teamName}\`
|
|
|
|
**Break glass:** 🚨 In case of emergency, adding the \`no-check-frontend-test-coverage\` label to this PR will skip checks.
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* Compares coverage between main and PR branches and generates a markdown report
|
|
* @param {string} mainPath - Path to main branch coverage summary JSON
|
|
* @param {string} prPath - Path to PR branch coverage summary JSON
|
|
* @param {string} outputPath - Path to write comparison markdown
|
|
* @returns {boolean} True if coverage check passed
|
|
*/
|
|
function compareCoverageByCodeowner(
|
|
mainPath = COVERAGE_MAIN_PATH,
|
|
prPath = COVERAGE_PR_PATH,
|
|
outputPath = COMPARISON_OUTPUT_PATH
|
|
) {
|
|
const mainCoverage = readCoverageFile(mainPath);
|
|
const prCoverage = readCoverageFile(prPath);
|
|
|
|
if (!mainCoverage.summary || !prCoverage.summary) {
|
|
console.error('Error: Coverage summary data is missing or invalid');
|
|
process.exit(1);
|
|
}
|
|
|
|
const markdown = generateMarkdown(mainCoverage, prCoverage);
|
|
const overallPass = getOverallStatus(mainCoverage.summary, prCoverage.summary);
|
|
|
|
try {
|
|
fs.writeFileSync(outputPath, markdown, 'utf8');
|
|
console.log(`✅ Coverage comparison written to ${outputPath}`);
|
|
} catch (err) {
|
|
console.error(`Error writing output file: ${err.message}`);
|
|
process.exit(1);
|
|
}
|
|
|
|
return overallPass;
|
|
}
|
|
|
|
if (require.main === module) {
|
|
const passed = compareCoverageByCodeowner();
|
|
if (!passed) {
|
|
console.error('❌ Coverage check failed: One or more metrics decreased');
|
|
process.exit(1);
|
|
}
|
|
console.log('✅ Coverage check passed: All metrics maintained or improved');
|
|
}
|
|
|
|
module.exports = { compareCoverageByCodeowner, generateMarkdown, getOverallStatus };
|