| /* | |
| * 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, | |
| REASSIGNED_REVIEWERS_LABEL, | |
| AWAITING_TRIAGE_LABEL, | |
| NEXT_ACTION_REVIEWERS_LABEL, | |
| AUTHOR_ACTION, | |
| } = require("./shared/constants"); | |
| const { hasLabel } = github; | |
| const ONE_DAY_MS = 24 * 60 * 60 * 1000; | |
| 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_LABEL)) { | |
| 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 (comment.user.login !== pullAuthor && comment.user.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 (comment.user.login !== pullAuthor && comment.user.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: string[] = Object.values( | |
| prState.reviewersAssignedForLabels | |
| ) as string[]; | |
| if (pull.requested_reviewers) { | |
| reviewersToExclude = reviewersToExclude.concat( | |
| pull.requested_reviewers.map((r: any) => r.login) | |
| ); | |
| } | |
| reviewersToExclude.push(pull.user.login); | |
| const reviewersForLabels: { [key: string]: string[] } = | |
| reviewerConfig.getReviewersForLabels(labelObjects, reviewersToExclude); | |
| const fallbackReviewers = reviewerConfig.getFallbackReviewers(); | |
| 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, | |
| fallbackReviewers | |
| ); | |
| 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, { | |
| labels: pull.labels, | |
| }) | |
| ); | |
| await github.requestPrReviewers( | |
| pull.number, | |
| Object.values(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, AWAITING_TRIAGE_LABEL)) { | |
| console.log(`Skipping PR ${pull.number} - awaiting triage`); | |
| return; | |
| } | |
| if (pull.draft) { | |
| if (hasLabel(pull, NEXT_ACTION_REVIEWERS_LABEL)) { | |
| await github.nextActionAuthor(pull.number, pull.labels); | |
| prState.nextAction = AUTHOR_ACTION; | |
| await stateClient.writePrState(pull.number, prState); | |
| } | |
| console.log(`Skipping PR ${pull.number} - draft`); | |
| return; | |
| } | |
| const sixtyDaysAgo = new Date(Date.now() - 60 * ONE_DAY_MS); | |
| const initialReviewDate = prState.reviewersAssignedAt | |
| ? new Date(prState.reviewersAssignedAt) | |
| : new Date(pull.created_at); | |
| if ( | |
| hasLabel(pull, REASSIGNED_REVIEWERS_LABEL) && | |
| hasLabel(pull, NEXT_ACTION_REVIEWERS_LABEL) && | |
| initialReviewDate.getTime() < sixtyDaysAgo.getTime() | |
| ) { | |
| console.log( | |
| `PR ${pull.number} has reassigned-reviewers and Next Action: Reviewers labels and review started >60 days ago - adding awaiting triage label` | |
| ); | |
| await github.getGitHubClient().rest.issues.addLabels({ | |
| owner: REPO_OWNER, | |
| repo: REPO, | |
| issue_number: pull.number, | |
| labels: [AWAITING_TRIAGE_LABEL], | |
| }); | |
| return; | |
| } | |
| if ( | |
| hasLabel(pull, SLOW_REVIEW_LABEL) && | |
| hasLabel(pull, NEXT_ACTION_REVIEWERS_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, | |
| }); | |
| await github.getGitHubClient().rest.issues.addLabels({ | |
| owner: REPO_OWNER, | |
| repo: REPO, | |
| issue_number: pull.number, | |
| labels: [REASSIGNED_REVIEWERS_LABEL], | |
| }); | |
| } | |
| return; | |
| } | |
| if (await isSlowReview(pull)) { | |
| const client = github.getGitHubClient(); | |
| let reviewersToPing = Object.values( | |
| prState.reviewersAssignedForLabels || {} | |
| ); | |
| if (pull.requested_reviewers) { | |
| reviewersToPing = reviewersToPing.concat( | |
| pull.requested_reviewers.map((r: any) => r.login) | |
| ); | |
| } | |
| reviewersToPing = [...new Set(reviewersToPing as string[])]; | |
| if (reviewersToPing.length > 0) { | |
| console.log( | |
| `Flagging pr ${pull.number} as slow. Tagging reviewers ${reviewersToPing}` | |
| ); | |
| await github.addPrComment( | |
| pull.number, | |
| commentStrings.slowReview(reviewersToPing) | |
| ); | |
| 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); | |
| } | |
| await stateClient.deleteStalePrStates(openPulls, 100); | |
| } | |
| processOldPrs(); | |
| export {}; |