mirror of
https://github.com/shaka-project/shaka-player.git
synced 2026-06-17 16:26:39 +03:00
d1f00b897c
This ports over internal tests suggested in #3889.
280 lines
7.9 KiB
JavaScript
280 lines
7.9 KiB
JavaScript
/*! @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,
|
|
};
|
|
}
|