|
70 | 70 | // Use run_number as tiebreaker since created_at might be identical for rapid reruns. |
71 | 71 | // Note: If workflows are manually re-run out of order, we use the highest run_number |
72 | 72 | // which represents the most recent attempt, regardless of trigger order. |
| 73 | + // |
| 74 | + // IMPORTANT: We need to fetch the latest attempt for each run, not just the run conclusion. |
| 75 | + // GitHub marks a run as "failed" even if a rerun succeeded, so we must check the actual |
| 76 | + // latest attempt to see if the failure was resolved. |
73 | 77 | const latestByWorkflow = new Map(); |
74 | 78 | for (const run of workflowRuns) { |
75 | 79 | const existing = latestByWorkflow.get(run.workflow_id); |
@@ -100,19 +104,50 @@ runs: |
100 | 104 | return; |
101 | 105 | } |
102 | 106 |
|
103 | | - // Check for workflows that failed on the previous commit. |
104 | | - // We treat these conclusions as failures: |
105 | | - // - 'failure': Obvious failure case |
106 | | - // - 'timed_out': Infrastructure or performance issue that should be investigated |
107 | | - // - 'cancelled': Might indicate timeout, CI infrastructure issues, or manual intervention needed |
108 | | - // Being conservative here prevents a green checkmark when the previous commit |
109 | | - // might have real issues that weren't fully validated |
110 | | - // - 'action_required': Requires manual intervention |
111 | | - // We treat 'skipped' and 'neutral' as non-blocking since they indicate |
112 | | - // intentional skips or informational-only workflows. |
113 | | - const failingRuns = Array.from(latestByWorkflow.values()).filter((run) => { |
114 | | - return ['failure', 'timed_out', 'cancelled', 'action_required'].includes(run.conclusion); |
115 | | - }); |
| 107 | + // For each workflow run, fetch the jobs to check the latest attempt's conclusion. |
| 108 | + // GitHub's run.conclusion reflects the overall run, but if a run was re-run and succeeded, |
| 109 | + // we want to consider that success, not the original failure. |
| 110 | + const failingRuns = []; |
| 111 | +
|
| 112 | + for (const run of Array.from(latestByWorkflow.values())) { |
| 113 | + // Fetch jobs for this run to check the latest attempt |
| 114 | + const jobsResponse = await github.rest.actions.listJobsForWorkflowRun({ |
| 115 | + owner: context.repo.owner, |
| 116 | + repo: context.repo.repo, |
| 117 | + run_id: run.id, |
| 118 | + per_page: 100 |
| 119 | + }); |
| 120 | +
|
| 121 | + const jobs = jobsResponse.data.jobs; |
| 122 | +
|
| 123 | + if (jobs.length === 0) { |
| 124 | + // No jobs found - treat as incomplete |
| 125 | + failingRuns.push(run); |
| 126 | + continue; |
| 127 | + } |
| 128 | +
|
| 129 | + // Get the maximum run_attempt number to find the latest attempt |
| 130 | + const latestAttempt = Math.max(...jobs.map(job => job.run_attempt)); |
| 131 | +
|
| 132 | + // Get all jobs from the latest attempt |
| 133 | + const latestJobs = jobs.filter(job => job.run_attempt === latestAttempt); |
| 134 | +
|
| 135 | + // Check if any job in the latest attempt has failed |
| 136 | + // We treat these conclusions as failures: |
| 137 | + // - 'failure': Obvious failure case |
| 138 | + // - 'timed_out': Infrastructure or performance issue that should be investigated |
| 139 | + // - 'cancelled': Might indicate timeout, CI infrastructure issues, or manual intervention needed |
| 140 | + // - 'action_required': Requires manual intervention |
| 141 | + // We treat 'skipped' and 'neutral' as non-blocking since they indicate |
| 142 | + // intentional skips or informational-only workflows. |
| 143 | + const hasFailedJob = latestJobs.some(job => |
| 144 | + ['failure', 'timed_out', 'cancelled', 'action_required'].includes(job.conclusion) |
| 145 | + ); |
| 146 | +
|
| 147 | + if (hasFailedJob) { |
| 148 | + failingRuns.push(run); |
| 149 | + } |
| 150 | + } |
116 | 151 |
|
117 | 152 | if (failingRuns.length === 0) { |
118 | 153 | core.info(`Previous master commit ${previousSha} completed without failures. Docs-only skip allowed.`); |
|
0 commit comments