Skip to content

Commit 3bb42ec

Browse files
authored
Create stale.yml workflow to label stale PRs (#13565)
The behavior is: - If a PR is not labeled stale, after 60 days inactivity label the PR as stale and comment about it. - If a PR is labeled stale, after 30 days inactivity close the PR. - `high priority` and `no-stale` PRs are exempt.
1 parent f90a836 commit 3bb42ec

File tree

1 file changed

+149
-0
lines changed

1 file changed

+149
-0
lines changed

.github/workflows/stale

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# The behavior is:
2+
# - If a PR is not labeled stale, after 60 days inactivity label the PR as stale and comment about it.
3+
# - If a PR is labeled stale, after 30 days inactivity close the PR.
4+
# - `high priority` and `no-stale` PRs are exempt.
5+
6+
name: Close stale pull requests
7+
8+
on:
9+
schedule:
10+
# Run daily at 00:30 UTC.
11+
- cron: '30 0 * * *'
12+
workflow_dispatch:
13+
14+
jobs:
15+
stale:
16+
if: ${{ github.repository == 'pytorch/executorch' }}
17+
runs-on: linux.large
18+
permissions:
19+
contents: read
20+
pull-requests: write
21+
22+
steps:
23+
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
24+
with:
25+
script: |
26+
// Do some dumb retries on requests.
27+
const retries = 7;
28+
const baseBackoff = 100;
29+
const sleep = timeout => new Promise(resolve => setTimeout(resolve, timeout));
30+
github.hook.wrap('request', async (request, options) => {
31+
for (let attempt = 1; attempt <= retries; attempt++) {
32+
try {
33+
return await request(options);
34+
} catch (err) {
35+
if (attempt < retries) {
36+
core.warning(`Request getting retried. Attempt: ${attempt}`);
37+
await sleep(baseBackoff * Math.pow(2, attempt));
38+
continue;
39+
}
40+
throw err;
41+
}
42+
}
43+
});
44+
45+
const MAX_API_REQUESTS = 100;
46+
47+
// If a PRs not labeled stale, label them stale after no update for 60 days.
48+
const STALE_LABEL_THRESHOLD_MS = 1000 * 60 * 60 * 24 * 60;
49+
// For PRs already labeled stale, close after not update for 30 days.
50+
const STALE_CLOSE_THRESHOLD_MS = 1000 * 60 * 60 * 24 * 30;
51+
52+
const STALE_MESSAGE =
53+
"Looks like this PR hasn't been updated in a while so we're going to go ahead and mark this as `Stale`. <br>" +
54+
"Feel free to remove the `Stale` label if you feel this was a mistake. <br>" +
55+
"If you are unable to remove the `Stale` label please contact a maintainer in order to do so. <br>" +
56+
"If you want the bot to never mark this PR stale again, add the `no-stale` label.<br>" +
57+
"`Stale` pull requests will automatically be closed after 30 days of inactivity.<br>";
58+
59+
let numAPIRequests = 0;
60+
let numProcessed = 0;
61+
62+
async function processPull(pull) {
63+
core.info(`[${pull.number}] URL: ${pull.html_url}`);
64+
numProcessed += 1;
65+
const labels = pull.labels.map((label) => label.name);
66+
67+
// Skip if certain labels are present.
68+
if (labels.includes("no-stale") || labels.includes("high priority")) {
69+
core.info(`[${pull.number}] Skipping because PR has an exempting label.`);
70+
return false;
71+
}
72+
73+
// Check if the PR is stale, according to our configured thresholds.
74+
let staleThresholdMillis;
75+
if (labels.includes("Stale")) {
76+
core.info(`[${pull.number}] PR is labeled stale, checking whether we should close it.`);
77+
staleThresholdMillis = STALE_CLOSE_THRESHOLD_MS;
78+
} else {
79+
core.info(`[${pull.number}] Checking whether to label PR as stale.`);
80+
staleThresholdMillis = STALE_LABEL_THRESHOLD_MS;
81+
}
82+
83+
const millisSinceLastUpdated =
84+
new Date().getTime() - new Date(pull.updated_at).getTime();
85+
86+
if (millisSinceLastUpdated < staleThresholdMillis) {
87+
core.info(`[${pull.number}] Skipping because PR was updated recently`);
88+
return false;
89+
}
90+
91+
// At this point, we know we should do something.
92+
// For PRs already labeled stale, close them.
93+
if (labels.includes("Stale")) {
94+
core.info(`[${pull.number}] Closing PR.`);
95+
numAPIRequests += 1;
96+
//await github.rest.issues.update({
97+
//owner: "pytorch",
98+
//repo: "executorch",
99+
//issue_number: pull.number,
100+
//state: "closed",
101+
//});
102+
} else {
103+
// For PRs not labeled stale, label them stale.
104+
core.info(`[${pull.number}] Labeling PR as stale.`);
105+
106+
numAPIRequests += 1;
107+
//await github.rest.issues.createComment({
108+
//owner: "pytorch",
109+
//repo: "executorch",
110+
//issue_number: pull.number,
111+
//body: STALE_MESSAGE,
112+
//});
113+
114+
numAPIRequests += 1;
115+
//await github.rest.issues.addLabels({
116+
//owner: "pytorch",
117+
//repo: "executorch",
118+
//issue_number: pull.number,
119+
//labels: ["Stale"],
120+
//});
121+
}
122+
}
123+
124+
for await (const response of github.paginate.iterator(
125+
github.rest.pulls.list,
126+
{
127+
owner: "pytorch",
128+
repo: "executorch",
129+
state: "open",
130+
sort: "created",
131+
direction: "asc",
132+
per_page: 100,
133+
}
134+
)) {
135+
numAPIRequests += 1;
136+
const pulls = response.data;
137+
// Awaiting in a loop is intentional here. We want to serialize execution so
138+
// that log groups are printed correctl
139+
for (const pull of pulls) {
140+
if (numAPIRequests > MAX_API_REQUESTS) {
141+
core.warning("Max API requests exceeded, exiting.");
142+
process.exit(0);
143+
}
144+
await core.group(`Processing PR #${pull.number}`, async () => {
145+
await processPull(pull);
146+
});
147+
}
148+
}
149+
core.info(`Processed ${numProcessed} PRs total.`);

0 commit comments

Comments
 (0)