/
main.js
279 lines (242 loc) · 7.86 KB
/
main.js
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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
/*! @license
* Shaka Player
* Copyright 2016 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview A workflow tool to maintain GitHub issues.
*/
const core = require('@actions/core');
const { Issue, Milestone } = require('./issues.js');
const TYPE_ACCESSIBILITY = 'type: accessibility';
const TYPE_ANNOUNCEMENT = 'type: announcement';
const TYPE_BUG = 'type: bug';
const TYPE_CI = 'type: CI';
const TYPE_CODE_HEALTH = 'type: code health';
const TYPE_DOCS = 'type: docs';
const TYPE_ENHANCEMENT = 'type: enhancement';
const TYPE_PERFORMANCE = 'type: performance';
const TYPE_PROCESS = 'type: process';
const TYPE_QUESTION = 'type: question';
const PRIORITY_P0 = 'priority: P0';
const PRIORITY_P1 = 'priority: P1';
const PRIORITY_P2 = 'priority: P2';
const PRIORITY_P3 = 'priority: P3';
const PRIORITY_P4 = 'priority: P4';
const STATUS_ARCHIVED = 'status: archived';
const STATUS_WAITING = 'status: waiting on response';
const FLAG_IGNORE = 'flag: bot ignore';
// Issues of these types default to the next milestone. See also
// BACKLOG_PRIORITIES below, which can override the type.
const LABELS_FOR_NEXT_MILESTONE = [
TYPE_ACCESSIBILITY,
TYPE_BUG,
TYPE_DOCS,
];
// Issues of these types default to the backlog.
const LABELS_FOR_BACKLOG = [
TYPE_CI,
TYPE_CODE_HEALTH,
TYPE_ENHANCEMENT,
TYPE_PERFORMANCE,
];
// An issue with one of these priorities will default to the backlog, even if
// it has one of the types in LABELS_FOR_NEXT_MILESTONE.
const BACKLOG_PRIORITIES = [
PRIORITY_P3,
PRIORITY_P4,
];
const PING_QUESTION_TEXT =
'Does this answer all your questions? ' +
'If so, would you please close the issue?';
const CLOSE_STALE_TEXT =
'Closing due to inactivity. If this is still an issue for you or if you ' +
'have further questions, the OP can ask shaka-bot to reopen it by ' +
'including `@shaka-bot reopen` in a comment.';
const PING_INACTIVE_QUESTION_DAYS = 4;
const CLOSE_AFTER_WAITING_DAYS = 7;
const ARCHIVE_AFTER_CLOSED_DAYS = 60;
async function archiveOldIssues(issue) {
// If the issue has been closed for a while, archive it.
// Exclude locked issues, so that this doesn't conflict with unarchiveIssues
// below.
if (!issue.locked && issue.closed &&
issue.closedDays >= ARCHIVE_AFTER_CLOSED_DAYS) {
await issue.addLabel(STATUS_ARCHIVED);
await issue.lock();
}
}
async function unarchiveIssues(issue) {
// If the archive label is removed from an archived issue, unarchive it.
if (issue.locked && !issue.hasLabel(STATUS_ARCHIVED)) {
await issue.unlock();
await issue.reopen();
}
}
async function reopenIssues(issue) {
// If the original author wants an issue reopened, reopen it.
if (issue.closed && !issue.hasLabel(STATUS_ARCHIVED)) {
// Important: only load comments if prior filters pass!
// If we loaded them on every issue, we could exceed our query quota!
await issue.loadComments();
for (const comment of issue.comments) {
body = comment.body.toLowerCase();
if (comment.author == issue.author &&
comment.ageInDays <= issue.closedDays &&
body.includes('@shaka-bot') &&
(body.includes('reopen') || body.includes('re-open'))) {
core.notice(`Found reopen request for issue #${issue.number}`);
await issue.reopen();
break;
}
}
}
}
async function manageWaitingIssues(issue) {
// Filter for waiting issues.
if (!issue.closed && issue.hasLabel(STATUS_WAITING)) {
const labelAgeInDays = await issue.getLabelAgeInDays(STATUS_WAITING);
// If an issue has been replied to, remove the waiting tag.
// Important: only load comments if prior filters pass!
// If we loaded them on every issue, we could exceed our query quota!
await issue.loadComments();
const latestNonTeamComment = issue.comments.find(c => !c.fromTeam);
if (latestNonTeamComment &&
latestNonTeamComment.ageInDays < labelAgeInDays) {
await issue.removeLabel(STATUS_WAITING);
return;
}
// If an issue has been in a waiting state for too long, close it as stale.
if (labelAgeInDays >= CLOSE_AFTER_WAITING_DAYS) {
await issue.postComment(CLOSE_STALE_TEXT);
await issue.close();
}
}
}
async function cleanUpIssueTags(issue) {
// If an issue with the waiting tag was closed, remove the tag.
if (issue.closed && issue.hasLabel(STATUS_WAITING)) {
await issue.removeLabel(STATUS_WAITING);
}
}
async function pingQuestions(issue) {
// If a question hasn't been responded to recently, ping it.
if (!issue.closed &&
issue.hasLabel(TYPE_QUESTION) &&
!issue.hasLabel(STATUS_WAITING)) {
// Important: only load comments if prior filters pass!
// If we loaded them on every issue, we could exceed our query quota!
await issue.loadComments();
// Most recent ones are first.
const lastComment = issue.comments[0];
if (lastComment &&
lastComment.fromTeam &&
// If the last comment was from the team, but not from the OP (in case
// the OP was a member of the team).
lastComment.author != issue.author &&
lastComment.ageInDays >= PING_INACTIVE_QUESTION_DAYS) {
await issue.postComment(`@${issue.author} ${PING_QUESTION_TEXT}`);
await issue.addLabel(STATUS_WAITING);
}
}
}
async function maintainMilestones(issue, nextMilestone, backlog) {
// Set or remove milestones based on type labels.
if (!issue.closed) {
if (issue.hasAnyLabel(LABELS_FOR_NEXT_MILESTONE)) {
if (!issue.milestone) {
// Some (low) priority flags will indicate that an issue should go to
// the backlog, in spite of its type.
if (issue.hasAnyLabel(BACKLOG_PRIORITIES)) {
await issue.setMilestone(backlog);
} else {
await issue.setMilestone(nextMilestone);
}
}
} else if (issue.hasAnyLabel(LABELS_FOR_BACKLOG)) {
if (!issue.milestone) {
await issue.setMilestone(backlog);
}
} else {
if (issue.milestone) {
await issue.removeMilestone();
}
}
}
}
const ALL_ISSUE_TASKS = [
reopenIssues,
archiveOldIssues,
unarchiveIssues,
manageWaitingIssues,
cleanUpIssueTags,
pingQuestions,
maintainMilestones,
];
async function processIssues(issues, nextMilestone, backlog) {
let success = true;
for (const issue of issues) {
if (issue.hasLabel(FLAG_IGNORE)) {
core.info(`Ignoring issue #${issue.number}`);
continue;
}
core.info(`Processing issue #${issue.number}`);
for (const task of ALL_ISSUE_TASKS) {
try {
await task(issue, nextMilestone, backlog);
} catch (error) {
// Make this show up in the Actions UI without needing to search the
// logs.
core.error(
`Failed to process issue #${issue.number} in task ${task.name}: ` +
`${error}\n${error.stack}`);
success = false;
}
}
}
return success;
}
async function main() {
const milestones = await Milestone.getAll();
const issues = await Issue.getAll();
const backlog = milestones.find(m => m.isBacklog());
if (!backlog) {
core.error('No backlog milestone found!');
process.exit(1);
}
milestones.sort(Milestone.compare);
const nextMilestone = milestones[0];
if (nextMilestone.version == null) {
core.error('No version milestone found!');
process.exit(1);
}
const success = await processIssues(issues, nextMilestone, backlog);
if (!success) {
process.exit(1);
}
}
// If this file is the entrypoint, run main. Otherwise, export certain pieces
// to the tests.
if (require.main == module) {
main();
} else {
module.exports = {
processIssues,
TYPE_ACCESSIBILITY,
TYPE_ANNOUNCEMENT,
TYPE_BUG,
TYPE_CODE_HEALTH,
TYPE_DOCS,
TYPE_ENHANCEMENT,
TYPE_PROCESS,
TYPE_QUESTION,
PRIORITY_P0,
PRIORITY_P1,
PRIORITY_P2,
PRIORITY_P3,
PRIORITY_P4,
STATUS_ARCHIVED,
STATUS_WAITING,
FLAG_IGNORE,
};
}