@@ -4,6 +4,11 @@ function isRetryableError(errorMessage) {
44 errorMessage . includes ( 'The operation was canceled.' ) ;
55}
66
7+ function isUserCancelled ( errorMessage ) {
8+ if ( ! errorMessage ) return false ;
9+ return errorMessage . includes ( 'The run was canceled by @' ) ;
10+ }
11+
712function isPriorityCancelled ( errorMessage ) {
813 if ( ! errorMessage ) return false ;
914 return errorMessage . includes ( 'Canceling since a higher priority waiting request for' ) ;
@@ -21,6 +26,18 @@ async function analyzeJob(github, context, core, job) {
2126
2227 core . info ( ` Job status: ${ jobDetails . status } , conclusion: ${ jobDetails . conclusion } ` ) ;
2328
29+ // Check if job was cancelled by user based on conclusion
30+ if ( jobDetails . conclusion === 'cancelled' ) {
31+ core . info ( ` ⛔️ Job "${ job . name } " is NOT retryable - cancelled (likely by user)` ) ;
32+ return {
33+ job,
34+ retryable : false ,
35+ annotationCount : 0 ,
36+ reason : 'Cancelled by user' ,
37+ priorityCancelled : false
38+ } ;
39+ }
40+
2441 const { data : annotations } = await github . rest . checks . listAnnotations ( {
2542 owner : context . repo . owner ,
2643 repo : context . repo . repo ,
@@ -61,6 +78,18 @@ async function analyzeJob(github, context, core, job) {
6178 } ;
6279 }
6380
81+ const userCancelled = isUserCancelled ( allFailureAnnotationMessages ) ;
82+ if ( userCancelled ) {
83+ core . info ( ` ⛔️ Job "${ job . name } " is NOT retryable - cancelled by user` ) ;
84+ return {
85+ job,
86+ retryable : false ,
87+ annotationCount : annotations . length ,
88+ reason : 'Cancelled by user' ,
89+ priorityCancelled : false
90+ } ;
91+ }
92+
6493 const isRetryable = isRetryableError ( allFailureAnnotationMessages ) ;
6594 if ( isRetryable ) {
6695 core . info ( ` ✅ Job "${ job . name } " is retryable - infrastructure issue detected in annotations` ) ;
@@ -297,24 +326,37 @@ async function findExistingRetryComment(github, context, core, pr) {
297326 }
298327}
299328
300- function getRetryCount ( existingComment ) {
301- if ( ! existingComment ) return 0 ;
329+ async function getRetryCountFromAPI ( github , context , core , workflowRun ) {
330+ try {
331+ // Get workflow runs for the same branch/commit
332+ const { data : workflowRuns } = await github . rest . actions . listWorkflowRuns ( {
333+ owner : context . repo . owner ,
334+ repo : context . repo . repo ,
335+ workflow_id : workflowRun . workflow_id ,
336+ branch : workflowRun . head_branch ,
337+ per_page : 100
338+ } ) ;
302339
303- // Try to extract retry count from the title
304- const titleMatch = existingComment . body . match ( / # # 🤖 S m a r t A u t o - r e t r y A n a l y s i s \s * (?: \( R e t r y ( \d + ) \) ) ? / ) ;
305- if ( titleMatch && titleMatch [ 1 ] ) {
306- return parseInt ( titleMatch [ 1 ] , 10 ) ;
307- }
340+ // Count runs that were retries (have the same head_sha)
341+ let retryCount = 0 ;
342+ const currentSha = workflowRun . head_sha ;
308343
309- // If no retry count in title, check if it's a retry by looking for retry indicators
310- if ( existingComment . body . includes ( '### ✅ **AUTO-RETRY INITIATED**' ) ) {
311- return 1 ; // This is likely the first retry
312- }
344+ for ( const run of workflowRuns . workflow_runs ) {
345+ if ( run . head_sha === currentSha && run . id !== workflowRun . id ) {
346+ // This is a previous run with the same commit SHA
347+ retryCount ++ ;
348+ }
349+ }
313350
314- return 0 ;
351+ core . info ( `Found ${ retryCount } previous workflow runs for the same commit` ) ;
352+ return retryCount ;
353+ } catch ( error ) {
354+ core . warning ( `Failed to get retry count from API: ${ error . message } ` ) ;
355+ return 0 ;
356+ }
315357}
316358
317- async function addCommentToPR ( github , context , core , runID , runURL , jobData , priorityCancelled ) {
359+ async function addCommentToPR ( github , context , core , runID , runURL , jobData , priorityCancelled , maxRetriesReached = false ) {
318360 try {
319361 // Get workflow run to find the branch
320362 const { data : workflowRun } = await github . rest . actions . getWorkflowRun ( {
@@ -334,8 +376,8 @@ async function addCommentToPR(github, context, core, runID, runURL, jobData, pri
334376 // Try to find existing retry comment
335377 const existingComment = await findExistingRetryComment ( github , context , core , pr ) ;
336378
337- // Get current retry count
338- const currentRetryCount = getRetryCount ( existingComment ) ;
379+ // Get current retry count from API
380+ const currentRetryCount = await getRetryCountFromAPI ( github , context , core , workflowRun ) ;
339381 const newRetryCount = jobData . retryableJobsCount > 0 ? currentRetryCount + 1 : currentRetryCount ;
340382
341383 // Build title with retry count
@@ -356,6 +398,53 @@ async function addCommentToPR(github, context, core, runID, runURL, jobData, pri
356398Higher priority request detected - retry cancelled to avoid conflicts.
357399
358400[View Workflow](${ runURL } )` ;
401+ } else if ( maxRetriesReached ) {
402+ // Comment for when max retries reached
403+ comment = `## 🤖 Smart Auto-retry Analysis${ titleSuffix }
404+
405+ > **Workflow:** [\`${ runID } \`](${ runURL } )
406+
407+ ### 📊 Summary
408+ - **Total Jobs:** ${ jobData . totalJobs }
409+ - **Failed Jobs:** ${ jobData . failedJobs . length }
410+ - **Retryable:** ${ jobData . retryableJobsCount }
411+ - **Code Issues:** ${ codeIssuesCount }
412+
413+ ### 🚫 **MAX RETRIES REACHED**
414+ Maximum retry count (3) has been reached. Manual intervention required.
415+
416+ [View Workflow](${ runURL } )` ;
417+
418+ comment += `
419+
420+ ### 🔍 Job Details
421+ ${ jobData . analyzedJobs . map ( job => {
422+ if ( job . reason . includes ( 'Analysis failed' ) ) {
423+ return `- ❓ **${ job . name } **: Analysis failed` ;
424+ }
425+ if ( job . reason . includes ( 'Cancelled by higher priority' ) ) {
426+ return `- ⛔️ **${ job . name } **: Cancelled by higher priority` ;
427+ }
428+ if ( job . reason . includes ( 'Cancelled by user' ) ) {
429+ return `- 🚫 **${ job . name } **: Cancelled by user` ;
430+ }
431+ if ( job . reason . includes ( 'No annotations found' ) ) {
432+ return `- ❓ **${ job . name } **: No annotations available` ;
433+ }
434+ if ( job . retryable ) {
435+ return `- 🔄 **${ job . name } **: ✅ Retryable (Infrastructure)` ;
436+ } else {
437+ return `- ❌ **${ job . name } **: Not retryable (Code/Test)` ;
438+ }
439+ } ) . join ( '\n' ) }
440+
441+ ---
442+
443+ <details>
444+ <summary>🤖 About</summary>
445+
446+ Automated analysis using job annotations to distinguish infrastructure issues (auto-retried) from code/test issues (manual fixes needed).
447+ </details>` ;
359448 } else {
360449 // Full comment for normal analysis
361450 comment = `## 🤖 Smart Auto-retry Analysis${ titleSuffix }
@@ -392,6 +481,9 @@ ${jobData.analyzedJobs.map(job => {
392481 if ( job . reason . includes ( 'Cancelled by higher priority' ) ) {
393482 return `- ⛔️ **${ job . name } **: Cancelled by higher priority` ;
394483 }
484+ if ( job . reason . includes ( 'Cancelled by user' ) ) {
485+ return `- 🚫 **${ job . name } **: Cancelled by user` ;
486+ }
395487 if ( job . reason . includes ( 'No annotations found' ) ) {
396488 return `- ❓ **${ job . name } **: No annotations available` ;
397489 }
@@ -500,6 +592,16 @@ module.exports = async ({ github, context, core }) => {
500592 return ;
501593 }
502594
595+ // Check retry count limit (max 3 retries)
596+ const currentRetryCount = await getRetryCountFromAPI ( github , context , core , workflowRun ) ;
597+ const maxRetries = 3 ;
598+
599+ if ( currentRetryCount >= maxRetries ) {
600+ core . info ( `Maximum retry count (${ maxRetries } ) reached. Skipping retry.` ) ;
601+ await addCommentToPR ( github , context , core , runID , runURL , { failedJobs, analyzedJobs, totalJobs, retryableJobsCount : 0 } , false , true ) ;
602+ return ;
603+ }
604+
503605 // Handle no retryable jobs
504606 if ( jobsToRetry . length === 0 ) {
505607 core . info ( 'No jobs found with retryable errors. Skipping retry.' ) ;
0 commit comments