@@ -2,6 +2,7 @@ import { normalizeJobName, normalizeTestName } from './report-ci-failures.mjs';
22
33const LOOKBACK_DAYS = 7 ;
44const MAX_TESTS = 10 ;
5+ const CONCURRENT_RUNS = 4 ;
56
67function jobFamily ( name ) {
78 return normalizeJobName (
@@ -20,10 +21,11 @@ function markdownCell(value) {
2021 . replace ( / [ \r \n ] + / g, ' ' ) ;
2122}
2223
23- export async function collectReport ( { github, context, now = new Date ( ) } ) {
24+ export async function collectReport ( { github, context, core , now = new Date ( ) } ) {
2425 const until = now . toISOString ( ) ;
2526 const since = new Date ( now . getTime ( ) - LOOKBACK_DAYS * 24 * 60 * 60 * 1000 ) . toISOString ( ) ;
2627 const repo = context . repo ;
28+ core . info ( `Listing develop CI runs from ${ since } to ${ until } .` ) ;
2729 const listedRuns = await github . paginate ( github . rest . actions . listWorkflowRuns , {
2830 ...repo ,
2931 workflow_id : 'build.yml' ,
@@ -39,83 +41,94 @@ export async function collectReport({ github, context, now = new Date() }) {
3941 const tests = new Map ( ) ;
4042 let failedJobsWithoutTests = 0 ;
4143 let failedJobs = 0 ;
44+ let completedRuns = 0 ;
45+ const pendingRuns = runs . filter ( run => run . conclusion !== 'success' || run . run_attempt > 1 ) ;
46+ const runsToInspect = pendingRuns . length ;
47+ core . info (
48+ `Found ${ runs . length } runs; ${ runsToInspect } need inspection, ${ runs . length - runsToInspect } passed on the first attempt.` ,
49+ ) ;
4250
43- for ( const run of runs ) {
44- if ( run . conclusion === 'success' && run . run_attempt === 1 ) {
45- continue ;
46- }
51+ async function worker ( ) {
52+ while ( pendingRuns . length > 0 ) {
53+ const run = pendingRuns . shift ( ) ;
54+ core . info ( `Run ${ run . id } : fetching jobs across ${ run . run_attempt } attempt(s).` ) ;
4755
48- const jobs = await github . paginate ( github . rest . actions . listJobsForWorkflowRun , {
49- ...repo ,
50- run_id : run . id ,
51- filter : 'all' ,
52- per_page : 100 ,
53- } ) ;
54- for ( const job of jobs ) {
55- if ( job . conclusion !== 'failure' || job . name . includes ( '(optional)' ) ) {
56- continue ;
57- }
58- failedJobs ++ ;
56+ const jobs = await github . paginate ( github . rest . actions . listJobsForWorkflowRun , {
57+ ...repo ,
58+ run_id : run . id ,
59+ filter : 'all' ,
60+ per_page : 100 ,
61+ } ) ;
62+ const jobsToInspect = jobs . filter ( job => job . conclusion === 'failure' && ! job . name . includes ( '(optional)' ) ) ;
63+ core . info ( `Run ${ run . id } : ${ jobs . length } job attempts, ${ jobsToInspect . length } failures to inspect.` ) ;
64+ for ( const job of jobsToInspect ) {
65+ failedJobs ++ ;
5966
60- let annotations ;
61- try {
62- annotations = await github . paginate ( github . rest . checks . listAnnotations , {
63- ...repo ,
64- check_run_id : Number ( job . check_run_url . split ( '/' ) . pop ( ) ) ,
65- per_page : 100 ,
66- } ) ;
67- } catch ( error ) {
68- if ( error . status !== 404 && error . status !== 410 ) {
69- throw error ;
67+ let annotations ;
68+ try {
69+ annotations = await github . paginate ( github . rest . checks . listAnnotations , {
70+ ...repo ,
71+ check_run_id : Number ( job . check_run_url . split ( '/' ) . pop ( ) ) ,
72+ per_page : 100 ,
73+ } ) ;
74+ } catch ( error ) {
75+ if ( error . status !== 404 && error . status !== 410 ) {
76+ throw error ;
77+ }
78+ warnings . push ( `Annotations unavailable for job ${ job . id } in run ${ run . id } .` ) ;
79+ continue ;
7080 }
71- warnings . push ( `Annotations unavailable for job ${ job . id } in run ${ run . id } .` ) ;
72- continue ;
73- }
7481
75- const failures = annotations . filter (
76- annotation =>
77- annotation . annotation_level === 'failure' &&
78- annotation . title &&
79- / (?: ^ | \/ ) (?: t e s t s ? | _ _ t e s t s _ _ | s u i t e s ) \/ | (?: ^ | [ / . ] ) (?: t e s t | s p e c ) \. [ c m ] ? [ j t ] s x ? $ / i. test ( annotation . path ) ,
80- ) ;
81- if ( failures . length === 0 ) {
82- failedJobsWithoutTests ++ ;
83- }
84-
85- // Match the exact matrix job, not its normalized family, when checking recovery.
86- const recovered = jobs . some (
87- later =>
88- later . name === job . name &&
89- later . head_sha === job . head_sha &&
90- later . run_attempt > job . run_attempt &&
91- later . conclusion === 'success' ,
92- ) ;
93- for ( const annotation of failures ) {
94- const family = jobFamily ( job . name ) ;
95- const name = normalizeTestName ( annotation . title ) ;
96- const key = JSON . stringify ( [ family , annotation . path , name ] ) ;
97- let test = tests . get ( key ) ;
98- if ( ! test ) {
99- test = {
100- family,
101- path : annotation . path ,
102- name,
103- runs : new Set ( ) ,
104- recoveredRuns : new Set ( ) ,
105- examples : new Map ( ) ,
106- } ;
107- tests . set ( key , test ) ;
108- }
109- test . runs . add ( run . id ) ;
110- if ( recovered ) {
111- test . recoveredRuns . add ( run . id ) ;
82+ const failures = annotations . filter (
83+ annotation =>
84+ annotation . annotation_level === 'failure' &&
85+ annotation . title &&
86+ / (?: ^ | \/ ) (?: t e s t s ? | _ _ t e s t s _ _ | s u i t e s ) \/ | (?: ^ | [ / . ] ) (?: t e s t | s p e c ) \. [ c m ] ? [ j t ] s x ? $ / i. test ( annotation . path ) ,
87+ ) ;
88+ if ( failures . length === 0 ) {
89+ failedJobsWithoutTests ++ ;
11290 }
113- if ( test . examples . size < 3 && ! test . examples . has ( run . id ) ) {
114- test . examples . set ( run . id , job . html_url ) ;
91+
92+ // Match the exact matrix job, not its normalized family, when checking recovery.
93+ const recovered = jobs . some (
94+ later =>
95+ later . name === job . name &&
96+ later . head_sha === job . head_sha &&
97+ later . run_attempt > job . run_attempt &&
98+ later . conclusion === 'success' ,
99+ ) ;
100+ for ( const annotation of failures ) {
101+ const family = jobFamily ( job . name ) ;
102+ const name = normalizeTestName ( annotation . title ) ;
103+ const key = JSON . stringify ( [ family , annotation . path , name ] ) ;
104+ let test = tests . get ( key ) ;
105+ if ( ! test ) {
106+ test = {
107+ family,
108+ path : annotation . path ,
109+ name,
110+ runs : new Set ( ) ,
111+ recoveredRuns : new Set ( ) ,
112+ examples : new Map ( ) ,
113+ } ;
114+ tests . set ( key , test ) ;
115+ }
116+ test . runs . add ( run . id ) ;
117+ if ( recovered ) {
118+ test . recoveredRuns . add ( run . id ) ;
119+ }
120+ if ( test . examples . size < 3 && ! test . examples . has ( run . id ) ) {
121+ test . examples . set ( run . id , job . html_url ) ;
122+ }
115123 }
116124 }
125+ completedRuns ++ ;
126+ core . info (
127+ `Completed ${ completedRuns } /${ runsToInspect } runs; ${ failedJobs } failed jobs inspected, ${ tests . size } failing tests found.` ,
128+ ) ;
117129 }
118130 }
131+ await Promise . all ( Array . from ( { length : CONCURRENT_RUNS } , ( ) => worker ( ) ) ) ;
119132
120133 const ranked = [ ...tests . values ( ) ] . sort (
121134 ( a , b ) =>
@@ -187,7 +200,7 @@ export function renderReport(report) {
187200}
188201
189202export default async function run ( { github, context, core } ) {
190- const report = await collectReport ( { github, context } ) ;
203+ const report = await collectReport ( { github, context, core } ) ;
191204 for ( const warning of report . warnings ) {
192205 core . warning ( warning ) ;
193206 }
0 commit comments