Skip to content

Commit 2e562ce

Browse files
feat(github-actions): add stale draft PR action
This adds a new behavior to the lock-closed local-action. It automatically closes draft PRs with no activity from the PR author over the past 4 weeks.
1 parent 3a72c83 commit 2e562ce

2 files changed

Lines changed: 227 additions & 1 deletion

File tree

.github/local-actions/lock-closed/lib/main.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ async function main() {
4040
const github = new Octokit({auth: token});
4141
for (let repo of reposToBeChecked) {
4242
await runLockClosedAction(github, repo);
43+
await closeStaleDraftPrs(github, repo);
4344
}
4445
} catch (error: any) {
4546
// TODO(josephperrott): properly set typings for error.
@@ -53,6 +54,129 @@ async function main() {
5354
}
5455
}
5556

57+
async function closeStaleDraftPrs(github: Octokit, repo: string): Promise<void> {
58+
const days = 28;
59+
const message =
60+
"This draft PR is being closed as has been stale for 4 weeks and has seen no activity from you. If you'd like to see this change land, please open a new pull request with these changes. Thank you for being an Angular contributor!";
61+
62+
const threshold = new Date();
63+
threshold.setDate(threshold.getDate() - days);
64+
const thresholdIso = threshold.toISOString();
65+
const thresholdDateString = thresholdIso.split('T')[0];
66+
67+
const repositoryName = `angular/${repo}`;
68+
const query = `repo:${repositoryName}+is:pr+is:draft+is:open+created:<${thresholdDateString}+sort:updated-asc`;
69+
console.info('Stale Draft PR Query: ' + query);
70+
71+
let closeCount = 0;
72+
const prResponse = await github.search.issuesAndPullRequests({
73+
q: query,
74+
per_page: 100,
75+
});
76+
77+
console.info(`Stale Draft PR Query found ${prResponse.data.total_count} items`);
78+
79+
if (!prResponse.data.items.length) {
80+
console.info(`No draft PRs to close`);
81+
return;
82+
}
83+
84+
console.info(`Attempting to close up to ${prResponse.data.items.length} draft PR(s)`);
85+
core.startGroup('Closing stale draft PRs');
86+
87+
for (const item of prResponse.data.items) {
88+
if (!item.pull_request) continue;
89+
90+
try {
91+
const pr = await github.pulls.get({
92+
owner: 'angular',
93+
repo,
94+
pull_number: item.number,
95+
});
96+
97+
if (!pr.data.draft || pr.data.state !== 'open') {
98+
console.info(`Skipping PR #${item.number}, no longer an open draft`);
99+
continue;
100+
}
101+
102+
if (new Date(pr.data.updated_at) < threshold) {
103+
console.info(
104+
`PR #${item.number} has had no activity from anyone since ${pr.data.updated_at}, closing immediately.`,
105+
);
106+
} else {
107+
const author = pr.data.user.login;
108+
109+
const commit = await github.repos.getCommit({
110+
owner: 'angular',
111+
repo,
112+
ref: pr.data.head.sha,
113+
});
114+
115+
const commitDate = new Date(
116+
commit.data.commit.author?.date || commit.data.commit.committer?.date || 0,
117+
);
118+
if (commitDate > threshold) {
119+
console.info(`Skipping PR #${item.number}, recent commit found from ${commitDate}`);
120+
continue;
121+
}
122+
123+
const comments = await github.issues.listComments({
124+
owner: 'angular',
125+
repo,
126+
issue_number: item.number,
127+
since: thresholdIso,
128+
per_page: 100,
129+
});
130+
131+
if (comments.data.some((c) => c.user?.login === author)) {
132+
console.info(`Skipping PR #${item.number}, recent issue comment found from author`);
133+
continue;
134+
}
135+
136+
const reviewComments = await github.pulls.listReviewComments({
137+
owner: 'angular',
138+
repo,
139+
pull_number: item.number,
140+
since: thresholdIso,
141+
per_page: 100,
142+
});
143+
144+
if (reviewComments.data.some((c) => c.user?.login === author)) {
145+
console.info(`Skipping PR #${item.number}, recent review comment found from author`);
146+
continue;
147+
}
148+
}
149+
150+
console.info(`Closing stale draft PR #${item.number}`);
151+
152+
await github.issues.createComment({
153+
owner: 'angular',
154+
repo,
155+
issue_number: item.number,
156+
body: message,
157+
});
158+
159+
await github.pulls.update({
160+
owner: 'angular',
161+
repo,
162+
pull_number: item.number,
163+
state: 'closed',
164+
});
165+
166+
await setTimeoutPromise(250);
167+
++closeCount;
168+
} catch (error: any) {
169+
core.warning(`Unable to close draft PR angular/${repo}#${item.number}: ${error.message}`);
170+
if (typeof error.request === 'object') {
171+
core.error(JSON.stringify(error.request, null, 2));
172+
}
173+
}
174+
}
175+
176+
core.endGroup();
177+
console.info(`Closed ${closeCount} stale draft PR(s)`);
178+
}
179+
56180
async function runLockClosedAction(github: Octokit, repo: string): Promise<void> {
57181
// NOTE: `days` and `message` must not be changed without dev-rel and dev-infra concurrence
58182

.github/local-actions/lock-closed/main.js

Lines changed: 103 additions & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)