mirror of
https://github.com/grafana/grafana.git
synced 2026-07-03 03:37:53 +00:00
Frontend coverage: show regressed files and HTML report artifact on failure (#124305)
This commit is contained in:
parent
6a2d839cda
commit
f6b3e475c3
4 changed files with 160 additions and 21 deletions
|
|
@ -296,9 +296,37 @@ jobs:
|
|||
if: steps.check-affected.outputs.affected == 'true'
|
||||
run: mkdir -p ./coverage-pr && cp ./pr/coverage-summary.json ./coverage-pr/coverage-summary.json
|
||||
|
||||
- name: Get codeowner coverage directory slug
|
||||
if: steps.check-affected.outputs.affected == 'true'
|
||||
id: coverage-dir
|
||||
run: |
|
||||
SLUG=$(node -e "
|
||||
const {createCodeownerSlug} = require('./pr/scripts/codeowners-manifest/utils.js');
|
||||
console.log(createCodeownerSlug('${{ matrix.codeowner }}'));
|
||||
")
|
||||
echo "slug=${SLUG}" >> "$GITHUB_OUTPUT"
|
||||
PATH=$(node -e "
|
||||
const {buildCodeownerDirectoryPath} = require('./pr/scripts/codeowners-manifest/utils.js');
|
||||
console.log(buildCodeownerDirectoryPath('${{ matrix.codeowner }}'));
|
||||
")
|
||||
echo "path=${PATH}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload PR HTML coverage report
|
||||
if: steps.check-affected.outputs.affected == 'true'
|
||||
id: upload-coverage
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: frontend-coverage-${{ steps.coverage-dir.outputs.slug }}
|
||||
path: ./pr/coverage/by-team/${{ steps.coverage-dir.outputs.path }}/html
|
||||
retention-days: 7
|
||||
|
||||
- name: Compare coverage
|
||||
if: steps.check-affected.outputs.affected == 'true'
|
||||
id: compare
|
||||
env:
|
||||
COVERAGE_ARTIFACT_URL: ${{ steps.upload-coverage.outputs.artifact-url }}
|
||||
PR_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
# Run comparison from workspace root where coverage-main/ and coverage-pr/ live
|
||||
if node ./pr/scripts/compare-coverage-by-codeowner.js; then
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ 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 { buildCodeownerDirectoryPath } = require('./scripts/codeowners-manifest/utils.js');
|
||||
|
||||
const CODEOWNERS_MANIFEST_FILENAMES_BY_TEAM_PATH = 'codeowners-manifest/filenames-by-team.json';
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ if (!codeownerName) {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
const outputDir = `./coverage/by-team/${createCodeownerDirectory(codeownerName)}`;
|
||||
const outputDir = path.join('./coverage/by-team', buildCodeownerDirectoryPath(codeownerName));
|
||||
const COVERAGE_SUMMARY_OUTPUT_PATH = './coverage-summary.json';
|
||||
|
||||
const codeownersFilePath = path.join(__dirname, CODEOWNERS_MANIFEST_FILENAMES_BY_TEAM_PATH);
|
||||
|
|
@ -147,6 +147,19 @@ function writeCoverageSummaryArtifact(coverageResults) {
|
|||
return;
|
||||
}
|
||||
|
||||
const files = {};
|
||||
if (coverageResults.files) {
|
||||
for (const file of coverageResults.files) {
|
||||
const relativePath = file.sourcePath.replace(process.cwd() + '/', '');
|
||||
files[relativePath] = {
|
||||
lines: { pct: file.summary.lines.pct },
|
||||
statements: { pct: file.summary.statements.pct },
|
||||
functions: { pct: file.summary.functions.pct },
|
||||
branches: { pct: file.summary.branches.pct },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const summary = {
|
||||
team: codeownerName,
|
||||
commit: process.env.GITHUB_SHA || 'unknown',
|
||||
|
|
@ -157,6 +170,7 @@ function writeCoverageSummaryArtifact(coverageResults) {
|
|||
functions: { pct: coverageResults.summary.functions.pct },
|
||||
branches: { pct: coverageResults.summary.branches.pct },
|
||||
},
|
||||
files,
|
||||
};
|
||||
|
||||
try {
|
||||
|
|
@ -167,24 +181,6 @@ function writeCoverageSummaryArtifact(coverageResults) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
const { readFile } = require('node:fs/promises');
|
||||
const path = require('node:path');
|
||||
|
||||
const { CODEOWNERS_JSON_PATH: CODEOWNERS_MANIFEST_CODEOWNERS_PATH } = require('./constants.js');
|
||||
|
||||
|
|
@ -61,10 +62,29 @@ function createCodeownerSlug(codeowner) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 buildCodeownerDirectoryPath(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 path.join(`${kind}s`, slug);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CODEOWNER_KIND,
|
||||
getCodeownerKind,
|
||||
createCodeownerSlug,
|
||||
buildCodeownerDirectoryPath,
|
||||
|
||||
/**
|
||||
* Imports codeowners manifest with caching
|
||||
|
|
|
|||
|
|
@ -80,6 +80,84 @@ function formatDelta(prValue, mainValue) {
|
|||
return '—';
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies files where coverage decreased between main and PR branches
|
||||
* @param {Object} mainCoverage - Main branch coverage data (with optional .files map)
|
||||
* @param {Object} prCoverage - PR branch coverage data (with optional .files map)
|
||||
* @returns {Array<{path: string, main: Object, pr: Object}>}
|
||||
*/
|
||||
function getFilesWithDecreasedCoverage(mainCoverage, prCoverage) {
|
||||
const mainFiles = mainCoverage.files || {};
|
||||
const prFiles = prCoverage.files || {};
|
||||
const metrics = ['lines', 'statements', 'functions', 'branches'];
|
||||
const decreased = [];
|
||||
|
||||
for (const [filePath, prFile] of Object.entries(prFiles)) {
|
||||
const mainFile = mainFiles[filePath];
|
||||
if (!mainFile) {
|
||||
continue; // new file — not a regression
|
||||
}
|
||||
|
||||
const anyDecreased = metrics.some((metric) => {
|
||||
const prPct = Math.round(prFile[metric].pct * 100) / 100;
|
||||
const mainPct = Math.round(mainFile[metric].pct * 100) / 100;
|
||||
return prPct < mainPct;
|
||||
});
|
||||
|
||||
if (anyDecreased) {
|
||||
decreased.push({ path: filePath, main: mainFile, pr: prFile });
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @grafana/no-locale-compare
|
||||
return decreased.sort((a, b) => a.path.localeCompare(b.path));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the "files with decreased coverage" markdown section
|
||||
* @param {Array} decreasedFiles - Output of getFilesWithDecreasedCoverage
|
||||
* @param {string} artifactUrl - URL to the uploaded HTML coverage artifact
|
||||
* @param {string} prSha - PR head commit SHA for GitHub file links
|
||||
* @param {string} repo - GitHub repository in "owner/repo" format
|
||||
* @returns {string} Markdown section
|
||||
*/
|
||||
function generateFailureDetailsSection(decreasedFiles, artifactUrl, prSha, repo) {
|
||||
const lines = [];
|
||||
if (decreasedFiles.length === 0) {
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
lines.push(`### Files with Decreased Coverage\n`);
|
||||
|
||||
if (artifactUrl) {
|
||||
lines.push(`📊 [View full HTML coverage report](${artifactUrl})\n`);
|
||||
}
|
||||
|
||||
const MAX_FILES = 20;
|
||||
const metrics = ['lines', 'statements', 'functions', 'branches'];
|
||||
const headers = ['File', 'Lines', 'Statements', 'Functions', 'Branches'];
|
||||
|
||||
lines.push(`| ${headers.join(' | ')} |`);
|
||||
lines.push(`|------|-------|------------|-----------|----------|`);
|
||||
|
||||
const shown = decreasedFiles.slice(0, MAX_FILES);
|
||||
for (const { path, main, pr } of shown) {
|
||||
const metricCells = metrics.map((metric) => {
|
||||
const prPct = Math.round(pr[metric].pct * 100) / 100;
|
||||
const mainPct = Math.round(main[metric].pct * 100) / 100;
|
||||
return prPct < mainPct ? `${formatPercentage(mainPct)} → ${formatPercentage(prPct)}` : '—';
|
||||
});
|
||||
|
||||
lines.push(`| ${path} | ${metricCells.join(' | ')} |`);
|
||||
}
|
||||
|
||||
if (decreasedFiles.length > MAX_FILES) {
|
||||
lines.push(`\n_...and ${decreasedFiles.length - MAX_FILES} more files. See the full report for details._`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates markdown report comparing main and PR coverage
|
||||
* @param {Object} mainCoverage - Main branch coverage data
|
||||
|
|
@ -126,12 +204,23 @@ function generateMarkdown(mainCoverage, prCoverage) {
|
|||
|
||||
const overallStatus = overallPass ? '✅ Passed' : '❌ Failed';
|
||||
|
||||
let failureDetails = '';
|
||||
if (!overallPass) {
|
||||
const artifactUrl = process.env.COVERAGE_ARTIFACT_URL || '';
|
||||
const prSha = process.env.PR_SHA || '';
|
||||
const repo = process.env.GITHUB_REPOSITORY || '';
|
||||
const decreasedFiles = getFilesWithDecreasedCoverage(mainCoverage, prCoverage);
|
||||
failureDetails = generateFailureDetailsSection(decreasedFiles, artifactUrl, prSha, repo);
|
||||
}
|
||||
|
||||
return `## Test Coverage Checks ${overallStatus} for ${teamName}
|
||||
|
||||
| Metric | Main | PR | Change | Status |
|
||||
|--------|------|----|----|--------|
|
||||
${tableRows}
|
||||
|
||||
${failureDetails}
|
||||
|
||||
**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.
|
||||
|
|
@ -181,4 +270,10 @@ if (require.main === module) {
|
|||
console.log('✅ Coverage check passed: All metrics maintained or improved');
|
||||
}
|
||||
|
||||
module.exports = { compareCoverageByCodeowner, generateMarkdown, getOverallStatus };
|
||||
module.exports = {
|
||||
compareCoverageByCodeowner,
|
||||
generateMarkdown,
|
||||
getOverallStatus,
|
||||
getFilesWithDecreasedCoverage,
|
||||
generateFailureDetailsSection,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue