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