| /* |
| * 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 {}; |