Skip to content

Commit b04f40c

Browse files
authored
ci: do not retry on user cancel (#18603)
1 parent da7285f commit b04f40c

File tree

1 file changed

+117
-15
lines changed

1 file changed

+117
-15
lines changed

.github/scripts/retry_failed_jobs.js

Lines changed: 117 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
712
function 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(/## 🤖 Smart Auto-retry Analysis\s*(?:\(Retry (\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
356398
Higher 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

Comments
 (0)