Skip to content

Commit 45db7c0

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 45db7c0

2 files changed

Lines changed: 209 additions & 1 deletion

File tree

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

Lines changed: 116 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,121 @@ 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:created-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+
const author = pr.data.user.login;
103+
104+
const commit = await github.repos.getCommit({
105+
owner: 'angular',
106+
repo,
107+
ref: pr.data.head.sha,
108+
});
109+
110+
const commitDate = new Date(
111+
commit.data.commit.author?.date || commit.data.commit.committer?.date || 0,
112+
);
113+
if (commitDate > threshold) {
114+
console.info(`Skipping PR #${item.number}, recent commit found from ${commitDate}`);
115+
continue;
116+
}
117+
118+
const comments = await github.issues.listComments({
119+
owner: 'angular',
120+
repo,
121+
issue_number: item.number,
122+
since: thresholdIso,
123+
});
124+
125+
if (comments.data.some((c) => c.user?.login === author)) {
126+
console.info(`Skipping PR #${item.number}, recent issue comment found from author`);
127+
continue;
128+
}
129+
130+
const reviewComments = await github.pulls.listReviewComments({
131+
owner: 'angular',
132+
repo,
133+
pull_number: item.number,
134+
since: thresholdIso,
135+
});
136+
137+
if (reviewComments.data.some((c) => c.user?.login === author)) {
138+
console.info(`Skipping PR #${item.number}, recent review comment found from author`);
139+
continue;
140+
}
141+
142+
console.info(`Closing stale draft PR #${item.number}`);
143+
144+
await github.issues.createComment({
145+
owner: 'angular',
146+
repo,
147+
issue_number: item.number,
148+
body: message,
149+
});
150+
151+
await github.pulls.update({
152+
owner: 'angular',
153+
repo,
154+
pull_number: item.number,
155+
state: 'closed',
156+
});
157+
158+
await setTimeoutPromise(250);
159+
++closeCount;
160+
} catch (error: any) {
161+
core.warning(`Unable to close draft PR angular/${repo}#${item.number}: ${error.message}`);
162+
if (typeof error.request === 'object') {
163+
core.error(JSON.stringify(error.request, null, 2));
164+
}
165+
}
166+
}
167+
168+
core.endGroup();
169+
console.info(`Closed ${closeCount} stale draft PR(s)`);
170+
}
171+
56172
async function runLockClosedAction(github: Octokit, repo: string): Promise<void> {
57173
// NOTE: `days` and `message` must not be changed without dev-rel and dev-infra concurrence
58174

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

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

0 commit comments

Comments
 (0)