diff --git a/.github/workflows/close-failed-prs.yml b/.github/workflows/close-failed-prs.yml index 8e2bd86a7e20..5d54eea07821 100644 --- a/.github/workflows/close-failed-prs.yml +++ b/.github/workflows/close-failed-prs.yml @@ -24,57 +24,131 @@ jobs: const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - cutoffDays); - const { data: prs } = await github.rest.pulls.list({ - owner: context.repo.owner, - repo: context.repo.repo, - state: 'open', - per_page: 100 - }); - - for (const pr of prs) { - const updated = new Date(pr.updated_at); - if (updated > cutoff) continue; - - const commits = await github.paginate(github.rest.pulls.listCommits, { + console.log(`Checking PRs older than: ${cutoff.toISOString()}`); + + try { + const { data: prs } = await github.rest.pulls.list({ owner: context.repo.owner, repo: context.repo.repo, - pull_number: pr.number, + state: 'open', + sort: 'updated', + direction: 'asc', per_page: 100 }); - const meaningfulCommits = commits.filter(c => { - const msg = c.commit.message.toLowerCase(); - const isMergeFromMain = mainBranches.some(branch => - msg.startsWith(`merge branch '${branch}'`) || - msg.includes(`merge remote-tracking branch '${branch}'`) - ); - return !isMergeFromMain; - }); + console.log(`Found ${prs.length} open PRs to check`); - const { data: checks } = await github.rest.checks.listForRef({ - owner: context.repo.owner, - repo: context.repo.repo, - ref: pr.head.sha - }); + for (const pr of prs) { + try { + const updated = new Date(pr.updated_at); + + if (updated > cutoff) { + console.log(`⏩ Skipping PR #${pr.number} - updated recently`); + continue; + } + + console.log(`🔍 Checking PR #${pr.number}: "${pr.title}"`); + + // Get commits + const commits = await github.paginate(github.rest.pulls.listCommits, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + per_page: 100 + }); + + const meaningfulCommits = commits.filter(c => { + const msg = c.commit.message.toLowerCase(); + const isMergeFromMain = mainBranches.some(branch => + msg.startsWith(`merge branch '${branch}'`) || + msg.includes(`merge remote-tracking branch '${branch}'`) + ); + return !isMergeFromMain; + }); + + // Get checks with error handling + let hasFailedChecks = false; + let allChecksCompleted = false; + let hasChecks = false; + + try { + const { data: checks } = await github.rest.checks.listForRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: pr.head.sha + }); + + hasChecks = checks.check_runs.length > 0; + hasFailedChecks = checks.check_runs.some(c => c.conclusion === 'failure'); + allChecksCompleted = checks.check_runs.every(c => + c.status === 'completed' || c.status === 'skipped' + ); + } catch (error) { + console.log(`⚠️ Could not fetch checks for PR #${pr.number}: ${error.message}`); + } + + // Get workflow runs with error handling + let hasFailedWorkflows = false; + let allWorkflowsCompleted = false; + let hasWorkflows = false; + + try { + const { data: runs } = await github.rest.actions.listWorkflowRuns({ + owner: context.repo.owner, + repo: context.repo.repo, + head_sha: pr.head.sha, + per_page: 50 + }); + + hasWorkflows = runs.workflow_runs.length > 0; + hasFailedWorkflows = runs.workflow_runs.some(r => r.conclusion === 'failure'); + allWorkflowsCompleted = runs.workflow_runs.every(r => + ['completed', 'skipped', 'cancelled'].includes(r.status) + ); + + console.log(`PR #${pr.number}: ${runs.workflow_runs.length} workflow runs found`); + + } catch (error) { + console.log(`⚠️ Could not fetch workflow runs for PR #${pr.number}: ${error.message}`); + } + + console.log(`PR #${pr.number}: ${meaningfulCommits.length} meaningful commits`); + console.log(`Checks - has: ${hasChecks}, failed: ${hasFailedChecks}, completed: ${allChecksCompleted}`); + console.log(`Workflows - has: ${hasWorkflows}, failed: ${hasFailedWorkflows}, completed: ${allWorkflowsCompleted}`); + + // Combine conditions - only consider if we actually have checks/workflows + const hasAnyFailure = (hasChecks && hasFailedChecks) || (hasWorkflows && hasFailedWorkflows); + const allCompleted = (!hasChecks || allChecksCompleted) && (!hasWorkflows || allWorkflowsCompleted); + + if (meaningfulCommits.length === 0 && hasAnyFailure && allCompleted) { + console.log(`✅ Closing PR #${pr.number} (${pr.title})`); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pr.number, + body: `This pull request has been automatically closed because its workflows or checks failed and it has been inactive for more than ${cutoffDays} days. Please fix the workflows and reopen if you'd like to continue. Merging from main/master alone does not count as activity.` + }); + + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pr.number, + state: 'closed' + }); + + console.log(`✅ Successfully closed PR #${pr.number}`); + } else { + console.log(`⏩ Not closing PR #${pr.number} - conditions not met`); + } - const hasFailed = checks.check_runs.some(c => c.conclusion === 'failure'); - const allCompleted = checks.check_runs.every(c => c.status === 'completed'); - - if (meaningfulCommits.length === 0 && hasFailed && allCompleted) { - console.log(`Closing PR #${pr.number} (${pr.title})`); - - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - body: `This pull request has been automatically closed because its workflows failed and it has been inactive for more than ${cutoffDays} days. Please fix the workflows and reopen if you'd like to continue. Merging from main/master alone does not count as activity.` - }); - - await github.rest.pulls.update({ - owner: context.repo.owner, - repo: context.repo.repo, - pull_number: pr.number, - state: 'closed' - }); + } catch (prError) { + console.error(`❌ Error processing PR #${pr.number}: ${prError.message}`); + continue; + } } + + } catch (error) { + console.error(`❌ Fatal error: ${error.message}`); + throw error; } \ No newline at end of file