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
197 lines
6.1 KiB
JavaScript
197 lines
6.1 KiB
JavaScript
const fs = require('fs');
|
|
const open = require('open').default;
|
|
const path = require('path');
|
|
|
|
const baseConfig = require('./jest.config.js');
|
|
const { CODEOWNER_KIND, getCodeownerKind, createCodeownerSlug } = require('./scripts/codeowners-manifest/utils.js');
|
|
|
|
const CODEOWNERS_MANIFEST_FILENAMES_BY_TEAM_PATH = 'codeowners-manifest/filenames-by-team.json';
|
|
|
|
const codeownerName = process.env.CODEOWNER_NAME;
|
|
if (!codeownerName) {
|
|
console.error('ERROR: CODEOWNER_NAME environment variable is required');
|
|
process.exit(1);
|
|
}
|
|
|
|
const outputDir = `./coverage/by-team/${createCodeownerDirectory(codeownerName)}`;
|
|
const COVERAGE_SUMMARY_OUTPUT_PATH = './coverage-summary.json';
|
|
|
|
const codeownersFilePath = path.join(__dirname, CODEOWNERS_MANIFEST_FILENAMES_BY_TEAM_PATH);
|
|
|
|
if (!fs.existsSync(codeownersFilePath)) {
|
|
console.error(`Codeowners file not found at ${codeownersFilePath} ...`);
|
|
console.error('Please run: yarn codeowners-manifest first to generate the mapping file');
|
|
process.exit(1);
|
|
}
|
|
|
|
const codeownersData = JSON.parse(fs.readFileSync(codeownersFilePath, 'utf8'));
|
|
const teamFiles = codeownersData[codeownerName] || [];
|
|
|
|
if (teamFiles.length === 0) {
|
|
console.error(`ERROR: No files found for team "${codeownerName}"`);
|
|
console.error('Available teams:', Object.keys(codeownersData).join(', '));
|
|
process.exit(1);
|
|
}
|
|
|
|
const sourceFiles = teamFiles.filter((file) => {
|
|
const ext = path.extname(file);
|
|
return (
|
|
['.ts', '.tsx', '.js', '.jsx'].includes(ext) &&
|
|
// exclude all tests and mocks
|
|
!path.matchesGlob(file, '**/test/**/*') &&
|
|
!file.includes('.test.') &&
|
|
!file.includes('.spec.') &&
|
|
!path.matchesGlob(file, '**/__mocks__/**/*') &&
|
|
// and storybook stories
|
|
!file.includes('.story.') &&
|
|
// and generated files
|
|
!file.includes('.gen.ts') &&
|
|
// and type definitions
|
|
!file.includes('.d.ts') &&
|
|
!file.endsWith('/types.ts') &&
|
|
// and anything in graveyard
|
|
!path.matchesGlob(file, '**/graveyard/**/*') &&
|
|
// and scripts directory
|
|
!file.startsWith('scripts/') &&
|
|
// and jest config files
|
|
!path.matchesGlob(file, '**/jest.config*.js')
|
|
);
|
|
});
|
|
|
|
const testFiles = teamFiles.filter((file) => {
|
|
const ext = path.extname(file);
|
|
return ['.ts', '.tsx', '.js', '.jsx'].includes(ext) && (file.includes('.test.') || file.includes('.spec.'));
|
|
});
|
|
|
|
if (testFiles.length === 0) {
|
|
console.log(`No test files found for team ${codeownerName}`);
|
|
process.exit(0);
|
|
}
|
|
|
|
console.log(
|
|
`🧪 Collecting coverage for ${sourceFiles.length} testable files and running ${testFiles.length} test files of ${teamFiles.length} files owned by ${codeownerName}.`
|
|
);
|
|
|
|
module.exports = {
|
|
...baseConfig,
|
|
|
|
collectCoverage: true,
|
|
collectCoverageFrom: sourceFiles.map((file) => `<rootDir>/${file}`),
|
|
coverageReporters: ['none'],
|
|
coverageDirectory: '/tmp/jest-coverage-ignore',
|
|
|
|
coverageProvider: 'v8',
|
|
reporters: [
|
|
'default',
|
|
[
|
|
'jest-monocart-coverage',
|
|
{
|
|
name: `Coverage Report - ${codeownerName} owned files`,
|
|
outputDir: outputDir,
|
|
reports: ['console-summary', 'v8', 'json', 'lcov'],
|
|
sourceFilter: (coveredFile) => sourceFiles.includes(coveredFile),
|
|
all: {
|
|
dir: ['./packages', './public'],
|
|
filter: (filePath) => {
|
|
const relativePath = filePath.replace(process.cwd() + '/', '');
|
|
return sourceFiles.includes(relativePath);
|
|
},
|
|
},
|
|
cleanCache: true,
|
|
onEnd: (coverageResults) => {
|
|
const reportURL = `file://${path.resolve(outputDir)}/index.html`;
|
|
console.log(`📄 Coverage report saved to ${reportURL}`);
|
|
|
|
if (process.env.SHOULD_OPEN_COVERAGE_REPORT === 'true') {
|
|
openCoverageReport(reportURL);
|
|
}
|
|
|
|
writeCoverageSummaryArtifact(coverageResults);
|
|
|
|
// TODO: Emit coverage metrics https://github.com/grafana/grafana/issues/111208
|
|
},
|
|
},
|
|
],
|
|
],
|
|
|
|
testRegex: undefined,
|
|
|
|
testMatch: testFiles.map((file) => `<rootDir>/${file}`),
|
|
};
|
|
|
|
/**
|
|
* @typedef {Object} CoverageMetric
|
|
* @property {number} pct - Percentage value
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} CoverageSummary
|
|
* @property {CoverageMetric} lines
|
|
* @property {CoverageMetric} statements
|
|
* @property {CoverageMetric} functions
|
|
* @property {CoverageMetric} branches
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Object} CoverageResults
|
|
* @property {CoverageSummary} summary
|
|
*/
|
|
|
|
/**
|
|
* Writes coverage summary artifact for CI/CD consumption
|
|
* @param {CoverageResults} coverageResults - Coverage results from jest-monocart-coverage
|
|
*/
|
|
function writeCoverageSummaryArtifact(coverageResults) {
|
|
if (!coverageResults || !coverageResults.summary) {
|
|
return;
|
|
}
|
|
|
|
const summary = {
|
|
team: codeownerName,
|
|
commit: process.env.GITHUB_SHA || 'unknown',
|
|
timestamp: new Date().toISOString(),
|
|
summary: {
|
|
lines: { pct: coverageResults.summary.lines.pct },
|
|
statements: { pct: coverageResults.summary.statements.pct },
|
|
functions: { pct: coverageResults.summary.functions.pct },
|
|
branches: { pct: coverageResults.summary.branches.pct },
|
|
},
|
|
};
|
|
|
|
try {
|
|
fs.writeFileSync(COVERAGE_SUMMARY_OUTPUT_PATH, JSON.stringify(summary, null, 2));
|
|
console.log(`📊 Coverage summary written to ${COVERAGE_SUMMARY_OUTPUT_PATH}`);
|
|
} catch (err) {
|
|
console.error(`Failed to write coverage summary: ${err}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a directory path for coverage reports grouped by codeowner kind
|
|
* @param {string} codeowner - CODEOWNERS codeowner
|
|
* @returns {string} Directory path relative to coverage/by-team/
|
|
*/
|
|
function createCodeownerDirectory(codeowner) {
|
|
const kind = getCodeownerKind(codeowner);
|
|
|
|
if (kind === CODEOWNER_KIND.UNKNOWN) {
|
|
throw new Error(
|
|
`Invalid codeowner format: "${codeowner}". Must be a GitHub team (@org/team), user (@username), or email (email@domain.tld)`
|
|
);
|
|
}
|
|
|
|
const slug = createCodeownerSlug(codeowner);
|
|
return `${kind}s/${slug}`;
|
|
}
|
|
|
|
/**
|
|
* Opens the coverage report in the default browser
|
|
* @param {string} reportURL - File URL to the coverage report HTML
|
|
*/
|
|
async function openCoverageReport(reportURL) {
|
|
try {
|
|
await open(reportURL);
|
|
} catch (err) {
|
|
console.error(`Failed to open coverage report: ${err}`);
|
|
}
|
|
}
|