11import * as fs from 'fs' ;
22import * as path from 'path' ;
33import * as core from '@actions/core' ;
4- import { CoverageThreshold , getProjectThresholds , ThresholdConfig } from './threshold-handler' ;
4+
5+ interface CoverageThreshold {
6+ lines ?: number ;
7+ statements ?: number ;
8+ functions ?: number ;
9+ branches ?: number ;
10+ }
11+
12+ interface ThresholdConfig {
13+ global : CoverageThreshold ;
14+ projects : Record < string , CoverageThreshold | null > ;
15+ }
516
617interface CoverageSummary {
718 lines : { pct : number } ;
@@ -20,23 +31,59 @@ interface ProjectCoverageResult {
2031 branches : number ;
2132 } | null ;
2233 status : 'PASSED' | 'FAILED' | 'SKIPPED' ;
23- failedMetrics ?: string [ ] ;
34+ }
35+
36+ /**
37+ * Parses the COVERAGE_THRESHOLDS environment variable
38+ */
39+ export function getCoverageThresholds ( ) : ThresholdConfig {
40+ if ( ! process . env . COVERAGE_THRESHOLDS ) {
41+ return { global : { } , projects : { } } ;
42+ }
43+
44+ try {
45+ return JSON . parse ( process . env . COVERAGE_THRESHOLDS ) ;
46+ } catch ( error ) {
47+ core . error ( `Error parsing COVERAGE_THRESHOLDS: ${ error . message } ` ) ;
48+ return { global : { } , projects : { } } ;
49+ }
50+ }
51+
52+ /**
53+ * Gets thresholds for a specific project
54+ */
55+ function getProjectThresholds ( project : string , thresholds : ThresholdConfig ) : CoverageThreshold | null {
56+ // If project explicitly set to null, return null to skip
57+ if ( thresholds . projects && thresholds . projects [ project ] === null ) {
58+ return null ;
59+ }
60+
61+ // If project has specific thresholds, use those
62+ if ( thresholds . projects && thresholds . projects [ project ] ) {
63+ return thresholds . projects [ project ] ;
64+ }
65+
66+ // Otherwise, use global thresholds if available
67+ if ( thresholds . global ) {
68+ return thresholds . global ;
69+ }
70+
71+ // If no thresholds defined, return null
72+ return null ;
2473}
2574
2675/**
2776 * Evaluates coverage for all projects against their thresholds
2877 */
29- export function evaluateCoverage ( projects : string [ ] , thresholds : ThresholdConfig ) : boolean {
78+ export function evaluateCoverage ( projects : string [ ] , thresholds : ThresholdConfig ) : { failedCount : number , projects : string [ ] } {
3079 if ( ! process . env . COVERAGE_THRESHOLDS ) {
31- core . info ( 'No coverage thresholds defined, skipping evaluation' ) ;
32- return true ; // No thresholds defined, pass by default
80+ return { failedCount : 0 , projects : [ ] } ; // No thresholds defined, pass by default
3381 }
3482
35- let allProjectsPassed = true ;
83+ let failedCount = 0 ;
84+ const failedProjects : string [ ] = [ ] ;
3685 const coverageResults : ProjectCoverageResult [ ] = [ ] ;
3786
38- core . info ( `Evaluating coverage for ${ projects . length } projects` ) ;
39-
4087 for ( const project of projects ) {
4188 const projectThresholds = getProjectThresholds ( project , thresholds ) ;
4289
@@ -60,46 +107,26 @@ export function evaluateCoverage(projects: string[], thresholds: ThresholdConfig
60107 project,
61108 thresholds : projectThresholds ,
62109 actual : null ,
63- status : 'FAILED' , // Mark as failed if no coverage report is found
64- failedMetrics : [ 'No coverage report found' ]
110+ status : 'FAILED' // Mark as failed if no coverage report is found
65111 } ) ;
66- allProjectsPassed = false ;
112+ failedCount ++ ;
113+ failedProjects . push ( project ) ;
67114 continue ;
68115 }
69116
70117 try {
71118 const coverageData = JSON . parse ( fs . readFileSync ( coveragePath , 'utf8' ) ) ;
72119 const summary = coverageData . total as CoverageSummary ;
73120
74- let projectPassed = true ;
75- const failedMetrics : string [ ] = [ ] ;
76-
77- // Check each metric if threshold is defined
78- if ( projectThresholds . lines !== undefined && summary . lines . pct < projectThresholds . lines ) {
79- projectPassed = false ;
80- failedMetrics . push ( `lines: ${ summary . lines . pct . toFixed ( 2 ) } % < ${ projectThresholds . lines } %` ) ;
81- }
82-
83- if ( projectThresholds . statements !== undefined && summary . statements . pct < projectThresholds . statements ) {
84- projectPassed = false ;
85- failedMetrics . push ( `statements: ${ summary . statements . pct . toFixed ( 2 ) } % < ${ projectThresholds . statements } %` ) ;
86- }
87-
88- if ( projectThresholds . functions !== undefined && summary . functions . pct < projectThresholds . functions ) {
89- projectPassed = false ;
90- failedMetrics . push ( `functions: ${ summary . functions . pct . toFixed ( 2 ) } % < ${ projectThresholds . functions } %` ) ;
91- }
92-
93- if ( projectThresholds . branches !== undefined && summary . branches . pct < projectThresholds . branches ) {
94- projectPassed = false ;
95- failedMetrics . push ( `branches: ${ summary . branches . pct . toFixed ( 2 ) } % < ${ projectThresholds . branches } %` ) ;
96- }
121+ const projectPassed =
122+ ( ! projectThresholds . lines || summary . lines . pct >= projectThresholds . lines ) &&
123+ ( ! projectThresholds . statements || summary . statements . pct >= projectThresholds . statements ) &&
124+ ( ! projectThresholds . functions || summary . functions . pct >= projectThresholds . functions ) &&
125+ ( ! projectThresholds . branches || summary . branches . pct >= projectThresholds . branches ) ;
97126
98127 if ( ! projectPassed ) {
99- core . error ( `Project ${ project } failed coverage thresholds: ${ failedMetrics . join ( ', ' ) } ` ) ;
100- allProjectsPassed = false ;
101- } else {
102- core . info ( `Project ${ project } passed all coverage thresholds` ) ;
128+ failedCount ++ ;
129+ failedProjects . push ( project ) ;
103130 }
104131
105132 coverageResults . push ( {
@@ -111,8 +138,7 @@ export function evaluateCoverage(projects: string[], thresholds: ThresholdConfig
111138 functions : summary . functions . pct ,
112139 branches : summary . branches . pct
113140 } ,
114- status : projectPassed ? 'PASSED' : 'FAILED' ,
115- failedMetrics : projectPassed ? undefined : failedMetrics
141+ status : projectPassed ? 'PASSED' : 'FAILED'
116142 } ) ;
117143 } catch ( error ) {
118144 core . error ( `Error processing coverage for ${ project } : ${ error . message } ` ) ;
@@ -122,20 +148,21 @@ export function evaluateCoverage(projects: string[], thresholds: ThresholdConfig
122148 actual : null ,
123149 status : 'FAILED'
124150 } ) ;
125- allProjectsPassed = false ;
151+ failedCount ++ ;
152+ failedProjects . push ( project ) ;
126153 }
127154 }
128155
129- // Post results to PR comment
130- postCoverageComment ( coverageResults ) ;
156+ // Post results to PR comment with special handling for single failing project
157+ postCoverageComment ( coverageResults , failedCount , failedProjects ) ;
131158
132- return allProjectsPassed ;
159+ return { failedCount , projects : failedProjects } ;
133160}
134161
135162/**
136163 * Formats the coverage results into a markdown table
137164 */
138- function formatCoverageComment ( results : ProjectCoverageResult [ ] , artifactUrl : string ) : string {
165+ function formatCoverageComment ( results : ProjectCoverageResult [ ] , failedCount : number , failedProjects : string [ ] , artifactUrl : string ) : string {
139166 let comment = '## Test Coverage Results\n\n' ;
140167
141168 if ( results . length === 0 ) {
@@ -150,8 +177,7 @@ function formatCoverageComment(results: ProjectCoverageResult[], artifactUrl: st
150177 if ( result . status === 'SKIPPED' ) {
151178 comment += `| ${ result . project } | All | N/A | N/A | ⏩ SKIPPED |\n` ;
152179 } else if ( result . actual === null ) {
153- const errorMessage = result . failedMetrics ? result . failedMetrics [ 0 ] : 'No Data' ;
154- comment += `| ${ result . project } | All | Defined | ${ errorMessage } | ❌ FAILED |\n` ;
180+ comment += `| ${ result . project } | All | Defined | No Data | ❌ FAILED |\n` ;
155181 } else {
156182 const metrics = [ 'lines' , 'statements' , 'functions' , 'branches' ] ;
157183 metrics . forEach ( ( metric , index ) => {
@@ -160,8 +186,7 @@ function formatCoverageComment(results: ProjectCoverageResult[], artifactUrl: st
160186
161187 const threshold = result . thresholds [ metric ] ;
162188 const actual = result . actual [ metric ] . toFixed ( 2 ) ;
163- const isMetricPassed = actual >= threshold ;
164- const status = isMetricPassed ? '✅ PASSED' : '❌ FAILED' ;
189+ const status = actual >= threshold ? '✅ PASSED' : '❌ FAILED' ;
165190
166191 // Only include project name in the first row for this project
167192 const projectCell = index === 0 ? result . project : '' ;
@@ -171,23 +196,17 @@ function formatCoverageComment(results: ProjectCoverageResult[], artifactUrl: st
171196 }
172197 } ) ;
173198
174- // Add overall status
175- const overallStatus = results . every ( r => r . status !== 'FAILED' ) ? '✅ PASSED' : '❌ FAILED' ;
176- comment += `\n### Overall Status: ${ overallStatus } \n` ;
177-
178- // Add failed projects list if any
179- const failedProjects = results . filter ( r => r . status === 'FAILED' ) ;
180- if ( failedProjects . length > 0 ) {
181- comment += '\n### Failed Projects\n\n' ;
182- failedProjects . forEach ( project => {
183- comment += `- **${ project . project } **: ` ;
184- if ( project . failedMetrics ) {
185- comment += project . failedMetrics . join ( ', ' ) ;
186- } else {
187- comment += 'Failed to meet coverage thresholds' ;
188- }
189- comment += '\n' ;
190- } ) ;
199+ // Add special message for single failing project
200+ if ( failedCount === 1 ) {
201+ comment += `\n### Policy Exception: One project (${ failedProjects [ 0 ] } ) failed, but PR is allowed to pass ⚠️\n` ;
202+ comment += `According to team policy, PRs are allowed to pass when only one project fails the coverage threshold.\n` ;
203+ } else if ( failedCount > 1 ) {
204+ comment += `\n### Overall Status: ❌ FAILED\n` ;
205+ comment += `${ failedCount } projects failed to meet coverage thresholds: ${ failedProjects . join ( ', ' ) } \n` ;
206+ comment += `PRs are only allowed to pass when at most one project fails the coverage threshold.\n` ;
207+ } else {
208+ comment += `\n### Overall Status: ✅ PASSED\n` ;
209+ comment += `All projects meet coverage thresholds! 🎉\n` ;
191210 }
192211
193212 // Add link to detailed HTML reports
@@ -201,11 +220,11 @@ function formatCoverageComment(results: ProjectCoverageResult[], artifactUrl: st
201220/**
202221 * Writes the coverage results to a file for PR comment
203222 */
204- function postCoverageComment ( results : ProjectCoverageResult [ ] ) : void {
223+ function postCoverageComment ( results : ProjectCoverageResult [ ] , failedCount : number , failedProjects : string [ ] ) : void {
205224 // The actual artifact URL will be provided by GitHub Actions in the workflow
206225 const artifactUrl = process . env . COVERAGE_ARTIFACT_URL || '' ;
207226
208- const comment = formatCoverageComment ( results , artifactUrl ) ;
227+ const comment = formatCoverageComment ( results , failedCount , failedProjects , artifactUrl ) ;
209228
210229 // Write to a file that will be used by thollander/actions-comment-pull-request action
211230 const gitHubCommentsFile = path . resolve ( process . cwd ( ) , 'coverage-report.txt' ) ;
0 commit comments