blob: 621e89b2d87d5717bbcc4664b3285a8842e0bcff [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const github = require("./shared/githubUtils");
const commentStrings = require("./shared/commentStrings");
const { ReviewerConfig } = require("./shared/reviewerConfig");
const { PersistentState } = require("./shared/persistentState");
const {
BOT_NAME,
REPO_OWNER,
REPO,
PATH_TO_CONFIG_FILE,
SLOW_REVIEW_LABEL,
} = require("./shared/constants");
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
function hasLabel(pull: any, labelName: string): boolean {
return pull.labels.some(
(label) => label.name.toLowerCase() === labelName.toLowerCase()
);
}
function getTwoWeekdaysAgo(): Date {
const twoWeekDaysAgo = new Date(Date.now() - 2 * ONE_DAY_MS);
const currentDay = new Date(Date.now()).getDay();
// If Saturday, sunday, monday, or tuesday, add extra time to account for weekend.
if (currentDay === 6) {
twoWeekDaysAgo.setDate(twoWeekDaysAgo.getDate() - 1);
} else if (currentDay <= 2) {
twoWeekDaysAgo.setDate(twoWeekDaysAgo.getDate() - 2);
}
return twoWeekDaysAgo;
}
async function isSlowReview(pull: any): Promise<boolean> {
if (!hasLabel(pull, "Next Action: Reviewers")) {
return false;
}
const lastModified = new Date(pull.updated_at);
const sevenDaysAgo = new Date(Date.now() - 7 * ONE_DAY_MS);
if (lastModified.getTime() < sevenDaysAgo.getTime()) {
return true;
}
// If it has no comments from a non-bot/author in the last 2 weekdays, return true
const twoWeekDaysAgo = getTwoWeekdaysAgo();
if (lastModified.getTime() >= twoWeekDaysAgo.getTime()) {
return false;
}
const pullAuthor = pull.user.login;
const githubClient = github.getGitHubClient();
const comments = (
await githubClient.rest.issues.listComments({
owner: REPO_OWNER,
repo: REPO,
issue_number: pull.number,
})
).data;
for (const comment of comments) {
if (
comments.some(
({ user: login }) => login !== pullAuthor && login !== BOT_NAME
)
) {
return false;
}
}
const reviewComments = (
await githubClient.rest.pulls.listReviewComments({
owner: REPO_OWNER,
repo: REPO,
pull_number: pull.number,
})
).data;
for (const comment of reviewComments) {
if (
comments.some(
({ user: login }) => login !== pullAuthor && login !== BOT_NAME
)
) {
return false;
}
}
return true;
}
async function assignToNewReviewers(
pull: any,
reviewerConfig: typeof ReviewerConfig,
stateClient: typeof PersistentState
) {
let prState = await stateClient.getPrState(pull.number);
let reviewerStateToUpdate = {};
const labelObjects = pull.labels;
let reviewersToExclude = Object.values(prState.reviewersAssignedForLabels);
reviewersToExclude.push(pull.user.login);
const reviewersForLabels: { [key: string]: string[] } =
reviewerConfig.getReviewersForLabels(labelObjects, reviewersToExclude);
for (const labelObject of labelObjects) {
const label = labelObject.name;
let availableReviewers = reviewersForLabels[label];
if (availableReviewers && availableReviewers.length > 0) {
let reviewersState = await stateClient.getReviewersForLabelState(label);
let chosenReviewer = await reviewersState.assignNextCommitter(
availableReviewers
);
reviewerStateToUpdate[label] = reviewersState;
prState.reviewersAssignedForLabels[label] = chosenReviewer;
}
}
console.log(`Assigning new reviewers for pr ${pull.number}`);
await github.addPrComment(
pull.number,
commentStrings.assignNewReviewer(prState.reviewersAssignedForLabels)
);
await stateClient.writePrState(pull.number, prState);
let labelsToUpdate = Object.keys(reviewerStateToUpdate);
for (const label of labelsToUpdate) {
await stateClient.writeReviewersForLabelState(
label,
reviewerStateToUpdate[label]
);
}
}
// Flag any prs that have been awaiting reviewer action at least 7 days,
// or have been awaiting initial review for at least 2 days and ping the reviewers
// If there's still no action after 2 more days, assign a new set of committers.
async function processPull(
pull: any,
reviewerConfig: typeof ReviewerConfig,
stateClient: typeof PersistentState
) {
const prState = await stateClient.getPrState(pull.number);
if (prState.stopReviewerNotifications) {
console.log(`Skipping PR ${pull.number} - notifications silenced`);
return;
}
if (hasLabel(pull, SLOW_REVIEW_LABEL)) {
const lastModified = new Date(pull.updated_at);
const twoWeekDaysAgo = getTwoWeekdaysAgo();
console.log(
`PR ${pull.number} has the slow review label. Last modified ${lastModified}`
);
if (lastModified.getTime() < twoWeekDaysAgo.getTime()) {
console.log(
`PR ${pull.number} still awaiting action - assigning new reviewers`
);
await assignToNewReviewers(pull, reviewerConfig, stateClient);
await github.getGitHubClient().rest.issues.removeLabel({
owner: REPO_OWNER,
repo: REPO,
issue_number: pull.number,
name: SLOW_REVIEW_LABEL,
});
}
return;
}
if (await isSlowReview(pull)) {
const client = github.getGitHubClient();
const currentReviewers = prState.reviewersAssignedForLabels;
if (currentReviewers && Object.values(currentReviewers).length > 0) {
console.log(
`Flagging pr ${pull.number} as slow. Tagging reviewers ${currentReviewers}`
);
await github.addPrComment(
pull.number,
commentStrings.slowReview(Object.values(currentReviewers))
);
await client.rest.issues.addLabels({
owner: REPO_OWNER,
repo: REPO,
issue_number: pull.number,
labels: [SLOW_REVIEW_LABEL],
});
}
}
}
async function processOldPrs() {
const githubClient = github.getGitHubClient();
let reviewerConfig = new ReviewerConfig(PATH_TO_CONFIG_FILE);
let stateClient = new PersistentState();
let openPulls = await githubClient.paginate(
"GET /repos/{owner}/{repo}/pulls",
{
owner: REPO_OWNER,
repo: REPO,
}
);
for (const pull of openPulls) {
await processPull(pull, reviewerConfig, stateClient);
}
}
processOldPrs();
export {};