| /* | |
| * 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("@actions/github"); | |
| const commentStrings = require("./shared/commentStrings"); | |
| const { processCommand } = require("./shared/userCommand"); | |
| const { | |
| addPrComment, | |
| getGitHubClient, | |
| nextActionAuthor, | |
| nextActionReviewers, | |
| getPullAuthorFromPayload, | |
| getPullNumberFromPayload, | |
| } = require("./shared/githubUtils"); | |
| const { processPull } = require("./processNewPrs"); | |
| const { PersistentState } = require("./shared/persistentState"); | |
| const { ReviewerConfig } = require("./shared/reviewerConfig"); | |
| const { | |
| BOT_NAME, | |
| PATH_TO_CONFIG_FILE, | |
| REPO_OWNER, | |
| REPO, | |
| SLOW_REVIEW_LABEL, | |
| REVIEWERS_ACTION, | |
| AUTHOR_ACTION, | |
| } = require("./shared/constants"); | |
| // Removes the slow label if the pr has been reviewed and returns an updated payload. | |
| async function removeSlowReviewLabel(payload: any) { | |
| let labels = payload.issue?.labels || payload.pull_request?.labels; | |
| if (labels.some((label) => label.name.toLowerCase() == SLOW_REVIEW_LABEL)) { | |
| const pullNumber = payload.issue?.number || payload.pull_request?.number; | |
| labels = ( | |
| await getGitHubClient().rest.issues.removeLabel({ | |
| owner: REPO_OWNER, | |
| repo: REPO, | |
| issue_number: pullNumber, | |
| name: "slow-review", | |
| }) | |
| ).data; | |
| if (payload.issues) { | |
| payload.issues.labels = labels; | |
| } | |
| if (payload.pull_request) { | |
| payload.pull_request.labels = labels; | |
| } | |
| } | |
| } | |
| async function areReviewersAssigned( | |
| pull: any, | |
| stateClient: typeof PersistentState | |
| ): Promise<boolean> { | |
| const prState = await stateClient.getPrState(pull.number); | |
| if (Object.values(prState.reviewersAssignedForLabels).length > 0) { | |
| return true; | |
| } | |
| if (pull.requested_reviewers && pull.requested_reviewers.length > 0) { | |
| return true; | |
| } | |
| return false; | |
| } | |
| async function processPrComment( | |
| payload: any, | |
| pull: any, | |
| stateClient: typeof PersistentState, | |
| reviewerConfig: typeof ReviewerConfig | |
| ) { | |
| const commentContents = payload.comment.body; | |
| const commentAuthor = payload.sender.login; | |
| const pullAuthor = getPullAuthorFromPayload(payload); | |
| console.log(commentContents); | |
| const processedCommand = await processCommand( | |
| payload, | |
| commentAuthor, | |
| commentContents, | |
| stateClient, | |
| reviewerConfig | |
| ); | |
| // Check to see if notifications have been stopped before processing further. | |
| // Notifications can be stopped by an "R: reviewer" comment, | |
| // and then restarted by adding "assign set of reviewers" comment. | |
| if ( | |
| (await stateClient.getPrState(getPullNumberFromPayload(payload))) | |
| .stopReviewerNotifications | |
| ) { | |
| console.log("Notifications have been paused for this pull - skipping"); | |
| return; | |
| } | |
| // If there's been a comment by a non-author, we can remove the slow review label | |
| if (commentAuthor !== pullAuthor && commentAuthor !== BOT_NAME) { | |
| await removeSlowReviewLabel(payload); | |
| } | |
| if (processedCommand) { | |
| // If we've processed a command, don't worry about trying to change the attention set. | |
| // This is not a meaningful push or comment from the author. | |
| console.log("Processed command"); | |
| return; | |
| } | |
| // If comment was from the author, we should shift attention back to the reviewers. | |
| console.log( | |
| "No command to be processed, checking if we should shift attention to reviewers" | |
| ); | |
| if (pullAuthor === commentAuthor) { | |
| await setNextActionReviewers(payload, pull, stateClient); | |
| } else { | |
| console.log( | |
| `Comment was from ${commentAuthor}, not author: ${pullAuthor}. No action to take.` | |
| ); | |
| } | |
| } | |
| /* | |
| * On pr push, ready_for_review, or author comment, we should put the attention set back on the reviewers | |
| */ | |
| async function setNextActionReviewers( | |
| payload: any, | |
| pull: any, | |
| stateClient: typeof PersistentState | |
| ) { | |
| if (pull.draft) { | |
| console.log("PR is a draft, not shifting attention to reviewers"); | |
| return; | |
| } | |
| if (!(await areReviewersAssigned(pull, stateClient))) { | |
| console.log("No reviewers assigned, dont need to manipulate attention set"); | |
| return; | |
| } | |
| const existingLabels = | |
| pull.labels || payload.issue?.labels || payload.pull_request?.labels; | |
| await nextActionReviewers(pull.number, existingLabels); | |
| let prState = await stateClient.getPrState(pull.number); | |
| prState.nextAction = REVIEWERS_ACTION; | |
| await stateClient.writePrState(pull.number, prState); | |
| } | |
| /* | |
| * When a PR is marked as draft, set the next action state to Author | |
| */ | |
| async function setNextActionAuthor( | |
| payload: any, | |
| pull: any, | |
| stateClient: typeof PersistentState | |
| ) { | |
| const existingLabels = | |
| pull.labels || payload.issue?.labels || payload.pull_request?.labels; | |
| await nextActionAuthor(pull.number, existingLabels); | |
| let prState = await stateClient.getPrState(pull.number); | |
| prState.nextAction = AUTHOR_ACTION; | |
| await stateClient.writePrState(pull.number, prState); | |
| } | |
| async function processPrUpdate() { | |
| const reviewerConfig = new ReviewerConfig(PATH_TO_CONFIG_FILE); | |
| const context = github.context; | |
| console.log("Event context:"); | |
| console.log(context); | |
| const payload = context.payload; | |
| if (!payload.issue?.pull_request && !payload.pull_request) { | |
| console.log("Issue, not pull request - returning"); | |
| return; | |
| } | |
| const pullNumber = getPullNumberFromPayload(payload); | |
| const stateClient = new PersistentState(); | |
| switch (github.context.eventName) { | |
| case "issue_comment": { | |
| console.log("Processing comment event"); | |
| if (payload.action !== "created") { | |
| console.log("Comment wasnt just created, skipping"); | |
| return; | |
| } | |
| const pull = ( | |
| await getGitHubClient().rest.pulls.get({ | |
| owner: REPO_OWNER, | |
| repo: REPO, | |
| pull_number: pullNumber, | |
| }) | |
| ).data; | |
| await processPrComment(payload, pull, stateClient, reviewerConfig); | |
| break; | |
| } | |
| case "pull_request_target": { | |
| if ( | |
| (await stateClient.getPrState(pullNumber)).stopReviewerNotifications | |
| ) { | |
| console.log("Notifications have been paused for this pull - skipping"); | |
| return; | |
| } | |
| const pull = ( | |
| await getGitHubClient().rest.pulls.get({ | |
| owner: REPO_OWNER, | |
| repo: REPO, | |
| pull_number: pullNumber, | |
| }) | |
| ).data; | |
| if (payload.action === "synchronize") { | |
| console.log("Processing synchronize action"); | |
| await setNextActionReviewers(payload, pull, stateClient); | |
| } else if (payload.action === "review_requested") { | |
| console.log("Processing review_requested action"); | |
| await setNextActionReviewers(payload, pull, stateClient); | |
| } else if (payload.action === "converted_to_draft") { | |
| console.log("Processing converted_to_draft action"); | |
| await setNextActionAuthor(payload, pull, stateClient); | |
| } else if (payload.action === "ready_for_review") { | |
| console.log("Processing ready_for_review action"); | |
| // If reviewers are already assigned, shift attention back to them. | |
| // Otherwise, try to assign initial reviewers immediately (e.g. when a draft | |
| // PR with passing checks is marked ready for review) instead of waiting for cron. | |
| if (await areReviewersAssigned(pull, stateClient)) { | |
| await setNextActionReviewers(payload, pull, stateClient); | |
| } else { | |
| await processPull(pull, reviewerConfig, stateClient); | |
| } | |
| } | |
| // TODO(damccorm) - it would be good to eventually handle the following events here, even though they're not part of the normal workflow | |
| // review requested, assigned, label added, label removed | |
| break; | |
| } | |
| default: | |
| console.log("Not a PR comment or push, doing nothing"); | |
| } | |
| } | |
| processPrUpdate(); | |
| export {}; |