Skip to content

Commit bbb5e22

Browse files
committed
PFM-TASK-6308 refactor: enhance coverage evaluation and reporting for affected projects
1 parent 2e8203f commit bbb5e22

4 files changed

Lines changed: 85 additions & 37 deletions

File tree

tools/scripts/run-many/affected-projects.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export function getAffectedProjects(
2121
jobCount: number,
2222
base: string,
2323
ref: string
24-
) {
24+
): string {
2525
let allAffectedProjects = [];
2626
if (target === 'e2e' && ref === '') {
2727
allAffectedProjects = Utils.getAllProjects(false, null, target);
@@ -32,5 +32,17 @@ export function getAffectedProjects(
3232
const projects = distributeProjectsEvenly(allAffectedProjects, jobCount);
3333
console.log(`Affected Projects:`);
3434
console.table(projects);
35+
36+
// Handle case when no projects are assigned to this job index
37+
if (jobIndex - 1 >= projects.length || !projects[jobIndex - 1] || projects[jobIndex - 1].length === 0) {
38+
return '';
39+
}
40+
3541
return projects[jobIndex - 1].join(',');
3642
}
43+
44+
// Check if there are any affected projects
45+
export function hasAffectedProjects(base: string, target?: string): boolean {
46+
const projects = Utils.getAllProjects(true, base, target);
47+
return projects.length > 0;
48+
}

tools/scripts/run-many/coverage-evaluator.ts

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,15 @@ export function evaluateCoverage(projects: string[], thresholds: ThresholdConfig
3030
core.info('No coverage thresholds defined, skipping evaluation');
3131
return true; // No thresholds defined, pass by default
3232
}
33-
33+
3434
let allProjectsPassed = true;
3535
const coverageResults: ProjectCoverageResult[] = [];
36-
36+
3737
core.info(`Evaluating coverage for ${projects.length} projects`);
38-
38+
3939
for (const project of projects) {
4040
const projectThresholds = getProjectThresholds(project, thresholds);
41-
41+
4242
// Skip projects with null thresholds
4343
if (projectThresholds === null) {
4444
core.info(`Coverage evaluation skipped for ${project}`);
@@ -50,9 +50,9 @@ export function evaluateCoverage(projects: string[], thresholds: ThresholdConfig
5050
});
5151
continue;
5252
}
53-
53+
5454
const coveragePath = path.resolve(process.cwd(), `coverage/${project}/coverage-summary.json`);
55-
55+
5656
if (!fs.existsSync(coveragePath)) {
5757
core.warning(`No coverage report found for ${project} at ${coveragePath}`);
5858
coverageResults.push({
@@ -64,42 +64,42 @@ export function evaluateCoverage(projects: string[], thresholds: ThresholdConfig
6464
allProjectsPassed = false;
6565
continue;
6666
}
67-
67+
6868
try {
6969
const coverageData = JSON.parse(fs.readFileSync(coveragePath, 'utf8'));
7070
const summary = coverageData.total as CoverageSummary;
71-
71+
7272
let projectPassed = true;
7373
const failedMetrics: string[] = [];
74-
74+
7575
// Check each metric if threshold is defined
7676
if (projectThresholds.lines !== undefined && summary.lines.pct < projectThresholds.lines) {
7777
projectPassed = false;
7878
failedMetrics.push(`lines: ${summary.lines.pct.toFixed(2)}% < ${projectThresholds.lines}%`);
7979
}
80-
80+
8181
if (projectThresholds.statements !== undefined && summary.statements.pct < projectThresholds.statements) {
8282
projectPassed = false;
8383
failedMetrics.push(`statements: ${summary.statements.pct.toFixed(2)}% < ${projectThresholds.statements}%`);
8484
}
85-
85+
8686
if (projectThresholds.functions !== undefined && summary.functions.pct < projectThresholds.functions) {
8787
projectPassed = false;
8888
failedMetrics.push(`functions: ${summary.functions.pct.toFixed(2)}% < ${projectThresholds.functions}%`);
8989
}
90-
90+
9191
if (projectThresholds.branches !== undefined && summary.branches.pct < projectThresholds.branches) {
9292
projectPassed = false;
9393
failedMetrics.push(`branches: ${summary.branches.pct.toFixed(2)}% < ${projectThresholds.branches}%`);
9494
}
95-
95+
9696
if (!projectPassed) {
9797
core.error(`Project ${project} failed coverage thresholds: ${failedMetrics.join(', ')}`);
9898
allProjectsPassed = false;
9999
} else {
100100
core.info(`Project ${project} passed all coverage thresholds`);
101101
}
102-
102+
103103
coverageResults.push({
104104
project,
105105
thresholds: projectThresholds,
@@ -122,10 +122,10 @@ export function evaluateCoverage(projects: string[], thresholds: ThresholdConfig
122122
allProjectsPassed = false;
123123
}
124124
}
125-
125+
126126
// Post results to PR comment
127127
postCoverageComment(coverageResults);
128-
128+
129129
return allProjectsPassed;
130130
}
131131

@@ -134,9 +134,15 @@ export function evaluateCoverage(projects: string[], thresholds: ThresholdConfig
134134
*/
135135
function formatCoverageComment(results: ProjectCoverageResult[], artifactUrl: string): string {
136136
let comment = '## Test Coverage Results\n\n';
137+
138+
if (results.length === 0) {
139+
comment += 'No projects were evaluated for coverage.\n';
140+
return comment;
141+
}
142+
137143
comment += '| Project | Metric | Threshold | Actual | Status |\n';
138144
comment += '|---------|--------|-----------|--------|--------|\n';
139-
145+
140146
results.forEach(result => {
141147
if (result.status === 'SKIPPED') {
142148
comment += `| ${result.project} | All | N/A | N/A | ⏩ SKIPPED |\n`;
@@ -147,28 +153,28 @@ function formatCoverageComment(results: ProjectCoverageResult[], artifactUrl: st
147153
metrics.forEach((metric, index) => {
148154
// Skip metrics that don't have a threshold
149155
if (!result.thresholds[metric]) return;
150-
156+
151157
const threshold = result.thresholds[metric];
152158
const actual = result.actual[metric].toFixed(2);
153159
const status = actual >= threshold ? '✅ PASSED' : '❌ FAILED';
154-
160+
155161
// Only include project name in the first row for this project
156162
const projectCell = index === 0 ? result.project : '';
157-
163+
158164
comment += `| ${projectCell} | ${metric} | ${threshold}% | ${actual}% | ${status} |\n`;
159165
});
160166
}
161167
});
162-
168+
163169
// Add overall status
164170
const overallStatus = results.every(r => r.status !== 'FAILED') ? '✅ PASSED' : '❌ FAILED';
165171
comment += `\n### Overall Status: ${overallStatus}\n`;
166-
172+
167173
// Add link to detailed HTML reports
168174
if (artifactUrl) {
169175
comment += `\n📊 [View Detailed HTML Coverage Reports](${artifactUrl})\n`;
170176
}
171-
177+
172178
return comment;
173179
}
174180

@@ -178,12 +184,24 @@ function formatCoverageComment(results: ProjectCoverageResult[], artifactUrl: st
178184
function postCoverageComment(results: ProjectCoverageResult[]): void {
179185
// The actual artifact URL will be provided by GitHub Actions in the workflow
180186
const artifactUrl = process.env.COVERAGE_ARTIFACT_URL || '';
181-
187+
182188
const comment = formatCoverageComment(results, artifactUrl);
183-
189+
184190
// Write to a file that will be used by thollander/actions-comment-pull-request action
185191
const gitHubCommentsFile = path.resolve(process.cwd(), 'coverage-report.txt');
186192
fs.writeFileSync(gitHubCommentsFile, comment);
187-
193+
188194
core.info('Coverage results saved for PR comment');
189195
}
196+
197+
/**
198+
* Generates a coverage report when no projects are affected
199+
*/
200+
export function generateEmptyCoverageReport(): void {
201+
const comment = '## Test Coverage Results\n\n⏩ No projects were affected by this change that require coverage evaluation.\n';
202+
203+
const gitHubCommentsFile = path.resolve(process.cwd(), 'coverage-report.txt');
204+
fs.writeFileSync(gitHubCommentsFile, comment);
205+
206+
core.info('Empty coverage report generated (no affected projects)');
207+
}

tools/scripts/run-many/run-many.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { getAffectedProjects } from './affected-projects';
22
import { execSync } from 'child_process';
33
import * as core from '@actions/core';
4+
import * as fs from 'fs';
45
import * as path from 'path';
56
import { getCoverageThresholds } from './threshold-handler';
6-
import { evaluateCoverage } from './coverage-evaluator';
7+
import { evaluateCoverage, generateEmptyCoverageReport } from './coverage-evaluator';
78

89
function getE2ECommand(command: string, base: string): string {
910
command = command.concat(` -c ci --base=${base} --verbose`);
@@ -31,6 +32,12 @@ function runCommand(command: string): void {
3132
}
3233
}
3334

35+
function ensureDirectoryExists(dirPath: string): void {
36+
if (!fs.existsSync(dirPath)) {
37+
fs.mkdirSync(dirPath, { recursive: true });
38+
}
39+
}
40+
3441
function main() {
3542
const target = process.argv[2];
3643
const jobIndex = Number(process.argv[3]);
@@ -46,22 +53,24 @@ function main() {
4653

4754
core.info(`Inputs:\n target ${target},\n jobIndex: ${jobIndex},\n jobCount ${jobCount},\n base ${base},\n ref ${ref}`)
4855

49-
const projectsString = getAffectedProjects(target, jobIndex, jobCount, base, ref);
50-
const projects = projectsString ? projectsString.split(',') : [];
51-
5256
// Check if coverage gate is enabled
5357
const coverageEnabled = !!process.env.COVERAGE_THRESHOLDS;
5458

55-
if (coverageEnabled && target === 'test') {
56-
core.info('Coverage gate is enabled');
57-
}
59+
// Get the affected projects
60+
const projectsString = getAffectedProjects(target, jobIndex, jobCount, base, ref);
61+
const projects = projectsString ? projectsString.split(',') : [];
62+
63+
// Check if there are any affected projects (for first job only, to avoid duplicate reports)
64+
const areAffectedProjects = projects.length > 0;
65+
const isFirstJob = jobIndex === 1;
5866

5967
// Modified command construction
6068
const runManyProjectsCmd = `npx nx run-many --targets=${target} --projects="${projectsString}"`;
6169
let cmd = `${runManyProjectsCmd} --parallel=false --prod`;
6270

6371
// Add coverage flag if enabled and target is test
6472
if (coverageEnabled && target === 'test') {
73+
core.info('Coverage gate is enabled');
6574
// Add coverage reporters for HTML, JSON, and JUnit output
6675
cmd += ' --coverage --coverageReporters=json,lcov,text,clover,html,json-summary --reporters=default,jest-junit';
6776
}
@@ -70,7 +79,7 @@ function main() {
7079
cmd = getE2ECommand(cmd, base);
7180
}
7281

73-
if (projects.length > 0) {
82+
if (areAffectedProjects) {
7483
runCommand(cmd);
7584

7685
// Evaluate coverage if enabled and target is test
@@ -90,6 +99,15 @@ function main() {
9099
}
91100
} else {
92101
core.info('No affected projects :)');
102+
103+
// Generate empty coverage report for first job only when coverage is enabled
104+
if (coverageEnabled && target === 'test' && isFirstJob) {
105+
// Ensure coverage directory exists for artifact upload
106+
ensureDirectoryExists(path.resolve(process.cwd(), 'coverage'));
107+
108+
// Generate empty report
109+
generateEmptyCoverageReport();
110+
}
93111
}
94112
}
95113

tools/scripts/run-many/threshold-handler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,12 @@ export function getCoverageThresholds(): ThresholdConfig {
2424
try {
2525
const thresholdConfig = JSON.parse(process.env.COVERAGE_THRESHOLDS);
2626
core.info(`Successfully parsed coverage thresholds`);
27-
27+
2828
// Validate structure
2929
if (!thresholdConfig.global) {
3030
core.warning('No global thresholds defined in configuration');
3131
}
32-
32+
3333
return thresholdConfig;
3434
} catch (error) {
3535
core.error(`Error parsing COVERAGE_THRESHOLDS: ${error.message}`);

0 commit comments

Comments
 (0)