Skip to content

Commit ae2e40a

Browse files
Enhance stale PR auto-close to handle failed workflows (#6951)
* chore: add workflow to close stale PRs with failed workflows * Include workflow failures when closing stale PRs
1 parent a4cf6e3 commit ae2e40a

File tree

1 file changed

+119
-45
lines changed

1 file changed

+119
-45
lines changed

.github/workflows/close-failed-prs.yml

Lines changed: 119 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -24,57 +24,131 @@ jobs:
2424
const cutoff = new Date();
2525
cutoff.setDate(cutoff.getDate() - cutoffDays);
2626
27-
const { data: prs } = await github.rest.pulls.list({
28-
owner: context.repo.owner,
29-
repo: context.repo.repo,
30-
state: 'open',
31-
per_page: 100
32-
});
33-
34-
for (const pr of prs) {
35-
const updated = new Date(pr.updated_at);
36-
if (updated > cutoff) continue;
37-
38-
const commits = await github.paginate(github.rest.pulls.listCommits, {
27+
console.log(`Checking PRs older than: ${cutoff.toISOString()}`);
28+
29+
try {
30+
const { data: prs } = await github.rest.pulls.list({
3931
owner: context.repo.owner,
4032
repo: context.repo.repo,
41-
pull_number: pr.number,
33+
state: 'open',
34+
sort: 'updated',
35+
direction: 'asc',
4236
per_page: 100
4337
});
4438
45-
const meaningfulCommits = commits.filter(c => {
46-
const msg = c.commit.message.toLowerCase();
47-
const isMergeFromMain = mainBranches.some(branch =>
48-
msg.startsWith(`merge branch '${branch}'`) ||
49-
msg.includes(`merge remote-tracking branch '${branch}'`)
50-
);
51-
return !isMergeFromMain;
52-
});
39+
console.log(`Found ${prs.length} open PRs to check`);
5340
54-
const { data: checks } = await github.rest.checks.listForRef({
55-
owner: context.repo.owner,
56-
repo: context.repo.repo,
57-
ref: pr.head.sha
58-
});
41+
for (const pr of prs) {
42+
try {
43+
const updated = new Date(pr.updated_at);
44+
45+
if (updated > cutoff) {
46+
console.log(`⏩ Skipping PR #${pr.number} - updated recently`);
47+
continue;
48+
}
49+
50+
console.log(`🔍 Checking PR #${pr.number}: "${pr.title}"`);
51+
52+
// Get commits
53+
const commits = await github.paginate(github.rest.pulls.listCommits, {
54+
owner: context.repo.owner,
55+
repo: context.repo.repo,
56+
pull_number: pr.number,
57+
per_page: 100
58+
});
59+
60+
const meaningfulCommits = commits.filter(c => {
61+
const msg = c.commit.message.toLowerCase();
62+
const isMergeFromMain = mainBranches.some(branch =>
63+
msg.startsWith(`merge branch '${branch}'`) ||
64+
msg.includes(`merge remote-tracking branch '${branch}'`)
65+
);
66+
return !isMergeFromMain;
67+
});
68+
69+
// Get checks with error handling
70+
let hasFailedChecks = false;
71+
let allChecksCompleted = false;
72+
let hasChecks = false;
73+
74+
try {
75+
const { data: checks } = await github.rest.checks.listForRef({
76+
owner: context.repo.owner,
77+
repo: context.repo.repo,
78+
ref: pr.head.sha
79+
});
80+
81+
hasChecks = checks.check_runs.length > 0;
82+
hasFailedChecks = checks.check_runs.some(c => c.conclusion === 'failure');
83+
allChecksCompleted = checks.check_runs.every(c =>
84+
c.status === 'completed' || c.status === 'skipped'
85+
);
86+
} catch (error) {
87+
console.log(`⚠️ Could not fetch checks for PR #${pr.number}: ${error.message}`);
88+
}
89+
90+
// Get workflow runs with error handling
91+
let hasFailedWorkflows = false;
92+
let allWorkflowsCompleted = false;
93+
let hasWorkflows = false;
94+
95+
try {
96+
const { data: runs } = await github.rest.actions.listWorkflowRuns({
97+
owner: context.repo.owner,
98+
repo: context.repo.repo,
99+
head_sha: pr.head.sha,
100+
per_page: 50
101+
});
102+
103+
hasWorkflows = runs.workflow_runs.length > 0;
104+
hasFailedWorkflows = runs.workflow_runs.some(r => r.conclusion === 'failure');
105+
allWorkflowsCompleted = runs.workflow_runs.every(r =>
106+
['completed', 'skipped', 'cancelled'].includes(r.status)
107+
);
108+
109+
console.log(`PR #${pr.number}: ${runs.workflow_runs.length} workflow runs found`);
110+
111+
} catch (error) {
112+
console.log(`⚠️ Could not fetch workflow runs for PR #${pr.number}: ${error.message}`);
113+
}
114+
115+
console.log(`PR #${pr.number}: ${meaningfulCommits.length} meaningful commits`);
116+
console.log(`Checks - has: ${hasChecks}, failed: ${hasFailedChecks}, completed: ${allChecksCompleted}`);
117+
console.log(`Workflows - has: ${hasWorkflows}, failed: ${hasFailedWorkflows}, completed: ${allWorkflowsCompleted}`);
118+
119+
// Combine conditions - only consider if we actually have checks/workflows
120+
const hasAnyFailure = (hasChecks && hasFailedChecks) || (hasWorkflows && hasFailedWorkflows);
121+
const allCompleted = (!hasChecks || allChecksCompleted) && (!hasWorkflows || allWorkflowsCompleted);
122+
123+
if (meaningfulCommits.length === 0 && hasAnyFailure && allCompleted) {
124+
console.log(`✅ Closing PR #${pr.number} (${pr.title})`);
125+
126+
await github.rest.issues.createComment({
127+
owner: context.repo.owner,
128+
repo: context.repo.repo,
129+
issue_number: pr.number,
130+
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.`
131+
});
132+
133+
await github.rest.pulls.update({
134+
owner: context.repo.owner,
135+
repo: context.repo.repo,
136+
pull_number: pr.number,
137+
state: 'closed'
138+
});
139+
140+
console.log(`✅ Successfully closed PR #${pr.number}`);
141+
} else {
142+
console.log(`⏩ Not closing PR #${pr.number} - conditions not met`);
143+
}
59144
60-
const hasFailed = checks.check_runs.some(c => c.conclusion === 'failure');
61-
const allCompleted = checks.check_runs.every(c => c.status === 'completed');
62-
63-
if (meaningfulCommits.length === 0 && hasFailed && allCompleted) {
64-
console.log(`Closing PR #${pr.number} (${pr.title})`);
65-
66-
await github.rest.issues.createComment({
67-
owner: context.repo.owner,
68-
repo: context.repo.repo,
69-
issue_number: pr.number,
70-
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.`
71-
});
72-
73-
await github.rest.pulls.update({
74-
owner: context.repo.owner,
75-
repo: context.repo.repo,
76-
pull_number: pr.number,
77-
state: 'closed'
78-
});
145+
} catch (prError) {
146+
console.error(`❌ Error processing PR #${pr.number}: ${prError.message}`);
147+
continue;
148+
}
79149
}
150+
151+
} catch (error) {
152+
console.error(`❌ Fatal error: ${error.message}`);
153+
throw error;
80154
}

0 commit comments

Comments
 (0)