1
1
import * as core from '@actions/core' ;
2
2
import * as glob from '@actions/glob' ;
3
3
import { readFileSync , existsSync } from 'fs' ;
4
+ import { parseCodeowners } from './codeowner.js' ;
4
5
5
6
interface Input {
6
7
'include-gitignore' : boolean ;
7
8
'ignore-default' : boolean ;
8
9
files : string ;
10
+ allRulesMustHit : boolean ;
9
11
}
10
12
11
13
function getInputs ( ) : Input {
12
14
const result = { } as Input ;
13
15
result [ 'include-gitignore' ] = getBoolInput ( 'include-gitignore' ) ;
14
16
result [ 'ignore-default' ] = getBoolInput ( 'ignore-default' ) ;
17
+ result . allRulesMustHit = getBoolInput ( 'allRulesMustHit' ) ;
15
18
result . files = core . getInput ( 'files' ) ;
16
19
return result ;
17
20
}
@@ -25,61 +28,67 @@ export const runAction = async (input: Input): Promise<void> => {
25
28
core . startGroup ( `Loading files to check.` ) ;
26
29
if ( input . files ) {
27
30
filesToCheck = input . files . split ( ' ' ) ;
28
- filesToCheck = await ( await glob . create ( filesToCheck . join ( '\n' ) ) ) . glob ( ) ;
29
31
} else {
30
32
filesToCheck = await ( await glob . create ( '*' ) ) . glob ( ) ;
33
+ if ( input [ 'include-gitignore' ] === true ) {
34
+ core . info ( 'Ignoring .gitignored files' ) ;
35
+ let gitIgnoreFiles : string [ ] = [ ] ;
36
+ if ( ! existsSync ( '.gitignore' ) ) {
37
+ core . warning ( 'No .gitignore file found, skipping check.' ) ;
38
+ } else {
39
+ const gitIgnoreBuffer = readFileSync ( '.gitignore' , 'utf8' ) ;
40
+ const gitIgnoreGlob = await glob . create ( gitIgnoreBuffer ) ;
41
+ gitIgnoreFiles = await gitIgnoreGlob . glob ( ) ;
42
+ core . info ( `.gitignore Files: ${ gitIgnoreFiles . length } ` ) ;
43
+ const lengthBefore = filesToCheck . length ;
44
+ filesToCheck = filesToCheck . filter (
45
+ ( file ) => ! gitIgnoreFiles . includes ( file ) ,
46
+ ) ;
47
+ const filesIgnored = lengthBefore - filesToCheck . length ;
48
+ core . info ( `Files Ignored: ${ filesIgnored } ` ) ;
49
+ }
50
+ }
51
+ }
52
+ core . info ( `Found ${ filesToCheck . length } files to check.` ) ;
53
+ if ( core . isDebug ( ) ) {
54
+ core . debug ( filesToCheck . join ( '\n' ) ) ;
31
55
}
32
- // core.info(JSON.stringify(filesToCheck));
33
56
core . endGroup ( ) ;
34
57
35
- core . startGroup ( 'Reading CODEOWNERS File' ) ;
58
+ core . startGroup ( 'Parsing CODEOWNERS File' ) ;
36
59
const codeownerContent = getCodeownerContent ( ) ;
37
- let codeownerFileGlobs = codeownerContent
38
- . split ( '\n' )
39
- . map ( ( line ) => line . split ( ' ' ) [ 0 ] )
40
- . filter ( ( file ) => ! file . startsWith ( '#' ) )
41
- . map ( ( file ) => file . replace ( / ^ \/ / , '' ) ) ;
60
+ let parsedCodeowners = parseCodeowners ( codeownerContent ) ;
42
61
if ( input [ 'ignore-default' ] === true ) {
43
- codeownerFileGlobs = codeownerFileGlobs . filter ( ( file ) => file !== '*' ) ;
62
+ parsedCodeowners = parsedCodeowners . filter ( ( rule ) => rule . pattern !== '*' ) ;
44
63
}
45
- const codeownersGlob = await glob . create ( codeownerFileGlobs . join ( '\n' ) ) ;
46
- let codeownersFiles = await codeownersGlob . glob ( ) ;
47
- // core.info(JSON.stringify(codeownersFiles));
64
+ core . info ( `CODEOWNERS Rules: ${ parsedCodeowners . length } ` ) ;
48
65
core . endGroup ( ) ;
49
66
50
67
core . startGroup ( 'Matching CODEOWNER Files with found files' ) ;
51
- codeownersFiles = codeownersFiles . filter ( ( file ) =>
52
- filesToCheck . includes ( file ) ,
53
- ) ;
54
- core . info ( `CODEOWNER Files in All Files: ${ codeownersFiles . length } ` ) ;
55
- core . info ( JSON . stringify ( codeownersFiles ) ) ;
56
- core . endGroup ( ) ;
57
-
58
- if ( input [ 'include-gitignore' ] === true ) {
59
- core . startGroup ( 'Ignoring .gitignored files' ) ;
60
- let gitIgnoreFiles : string [ ] = [ ] ;
61
- if ( ! existsSync ( '.gitignore' ) ) {
62
- core . warning ( 'No .gitignore file found' ) ;
68
+ const rulesResult = parsedCodeowners . map ( ( rule ) => ( {
69
+ rule,
70
+ filtes : [ ] as string [ ] ,
71
+ } ) ) ;
72
+ rulesResult . reverse ( ) ; // last rule takes precedence.
73
+ const missedFiles : string [ ] = [ ] ;
74
+ filesToCheck . forEach ( ( file ) => {
75
+ const matchedRule = rulesResult . find ( ( { rule } ) => rule . isMatch ( file ) ) ;
76
+ if ( matchedRule ) {
77
+ matchedRule . filtes . push ( file ) ;
63
78
} else {
64
- const gitIgnoreBuffer = readFileSync ( '.gitignore' , 'utf8' ) ;
65
- const gitIgnoreGlob = await glob . create ( gitIgnoreBuffer ) ;
66
- gitIgnoreFiles = await gitIgnoreGlob . glob ( ) ;
67
- core . info ( `.gitignore Files: ${ gitIgnoreFiles . length } ` ) ;
68
- const lengthBefore = filesToCheck . length ;
69
- filesToCheck = filesToCheck . filter (
70
- ( file ) => ! gitIgnoreFiles . includes ( file ) ,
71
- ) ;
72
- const filesIgnored = lengthBefore - filesToCheck . length ;
73
- core . info ( `Files Ignored: ${ filesIgnored } ` ) ;
79
+ missedFiles . push ( file ) ;
74
80
}
75
- core . endGroup ( ) ;
81
+ } ) ;
82
+
83
+ core . info ( `${ missedFiles . length } files missing codeowners` ) ;
84
+ if ( core . isDebug ( ) ) {
85
+ core . debug ( JSON . stringify ( missedFiles ) ) ;
76
86
}
87
+ core . endGroup ( ) ;
77
88
78
89
core . startGroup ( 'Checking CODEOWNERS Coverage' ) ;
79
- const filesNotCovered = filesToCheck . filter (
80
- ( file ) => ! codeownersFiles . includes ( file ) ,
81
- ) ;
82
- const amountCovered = filesToCheck . length - filesNotCovered . length ;
90
+
91
+ const amountCovered = filesToCheck . length - missedFiles . length ;
83
92
84
93
const coveragePercent =
85
94
filesToCheck . length === 0
@@ -92,18 +101,31 @@ export const runAction = async (input: Input): Promise<void> => {
92
101
} ) ;
93
102
core . endGroup ( ) ;
94
103
core . startGroup ( 'Annotating files' ) ;
95
- filesNotCovered . forEach ( ( file ) =>
104
+ missedFiles . forEach ( ( file ) =>
96
105
core . error ( `File not covered by CODEOWNERS: ${ file } ` , {
97
106
title : 'File mssing in CODEOWNERS' ,
98
107
file : file ,
99
108
} ) ,
100
109
) ;
101
110
core . endGroup ( ) ;
102
- if ( filesNotCovered . length > 0 ) {
111
+ if ( missedFiles . length > 0 ) {
103
112
core . setFailed (
104
- `${ filesNotCovered . length } /${ filesToCheck . length } files not covered in CODEOWNERS` ,
113
+ `${ missedFiles . length } /${ filesToCheck . length } files not covered in CODEOWNERS` ,
105
114
) ;
106
115
}
116
+ if ( input . allRulesMustHit ) {
117
+ const unusedRules = rulesResult . filter ( ( { filtes } ) => filtes . length === 0 ) ;
118
+ if ( unusedRules . length > 0 ) {
119
+ core . setFailed ( `${ unusedRules . length } rules not used` ) ;
120
+ }
121
+ unusedRules . forEach ( ( { rule } ) => {
122
+ core . error ( `Rule not used: ${ rule . pattern } ` , {
123
+ title : 'Rule not used' ,
124
+ file : 'CODEOWNERS' ,
125
+ startLine : rule . lineNumber ,
126
+ } ) ;
127
+ } ) ;
128
+ }
107
129
} ;
108
130
109
131
export async function run ( ) : Promise < void > {
0 commit comments