blob: db723e5623fa750101c9baa4fdd7a9df30830264 [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 { getChecksStatus } = require("./shared/checks");
const commentStrings = require("./shared/commentStrings");
const { ReviewerConfig } = require("./shared/reviewerConfig");
const { PersistentState } = require("./shared/persistentState");
const { Pr } = require("./shared/pr");
const {
REPO_OWNER,
REPO,
PATH_TO_CONFIG_FILE,
REVIEWERS_ACTION,
} = require("./shared/constants");
import { CheckStatus } from "./shared/checks";
/*
* Returns true if the pr needs to be processed or false otherwise.
* We don't need to process PRs that:
* 1) Have WIP in their name
* 2) Are less than 20 minutes old
* 3) Are draft prs
* 4) Are closed
* 5) Have already been processed
* 6) Have notifications stopped
* 8) The pr happens after the date we turn on the automation. TODO(damccorm) - remove this once this has been rolled out for a while.
* unless we're supposed to remind the user after tests pass
* (in which case that's all we need to do).
*/
function needsProcessed(pull: any, prState: typeof Pr): boolean {
const firstPythonPrToProcess = new Date(2022, 5, 16, 14); // June 16 2022, 14:00 UTC (note that JavaScript months are 0 indexed)
const firstPrToProcess = new Date(2022, 6, 15, 23); // July 15 2022, 23:00 UTC (note that JavaScript months are 0 indexed)
const createdAt = new Date(pull.created_at);
if (
createdAt < firstPrToProcess &&
!pull.labels.find((label) => label.name.toLowerCase() === "go")
) {
if (
createdAt < firstPythonPrToProcess ||
!pull.labels.find((label) => label.name.toLowerCase() === "python")
) {
console.log(
`Skipping PR ${pull.number} because it was created at ${createdAt}, before the first pr to process date of ${firstPrToProcess}`
);
return false;
}
}
if (prState.remindAfterTestsPass && prState.remindAfterTestsPass.length > 0) {
return true;
}
if (pull.title.toLowerCase().indexOf("wip") >= 0) {
console.log(`Skipping PR ${pull.number} because it is a WIP`);
return false;
}
let timeCutoff = new Date(new Date().getTime() - 20 * 60000);
if (new Date(pull.created_at) > timeCutoff) {
console.log(
`Skipping PR ${pull.number} because it was created less than 20 minutes ago`
);
return false;
}
if (pull.state.toLowerCase() !== "open") {
console.log(`Skipping PR ${pull.number} because it is closed`);
return false;
}
if (pull.draft) {
console.log(`Skipping PR ${pull.number} because it is a draft`);
return false;
}
if (prState.stopReviewerNotifications) {
console.log(
`Skipping PR ${pull.number} because reviewer notifications have been stopped`
);
return false;
}
return true;
}
/*
* If the checks passed in via checkstate have completed, notifies the users who have configured notifications.
*/
async function remindIfChecksCompleted(
pull: any,
stateClient: typeof PersistentState,
checkState: CheckStatus,
prState: typeof Pr
) {
console.log(
`Notifying reviewers if checks for PR ${pull.number} have completed, then returning`
);
if (!checkState.completed) {
return;
}
if (checkState.succeeded) {
await github.addPrComment(
pull.number,
commentStrings.allChecksPassed(prState.remindAfterTestsPass)
);
} else {
await github.addPrComment(
pull.number,
commentStrings.someChecksFailing(prState.remindAfterTestsPass)
);
}
prState.remindAfterTestsPass = [];
await stateClient.writePrState(pull.number, prState);
}
/*
* If we haven't already, let the author know checks are failing.
*/
async function notifyChecksFailed(
pull: any,
stateClient: typeof PersistentState,
prState: typeof Pr
) {
console.log(
`Checks are failing for PR ${pull.number}. Commenting if we haven't already and skipping.`
);
if (!prState.commentedAboutFailingChecks) {
await github.addPrComment(
pull.number,
commentStrings.failingChecksCantAssign()
);
}
prState.commentedAboutFailingChecks = true;
await stateClient.writePrState(pull.number, prState);
}
async function approvedBy(pull: any): Promise<string[]> {
const reviews = await github.getGitHubClient().rest.pulls.listReviews({
owner: REPO_OWNER,
repo: REPO,
pull_number: pull.number,
});
return reviews.data
.filter((review) => review.state == "APPROVED")
.map((review) => review.user.login);
}
/*
* Performs all the business logic of processing a new pull request, including:
* 1) Checking if it needs processed
* 2) Reminding reviewers if checks have completed (if they've subscribed to that)
* 3) Picking/assigning reviewers
* 4) Adding "Next Action: Reviewers label"
* 5) Storing the state of the pull request/reviewers in a dedicated branch.
*/
async function processPull(
pull: any,
reviewerConfig: typeof ReviewerConfig,
stateClient: typeof PersistentState
) {
let prState = await stateClient.getPrState(pull.number);
if (!needsProcessed(pull, prState)) {
return;
}
console.log(`Processing PR ${pull.number}`);
// If reviewers are already assigned, we just need to check if we should assign a committer.
if (Object.keys(prState.reviewersAssignedForLabels).length > 0) {
if (prState.committerAssigned) {
console.log(
`Skipping PR ${pull.number} because a committer has been assigned`
);
return;
}
const approvers = await approvedBy(pull);
if (!approvers || approvers.length == 0) {
console.log(
`Skipping PR ${pull.number} because reviewers are assigned but haven't approved`
);
return;
}
// TODO(https://github.com/apache/beam/issues/21417) - also check if the author is a committer, if they are don't auto-assign a committer
for (const approver of approvers) {
const labelOfReviewer = prState.getLabelForReviewer(approver);
if (labelOfReviewer) {
if (
(await github.checkIfCommitter(pull.user.login)) ||
(await prState.isAnyAssignedReviewerCommitter()) ||
(await github.checkIfCommitter(approver))
) {
console.log(
"Author or reviewer is committer, not forwarding to another committer"
);
// Cache this result so we don't need to keep looking it up.
prState.committerAssigned = true;
await stateClient.writePrState(pull.number, prState);
return;
}
console.log(`Assigning a committer for label ${labelOfReviewer}`);
let reviewersState = await stateClient.getReviewersForLabelState(
labelOfReviewer
);
const availableReviewers =
reviewerConfig.getReviewersForLabel(labelOfReviewer);
const chosenCommitter = await reviewersState.assignNextCommitter(
availableReviewers
);
prState.reviewersAssignedForLabels[labelOfReviewer] = chosenCommitter;
prState.committerAssigned = true;
// Set next action to committer
await github.addPrComment(
pull.number,
commentStrings.assignCommitter(chosenCommitter)
);
await github.nextActionReviewers(pull.number, pull.labels);
prState.nextAction = REVIEWERS_ACTION;
// Persist state
await stateClient.writePrState(pull.number, prState);
await stateClient.writeReviewersForLabelState(
labelOfReviewer,
reviewersState
);
return;
}
}
// If none of the approvers were assigned to the pr, no-op.
return;
}
let checkState = await getChecksStatus(REPO_OWNER, REPO, pull.head.sha);
if (prState.remindAfterTestsPass && prState.remindAfterTestsPass.length > 0) {
return await remindIfChecksCompleted(
pull,
stateClient,
checkState,
prState
);
}
if (!checkState.succeeded) {
if (!checkState.completed) {
return;
}
return await notifyChecksFailed(pull, stateClient, prState);
}
prState.commentedAboutFailingChecks = false;
// Pick reviewers to assign. Store them in reviewerStateToUpdate and update the prState object with those reviewers (and their associated labels)
let reviewerStateToUpdate: { [key: string]: typeof ReviewersForLabel } = {};
const reviewersForLabels: { [key: string]: string[] } =
reviewerConfig.getReviewersForLabels(pull.labels, [pull.user.login]);
var labels = Object.keys(reviewersForLabels);
if (!labels || labels.length === 0) {
return;
}
for (const label of labels) {
let availableReviewers = reviewersForLabels[label];
let reviewersState = await stateClient.getReviewersForLabelState(label);
let chosenReviewer = reviewersState.assignNextReviewer(availableReviewers);
reviewerStateToUpdate[label] = reviewersState;
prState.reviewersAssignedForLabels[label] = chosenReviewer;
}
console.log(`Assigning reviewers for PR ${pull.number}`);
await github.addPrComment(
pull.number,
commentStrings.assignReviewer(prState.reviewersAssignedForLabels)
);
github.nextActionReviewers(pull.number, pull.labels);
prState.nextAction = "Reviewers";
await stateClient.writePrState(pull.number, prState);
let labelsToUpdate = Object.keys(reviewerStateToUpdate);
for (const label of labelsToUpdate) {
await stateClient.writeReviewersForLabelState(
label,
reviewerStateToUpdate[label]
);
}
}
async function processNewPrs() {
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);
}
}
processNewPrs();
export {};