@@ -13,13 +13,11 @@ const {
1313 RETRIES ,
1414} = process . env
1515
16- const maxRerunFailedJobs = 3
17-
1816const octokit = new Octokit ( { auth : GITHUB_TOKEN } )
1917const owner = 'DataDog'
2018const repo = 'dd-trace-js'
2119const ref = context . payload . pull_request ?. head . sha || GITHUB_SHA
22- const params = { owner , repo , ref }
20+
2321const conclusionEmojis = {
2422 action_required : '🔶' ,
2523 cancelled : '🚫' ,
@@ -42,179 +40,154 @@ const conclusionSeverity = {
4240 success : 7 ,
4341}
4442
45- let retries = 0
46- let hasRerun = false
47-
48- async function hasCompleted ( ) {
49- const { data : inProgressRuns } = await octokit . rest . checks . listForRef ( {
50- ...params ,
51- per_page : 1 , // Minimum is 1 but we don't need any pages.
52- status : 'in_progress' ,
53- } )
54-
55- // If there are any in progress runs it means we're not ready to check
56- // statuses. We will always have minimum 1 for the All Green job.
57- if ( inProgressRuns . total_count > 1 ) return false
43+ const failureConclusions = new Set ( [ 'failure' , 'timed_out' ] )
5844
59- const { data : queuedRuns } = await octokit . rest . checks . listForRef ( {
60- ...params ,
61- per_page : 1 , // Minimum is 1 but we don't need any pages.
62- status : 'queued' ,
63- } )
64-
65- // Same as above, but jobs that are queued are not even in progress yet.
66- if ( queuedRuns . total_count > 0 ) return false
67-
68- return true
69- }
45+ let retries = 0
46+ const retriedRunIds = new Set ( )
7047
71- async function checkCompleted ( ) {
72- if ( ! await hasCompleted ( ) ) {
73- retries ++
48+ // ETag cache for the workflow-runs poll. GitHub returns 304 Not Modified when
49+ // the response is unchanged, and 304 responses don't count against the rate
50+ // limit. workflow_runs are sorted newest first, so an unchanged first page is
51+ // a reliable proxy for "no changes since last poll".
52+ let runsCache
7453
75- if ( RETRIES && retries > RETRIES ) {
76- throw new Error ( `State is still pending after ${ RETRIES } retries.` )
54+ async function getRuns ( ) {
55+ try {
56+ const allRuns = [ ]
57+ let etag
58+ for await ( const { data, headers } of octokit . paginate . iterator (
59+ octokit . rest . actions . listWorkflowRunsForRepo ,
60+ {
61+ owner,
62+ repo,
63+ head_sha : ref ,
64+ per_page : 100 ,
65+ headers : runsCache ? { 'if-none-match' : runsCache . etag } : { } ,
66+ }
67+ ) ) {
68+ etag ??= headers . etag
69+ allRuns . push ( ...data )
7770 }
78-
79- console . log ( `Status is still pending, waiting for ${ POLLING_INTERVAL } minutes before retrying.` )
80- await setTimeout ( POLLING_INTERVAL * 60_000 )
81- console . log ( 'Retrying.' )
82- await checkCompleted ( )
71+ // Isolate per trigger so a parallel all-green run on the same SHA doesn't
72+ // see our runs (and we don't see theirs). Filter by event, by PR number
73+ // when on a PR (handles two PRs sharing the same head commit), and drop
74+ // our own All Green run since it stays in_progress while we poll.
75+ const myPR = context . payload . pull_request ?. number
76+ const filtered = allRuns . filter ( r =>
77+ r . name !== context . workflow &&
78+ r . event === context . eventName &&
79+ ( myPR == null || r . pull_requests ?. some ( pr => pr . number === myPR ) )
80+ )
81+ runsCache = { etag, runs : filtered }
82+ return filtered
83+ } catch ( err ) {
84+ if ( err . status === 304 && runsCache ) return runsCache . runs
85+ throw err
8386 }
8487}
8588
86- async function getLatestRuns ( ) {
87- const checkRuns = await octokit . paginate (
88- 'GET /repos/:owner/:repo/commits/:ref/check-runs' ,
89- {
90- ...params ,
91- per_page : 100 ,
92- }
93- )
89+ async function pollUntilDone ( ) {
90+ const runs = await getRuns ( )
9491
95- // When a check is re-run, older runs remain with their original conclusions.
96- // Deduplicate by name and evaluate only the latest run for each check.
97- const latestByName = new Map ( )
98- for ( const run of checkRuns ) {
99- const existing = latestByName . get ( run . name )
100- if ( ! existing || new Date ( run . started_at ) >= new Date ( existing . started_at ) ) {
101- latestByName . set ( run . name , run )
102- }
103- }
104-
105- return [ ...latestByName . values ( ) ]
106- }
92+ const toRetry = runs . filter ( r =>
93+ r . status === 'completed' &&
94+ failureConclusions . has ( r . conclusion ) &&
95+ ! retriedRunIds . has ( r . id )
96+ )
10797
108- async function rerunFailedWorkflows ( failedRuns ) {
109- const failedCountByCheckSuiteId = new Map ( )
110- for ( const run of failedRuns ) {
111- const id = run . check_suite ?. id
112- if ( id !== undefined ) {
113- failedCountByCheckSuiteId . set ( id , ( failedCountByCheckSuiteId . get ( id ) ?? 0 ) + 1 )
114- }
98+ if ( toRetry . length > 0 ) {
99+ await rerunFailedWorkflows ( toRetry )
100+ for ( const run of toRetry ) retriedRunIds . add ( run . id )
101+ runsCache = undefined
115102 }
116103
117- const eligibleSuiteIds = [ ...failedCountByCheckSuiteId . entries ( ) ]
118- . filter ( ( [ , count ] ) => count <= maxRerunFailedJobs )
119- . map ( ( [ id ] ) => id )
104+ const pending = runs . filter ( r => r . status !== 'completed' ) . length
105+ if ( pending === 0 && toRetry . length === 0 ) return { runs, done : true }
120106
121- // If a workflow has many jobs failed, it's unlikely to be flakiness to no
122- // point in re-running.
123- if ( eligibleSuiteIds . length < failedCountByCheckSuiteId . size ) {
124- console . log (
125- `Skipping rerun for ${ failedCountByCheckSuiteId . size - eligibleSuiteIds . length } workflow(s) ` +
126- `with more than ${ maxRerunFailedJobs } failed job(s).`
127- )
128- }
107+ retries ++
129108
130- const workflowRunsPerSuite = await Promise . all (
131- eligibleSuiteIds . map ( checkSuiteId =>
132- octokit . rest . actions . listWorkflowRunsForRepo ( { owner, repo, check_suite_id : checkSuiteId } )
133- . then ( ( { data } ) => data . workflow_runs )
134- )
135- )
109+ if ( RETRIES && retries > RETRIES ) return { runs, done : false }
136110
137- const workflowRuns = workflowRunsPerSuite . flat ( )
111+ console . log ( `Status is still pending, waiting for ${ POLLING_INTERVAL } minutes before retrying.` )
112+ await setTimeout ( POLLING_INTERVAL * 60_000 )
113+ console . log ( 'Retrying.' )
114+ return pollUntilDone ( )
115+ }
138116
117+ async function rerunFailedWorkflows ( workflowRuns ) {
139118 await Promise . all (
140119 workflowRuns . map ( workflowRun => {
141120 console . log ( `Rerunning failed jobs for workflow run ${ workflowRun . id } (${ workflowRun . name } ).` )
142121 return octokit . rest . actions . reRunWorkflowFailedJobs ( { owner, repo, run_id : workflowRun . id } )
143122 } )
144123 )
145-
146- return workflowRuns . length > 0
147124}
148125
149126async function checkAllGreen ( ) {
150- let latestRuns
127+ const { runs , done } = await pollUntilDone ( )
151128
152- try {
153- await checkCompleted ( )
154- } finally {
155- latestRuns = await getLatestRuns ( )
129+ await printSummary ( runs )
130+
131+ if ( ! done ) {
132+ console . log ( `State is still pending after ${ RETRIES } retries.` )
133+ process . exitCode = 1
134+ return
156135 }
157136
158- const failedRuns = latestRuns . filter ( run =>
159- run . conclusion === 'failure' || run . conclusion === 'timed_out'
160- )
137+ const failedRuns = runs . filter ( r => failureConclusions . has ( r . conclusion ) )
161138
162139 if ( failedRuns . length === 0 ) {
163- await printSummary ( latestRuns )
164140 console . log ( 'All jobs were successful.' )
165- return
141+ } else {
142+ console . log ( 'One or more jobs failed.' )
143+ process . exitCode = 1
166144 }
145+ }
167146
168- if ( ! hasRerun ) {
169- hasRerun = true
170- console . log ( `${ failedRuns . length } job(s) failed. Rerunning failed workflows...` )
171- const didRerun = await rerunFailedWorkflows ( failedRuns )
172- if ( didRerun ) {
173- retries = 0
174- console . log ( `Waiting for ${ POLLING_INTERVAL } minutes before polling for rerun results.` )
175- await setTimeout ( POLLING_INTERVAL * 60_000 )
176- await checkAllGreen ( )
177- return
178- }
179- }
147+ function formatConclusion ( conclusion ) {
148+ return conclusion ? `${ conclusion } ${ conclusionEmojis [ conclusion ] } ` : ' '
149+ }
180150
181- await printSummary ( latestRuns )
182- throw new Error ( 'One or more jobs failed.' )
151+ function bySeverity ( a , b ) {
152+ return ( conclusionSeverity [ a . conclusion ] ?? 8 ) - ( conclusionSeverity [ b . conclusion ] ?? 8 )
183153}
184154
185- async function printSummary ( checkRuns ) {
186- const runs = [ ... checkRuns ]
187- . sort ( ( a , b ) => ( conclusionSeverity [ a . conclusion ] ?? 8 ) - ( conclusionSeverity [ b . conclusion ] ?? 8 ) )
155+ async function printSummary ( runs ) {
156+ const rows = runs
157+ . sort ( bySeverity )
188158 . map ( run => ( {
189159 name : run . name ,
190160 status : run . status ,
191- conclusion : run . conclusion
192- ? `${ run . conclusion } ${ conclusionEmojis [ run . conclusion ] } `
193- : ' ' ,
194- started_at : run . started_at ,
195- completed_at : run . completed_at ?? ' ' ,
161+ conclusion : formatConclusion ( run . conclusion ) ,
162+ // workflow_run has no completed_at; updated_at reflects the final state
163+ // change once status === 'completed', otherwise it's an in-flight tick.
164+ started_at : run . run_started_at ,
165+ completed_at : run . status === 'completed' ? run . updated_at : ' ' ,
166+ url : run . html_url ,
196167 } ) )
197168
198- console . table ( runs )
169+ // console.table can't render HTML, so the raw URL goes here as its own
170+ // column. The GitHub Actions summary below renders the name as a link.
171+ console . table ( rows )
199172
200173 const header = [
201- { data : 'name ' , header : true } ,
174+ { data : 'workflow ' , header : true } ,
202175 { data : 'status' , header : true } ,
203176 { data : 'conclusion' , header : true } ,
204177 { data : 'started_at' , header : true } ,
205178 { data : 'completed_at' , header : true } ,
206179 ]
207180
208- const body = runs . map ( run => [
209- run . name ,
210- run . status ,
211- run . conclusion ,
212- run . started_at ,
213- run . completed_at ,
181+ const body = rows . map ( row => [
182+ `<a href=" ${ row . url } "> ${ row . name } </a>` ,
183+ row . status ,
184+ row . conclusion ,
185+ row . started_at ,
186+ row . completed_at ,
214187 ] )
215188
216189 await summary
217- . addHeading ( 'Checks Summary' )
190+ . addHeading ( 'Workflows Summary' )
218191 . addTable ( [ header , ...body ] )
219192 . write ( )
220193}
0 commit comments