KonradJanica commented on a change in pull request #16950:
URL: https://github.com/apache/beam/pull/16950#discussion_r815230131



##########
File path: scripts/ci/pr-bot/.gitignore
##########
@@ -0,0 +1,41 @@
+lib-cov
+*.seed
+*.log
+*.csv
+*.dat
+*.out
+*.pid
+*.gz
+*.swp
+
+pids
+logs
+results
+tmp
+
+# Build
+public/css/main.css

Review comment:
       Why is this css file ignored? It seems a bit strange that only a single 
css file is built.

##########
File path: scripts/ci/pr-bot/processNewPrs.ts
##########
@@ -0,0 +1,220 @@
+/*
+ * 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 } = 
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
+// 7) The pr doesn't contain the go label (temporary). TODO(damccorm) - remove 
this when we're ready to roll this out to everyone.
+// unless we're supposed to remind the user after tests pass
+// (in which case that's all we need to do).
+function needsProcessed(pull, prState: typeof Pr): boolean {
+  if (!pull.labels.find((label) => label.name.toLowerCase() == "go")) {

Review comment:
       1. Use triple equals for strict string comparison. 
   2. Single quotation marks are more standard than double. That being said, be 
consistent with whatever is more common in the repo.

##########
File path: scripts/ci/pr-bot/processNewPrs.ts
##########
@@ -0,0 +1,220 @@
+/*
+ * 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 } = 
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
+// 7) The pr doesn't contain the go label (temporary). TODO(damccorm) - remove 
this when we're ready to roll this out to everyone.
+// unless we're supposed to remind the user after tests pass
+// (in which case that's all we need to do).
+function needsProcessed(pull, prState: typeof Pr): boolean {
+  if (!pull.labels.find((label) => label.name.toLowerCase() == "go")) {
+    console.log(
+      `Skipping PR ${pull.number} because it doesn't contain the go label`
+    );
+    return false;
+  }
+  if (prState.remindAfterTestsPass && prState.remindAfterTestsPass.length > 0) 
{

Review comment:
       `prState.remindAfterTestsPass?.length > 0`

##########
File path: scripts/ci/pr-bot/processNewPrs.ts
##########
@@ -0,0 +1,220 @@
+/*
+ * 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 } = 
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
+// 7) The pr doesn't contain the go label (temporary). TODO(damccorm) - remove 
this when we're ready to roll this out to everyone.
+// unless we're supposed to remind the user after tests pass
+// (in which case that's all we need to do).
+function needsProcessed(pull, prState: typeof Pr): boolean {
+  if (!pull.labels.find((label) => label.name.toLowerCase() == "go")) {
+    console.log(
+      `Skipping PR ${pull.number} because it doesn't contain the go label`
+    );
+    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 (Object.keys(prState.reviewersAssignedForLabels).length > 0) {
+    console.log(
+      `Skipping PR ${pull.number} because it already has been assigned`
+    );
+    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,
+  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) {
+    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,
+  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);
+}
+
+// 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,
+  reviewerConfig: typeof ReviewerConfig,
+  stateClient: typeof PersistentState
+) {
+  let prState = await stateClient.getPrState(pull.number);
+  if (!needsProcessed(pull, prState)) {
+    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) {
+    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 = {};

Review comment:
       Missing type. This will not transpile under typescript [strict 
mode](https://www.byte.ski/blog/article/what-is-strict-mode-in-typescript-and-why-and-when-you-should-use-it).

##########
File path: scripts/ci/pr-bot/processNewPrs.ts
##########
@@ -0,0 +1,220 @@
+/*
+ * 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 } = 
require("./shared/constants");
+import { CheckStatus } from "./shared/checks";
+
+// Returns true if the pr needs to be processed or false otherwise.

Review comment:
       Typescript uses [JSDoc](https://jsdoc.app/about-getting-started.html) 
style comments for functions, i.e. asterisks are preferred over '//' 

##########
File path: scripts/ci/pr-bot/processNewPrs.ts
##########
@@ -0,0 +1,220 @@
+/*
+ * 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 } = 
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
+// 7) The pr doesn't contain the go label (temporary). TODO(damccorm) - remove 
this when we're ready to roll this out to everyone.
+// unless we're supposed to remind the user after tests pass
+// (in which case that's all we need to do).
+function needsProcessed(pull, prState: typeof Pr): boolean {
+  if (!pull.labels.find((label) => label.name.toLowerCase() == "go")) {
+    console.log(
+      `Skipping PR ${pull.number} because it doesn't contain the go label`
+    );
+    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 (Object.keys(prState.reviewersAssignedForLabels).length > 0) {
+    console.log(
+      `Skipping PR ${pull.number} because it already has been assigned`
+    );
+    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,
+  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) {

Review comment:
       Unnest this with an early return.

##########
File path: scripts/ci/pr-bot/processPrUpdate.ts
##########
@@ -0,0 +1,198 @@
+/*
+ * 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, nextActionReviewers } = require("./shared/githubUtils");
+const { PersistentState } = require("./shared/persistentState");
+const { ReviewerConfig } = require("./shared/reviewerConfig");
+const { PATH_TO_CONFIG_FILE } = require("./shared/constants");
+
+async function areReviewersAssigned(
+  pullNumber: number,
+  stateClient: typeof PersistentState
+): Promise<boolean> {
+  const prState = await stateClient.getPrState(pullNumber);
+  return Object.values(prState.reviewersAssignedForLabels).length > 0;
+}
+
+async function processPrComment(
+  payload,
+  stateClient: typeof PersistentState,
+  reviewerConfig: typeof ReviewerConfig
+) {
+  const commentContents = payload.comment.body;
+  const commentAuthor = payload.sender.login;
+  console.log(commentContents);
+  if (
+    await processCommand(
+      payload,
+      commentAuthor,
+      commentContents,
+      stateClient,
+      reviewerConfig
+    )
+  ) {
+    // 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"
+  );
+  const pullAuthor =
+    payload.issue?.user?.login || payload.pull_request?.user?.login;
+  if (pullAuthor == commentAuthor) {
+    await setNextActionReviewers(payload, stateClient);
+  } else {
+    console.log(
+      `Comment was from ${commentAuthor}, not author: ${pullAuthor}. No action 
to take.`
+    );
+  }
+}
+
+// On approval from a reviewer we have assigned, assign committer if one not 
already assigned
+async function processPrReview(
+  payload,
+  stateClient: typeof PersistentState,
+  reviewerConfig: typeof ReviewerConfig
+) {
+  if (payload.review.state != "approved") {
+    return;
+  }
+
+  const pullNumber = payload.issue?.number || payload.pull_request?.number;
+  if (!(await areReviewersAssigned(pullNumber, stateClient))) {
+    return;
+  }
+
+  let prState = await stateClient.getPrState(pullNumber);
+  // TODO(damccorm) - also check if the author is a committer, if they are 
don't auto-assign a committer
+  if (await prState.isAnyAssignedReviewerCommitter()) {
+    return;
+  }
+
+  let labelOfReviewer = prState.getLabelForReviewer(payload.sender.login);

Review comment:
       `const labelOfReviewer`

##########
File path: scripts/ci/pr-bot/processPrUpdate.ts
##########
@@ -0,0 +1,198 @@
+/*
+ * 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, nextActionReviewers } = require("./shared/githubUtils");
+const { PersistentState } = require("./shared/persistentState");
+const { ReviewerConfig } = require("./shared/reviewerConfig");
+const { PATH_TO_CONFIG_FILE } = require("./shared/constants");
+
+async function areReviewersAssigned(
+  pullNumber: number,
+  stateClient: typeof PersistentState
+): Promise<boolean> {
+  const prState = await stateClient.getPrState(pullNumber);
+  return Object.values(prState.reviewersAssignedForLabels).length > 0;
+}
+
+async function processPrComment(
+  payload,
+  stateClient: typeof PersistentState,
+  reviewerConfig: typeof ReviewerConfig
+) {
+  const commentContents = payload.comment.body;
+  const commentAuthor = payload.sender.login;
+  console.log(commentContents);
+  if (
+    await processCommand(
+      payload,
+      commentAuthor,
+      commentContents,
+      stateClient,
+      reviewerConfig
+    )
+  ) {
+    // 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"
+  );
+  const pullAuthor =
+    payload.issue?.user?.login || payload.pull_request?.user?.login;
+  if (pullAuthor == commentAuthor) {
+    await setNextActionReviewers(payload, stateClient);
+  } else {
+    console.log(
+      `Comment was from ${commentAuthor}, not author: ${pullAuthor}. No action 
to take.`
+    );
+  }
+}
+
+// On approval from a reviewer we have assigned, assign committer if one not 
already assigned
+async function processPrReview(
+  payload,
+  stateClient: typeof PersistentState,
+  reviewerConfig: typeof ReviewerConfig
+) {
+  if (payload.review.state != "approved") {
+    return;
+  }
+
+  const pullNumber = payload.issue?.number || payload.pull_request?.number;
+  if (!(await areReviewersAssigned(pullNumber, stateClient))) {
+    return;
+  }
+
+  let prState = await stateClient.getPrState(pullNumber);
+  // TODO(damccorm) - also check if the author is a committer, if they are 
don't auto-assign a committer
+  if (await prState.isAnyAssignedReviewerCommitter()) {
+    return;
+  }
+
+  let labelOfReviewer = prState.getLabelForReviewer(payload.sender.login);
+  if (labelOfReviewer) {
+    let reviewersState = await stateClient.getReviewersForLabelState(
+      labelOfReviewer
+    );
+    let availableReviewers =
+      reviewerConfig.getReviewersForLabel(labelOfReviewer);
+    let chosenCommitter = await reviewersState.assignNextCommitter(
+      availableReviewers
+    );
+    prState.reviewersAssignedForLabels[labelOfReviewer] = chosenCommitter;
+
+    // Set next action to committer
+    await addPrComment(
+      pullNumber,
+      commentStrings.assignCommitter(chosenCommitter)
+    );
+    const existingLabels =
+      payload.issue?.labels || payload.pull_request?.labels;
+    await nextActionReviewers(pullNumber, existingLabels);
+    prState.nextAction = "Reviewers";

Review comment:
       `"Reviewers"` is used multiple times. Please add a constant string at 
the top of the file.

##########
File path: scripts/ci/pr-bot/processPrUpdate.ts
##########
@@ -0,0 +1,198 @@
+/*
+ * 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, nextActionReviewers } = require("./shared/githubUtils");
+const { PersistentState } = require("./shared/persistentState");
+const { ReviewerConfig } = require("./shared/reviewerConfig");
+const { PATH_TO_CONFIG_FILE } = require("./shared/constants");
+
+async function areReviewersAssigned(
+  pullNumber: number,
+  stateClient: typeof PersistentState
+): Promise<boolean> {
+  const prState = await stateClient.getPrState(pullNumber);
+  return Object.values(prState.reviewersAssignedForLabels).length > 0;
+}
+
+async function processPrComment(
+  payload,
+  stateClient: typeof PersistentState,
+  reviewerConfig: typeof ReviewerConfig
+) {
+  const commentContents = payload.comment.body;
+  const commentAuthor = payload.sender.login;
+  console.log(commentContents);
+  if (
+    await processCommand(
+      payload,
+      commentAuthor,
+      commentContents,
+      stateClient,
+      reviewerConfig
+    )
+  ) {
+    // 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"
+  );
+  const pullAuthor =
+    payload.issue?.user?.login || payload.pull_request?.user?.login;
+  if (pullAuthor == commentAuthor) {
+    await setNextActionReviewers(payload, stateClient);
+  } else {
+    console.log(
+      `Comment was from ${commentAuthor}, not author: ${pullAuthor}. No action 
to take.`
+    );
+  }
+}
+
+// On approval from a reviewer we have assigned, assign committer if one not 
already assigned
+async function processPrReview(
+  payload,
+  stateClient: typeof PersistentState,
+  reviewerConfig: typeof ReviewerConfig
+) {
+  if (payload.review.state != "approved") {
+    return;
+  }
+
+  const pullNumber = payload.issue?.number || payload.pull_request?.number;
+  if (!(await areReviewersAssigned(pullNumber, stateClient))) {
+    return;
+  }
+
+  let prState = await stateClient.getPrState(pullNumber);
+  // TODO(damccorm) - also check if the author is a committer, if they are 
don't auto-assign a committer
+  if (await prState.isAnyAssignedReviewerCommitter()) {
+    return;
+  }
+
+  let labelOfReviewer = prState.getLabelForReviewer(payload.sender.login);
+  if (labelOfReviewer) {
+    let reviewersState = await stateClient.getReviewersForLabelState(
+      labelOfReviewer
+    );
+    let availableReviewers =
+      reviewerConfig.getReviewersForLabel(labelOfReviewer);
+    let chosenCommitter = await reviewersState.assignNextCommitter(
+      availableReviewers
+    );
+    prState.reviewersAssignedForLabels[labelOfReviewer] = chosenCommitter;
+
+    // Set next action to committer
+    await addPrComment(
+      pullNumber,
+      commentStrings.assignCommitter(chosenCommitter)
+    );
+    const existingLabels =
+      payload.issue?.labels || payload.pull_request?.labels;
+    await nextActionReviewers(pullNumber, existingLabels);
+    prState.nextAction = "Reviewers";
+
+    // Persist state
+    await stateClient.writePrState(pullNumber, prState);
+    await stateClient.writeReviewersForLabelState(
+      labelOfReviewer,
+      reviewersState
+    );
+  }
+}
+
+// On pr push or author comment, we should put the attention set back on the 
reviewers
+async function setNextActionReviewers(
+  payload,
+  stateClient: typeof PersistentState
+) {
+  const pullNumber = payload.issue?.number || payload.pull_request?.number;
+  if (!(await areReviewersAssigned(pullNumber, stateClient))) {
+    console.log("No reviewers assigned, dont need to manipulate attention 
set");
+    return;
+  }
+  const existingLabels = payload.issue?.labels || payload.pull_request?.labels;
+  await nextActionReviewers(pullNumber, existingLabels);
+  let prState = await stateClient.getPrState(pullNumber);
+  prState.nextAction = "Reviewers";
+  await stateClient.writePrState(pullNumber, 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;
+
+  // TODO(damccorm) - remove this when we roll out to more than go
+  const existingLabels = payload.issue?.labels || payload.pull_request?.labels;
+  if (!existingLabels.find((label) => label.name.toLowerCase() == "go")) {
+    console.log("Does not contain the go label - skipping");
+    return;
+  }
+
+  if (!payload.issue?.pull_request && !payload.pull_request) {
+    console.log("Issue, not pull request - returning");
+    return;
+  }
+  const pullNumber = payload.issue?.number || payload.pull_request?.number;
+
+  let stateClient = new PersistentState();
+  let prState = await stateClient.getPrState(pullNumber);

Review comment:
       `let` => `const`

##########
File path: scripts/ci/pr-bot/processNewPrs.ts
##########
@@ -0,0 +1,220 @@
+/*
+ * 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 } = 
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
+// 7) The pr doesn't contain the go label (temporary). TODO(damccorm) - remove 
this when we're ready to roll this out to everyone.
+// unless we're supposed to remind the user after tests pass
+// (in which case that's all we need to do).
+function needsProcessed(pull, prState: typeof Pr): boolean {
+  if (!pull.labels.find((label) => label.name.toLowerCase() == "go")) {
+    console.log(
+      `Skipping PR ${pull.number} because it doesn't contain the go label`
+    );
+    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") {

Review comment:
       `!==`

##########
File path: scripts/ci/pr-bot/processPrUpdate.ts
##########
@@ -0,0 +1,198 @@
+/*
+ * 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, nextActionReviewers } = require("./shared/githubUtils");
+const { PersistentState } = require("./shared/persistentState");
+const { ReviewerConfig } = require("./shared/reviewerConfig");
+const { PATH_TO_CONFIG_FILE } = require("./shared/constants");
+
+async function areReviewersAssigned(
+  pullNumber: number,
+  stateClient: typeof PersistentState
+): Promise<boolean> {
+  const prState = await stateClient.getPrState(pullNumber);
+  return Object.values(prState.reviewersAssignedForLabels).length > 0;
+}
+
+async function processPrComment(
+  payload,
+  stateClient: typeof PersistentState,
+  reviewerConfig: typeof ReviewerConfig
+) {
+  const commentContents = payload.comment.body;
+  const commentAuthor = payload.sender.login;
+  console.log(commentContents);
+  if (
+    await processCommand(
+      payload,
+      commentAuthor,
+      commentContents,
+      stateClient,
+      reviewerConfig
+    )
+  ) {
+    // 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"
+  );
+  const pullAuthor =
+    payload.issue?.user?.login || payload.pull_request?.user?.login;
+  if (pullAuthor == commentAuthor) {

Review comment:
       `===`

##########
File path: scripts/ci/pr-bot/processNewPrs.ts
##########
@@ -0,0 +1,220 @@
+/*
+ * 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 } = 
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
+// 7) The pr doesn't contain the go label (temporary). TODO(damccorm) - remove 
this when we're ready to roll this out to everyone.
+// unless we're supposed to remind the user after tests pass
+// (in which case that's all we need to do).
+function needsProcessed(pull, prState: typeof Pr): boolean {
+  if (!pull.labels.find((label) => label.name.toLowerCase() == "go")) {
+    console.log(
+      `Skipping PR ${pull.number} because it doesn't contain the go label`
+    );
+    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 (Object.keys(prState.reviewersAssignedForLabels).length > 0) {
+    console.log(
+      `Skipping PR ${pull.number} because it already has been assigned`
+    );
+    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,
+  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) {
+    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,
+  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);
+}
+
+// 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,
+  reviewerConfig: typeof ReviewerConfig,
+  stateClient: typeof PersistentState
+) {
+  let prState = await stateClient.getPrState(pull.number);
+  if (!needsProcessed(pull, prState)) {
+    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) {
+    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 = {};
+  const reviewersForLabels: { [key: string]: string[] } =
+    reviewerConfig.getReviewersForLabels(pull.labels, [pull.user.login]);
+  var labels = Object.keys(reviewersForLabels);
+  if (!labels || labels.length === 0) {
+    return;
+  }
+  for (let i = 0; i < labels.length; i++) {
+    let label = labels[i];
+    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 (let i = 0; i < labelsToUpdate.length; i++) {
+    let label = labelsToUpdate[i];
+    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 (let i = 0; i < openPulls.length; i++) {
+    let pull = openPulls[i];
+    await processPull(pull, reviewerConfig, stateClient);
+  }
+}
+
+processNewPrs();
+
+export {};

Review comment:
       I've never seen this self executing pattern before. I think exporting 
`processNewPrs` and then calling it in the index.ts file would be more 
explicit. Hiding execution under an import could make debugging more difficult 
later.

##########
File path: scripts/ci/pr-bot/processPrUpdate.ts
##########
@@ -0,0 +1,198 @@
+/*
+ * 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, nextActionReviewers } = require("./shared/githubUtils");
+const { PersistentState } = require("./shared/persistentState");
+const { ReviewerConfig } = require("./shared/reviewerConfig");
+const { PATH_TO_CONFIG_FILE } = require("./shared/constants");
+
+async function areReviewersAssigned(
+  pullNumber: number,
+  stateClient: typeof PersistentState
+): Promise<boolean> {
+  const prState = await stateClient.getPrState(pullNumber);
+  return Object.values(prState.reviewersAssignedForLabels).length > 0;
+}
+
+async function processPrComment(
+  payload,
+  stateClient: typeof PersistentState,
+  reviewerConfig: typeof ReviewerConfig
+) {
+  const commentContents = payload.comment.body;
+  const commentAuthor = payload.sender.login;
+  console.log(commentContents);
+  if (
+    await processCommand(
+      payload,
+      commentAuthor,
+      commentContents,
+      stateClient,
+      reviewerConfig
+    )
+  ) {
+    // 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"
+  );
+  const pullAuthor =

Review comment:
       I've seen this author access pattern in a different review. If this 
pattern is repeated, consider adding a helper method.

##########
File path: scripts/ci/pr-bot/processPrUpdate.ts
##########
@@ -0,0 +1,198 @@
+/*
+ * 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, nextActionReviewers } = require("./shared/githubUtils");
+const { PersistentState } = require("./shared/persistentState");
+const { ReviewerConfig } = require("./shared/reviewerConfig");
+const { PATH_TO_CONFIG_FILE } = require("./shared/constants");
+
+async function areReviewersAssigned(
+  pullNumber: number,
+  stateClient: typeof PersistentState
+): Promise<boolean> {
+  const prState = await stateClient.getPrState(pullNumber);
+  return Object.values(prState.reviewersAssignedForLabels).length > 0;
+}
+
+async function processPrComment(
+  payload,
+  stateClient: typeof PersistentState,
+  reviewerConfig: typeof ReviewerConfig
+) {
+  const commentContents = payload.comment.body;
+  const commentAuthor = payload.sender.login;
+  console.log(commentContents);
+  if (
+    await processCommand(
+      payload,
+      commentAuthor,
+      commentContents,
+      stateClient,
+      reviewerConfig
+    )
+  ) {
+    // 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"
+  );
+  const pullAuthor =
+    payload.issue?.user?.login || payload.pull_request?.user?.login;
+  if (pullAuthor == commentAuthor) {
+    await setNextActionReviewers(payload, stateClient);
+  } else {
+    console.log(
+      `Comment was from ${commentAuthor}, not author: ${pullAuthor}. No action 
to take.`
+    );
+  }
+}
+
+// On approval from a reviewer we have assigned, assign committer if one not 
already assigned
+async function processPrReview(
+  payload,
+  stateClient: typeof PersistentState,
+  reviewerConfig: typeof ReviewerConfig
+) {
+  if (payload.review.state != "approved") {
+    return;
+  }
+
+  const pullNumber = payload.issue?.number || payload.pull_request?.number;
+  if (!(await areReviewersAssigned(pullNumber, stateClient))) {
+    return;
+  }
+
+  let prState = await stateClient.getPrState(pullNumber);

Review comment:
       `const prState`

##########
File path: scripts/ci/pr-bot/processPrUpdate.ts
##########
@@ -0,0 +1,198 @@
+/*
+ * 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, nextActionReviewers } = require("./shared/githubUtils");
+const { PersistentState } = require("./shared/persistentState");
+const { ReviewerConfig } = require("./shared/reviewerConfig");
+const { PATH_TO_CONFIG_FILE } = require("./shared/constants");
+
+async function areReviewersAssigned(
+  pullNumber: number,
+  stateClient: typeof PersistentState
+): Promise<boolean> {
+  const prState = await stateClient.getPrState(pullNumber);
+  return Object.values(prState.reviewersAssignedForLabels).length > 0;
+}
+
+async function processPrComment(
+  payload,
+  stateClient: typeof PersistentState,
+  reviewerConfig: typeof ReviewerConfig
+) {
+  const commentContents = payload.comment.body;
+  const commentAuthor = payload.sender.login;
+  console.log(commentContents);
+  if (
+    await processCommand(
+      payload,
+      commentAuthor,
+      commentContents,
+      stateClient,
+      reviewerConfig
+    )
+  ) {
+    // 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"
+  );
+  const pullAuthor =
+    payload.issue?.user?.login || payload.pull_request?.user?.login;
+  if (pullAuthor == commentAuthor) {
+    await setNextActionReviewers(payload, stateClient);
+  } else {
+    console.log(
+      `Comment was from ${commentAuthor}, not author: ${pullAuthor}. No action 
to take.`
+    );
+  }
+}
+
+// On approval from a reviewer we have assigned, assign committer if one not 
already assigned
+async function processPrReview(
+  payload,
+  stateClient: typeof PersistentState,
+  reviewerConfig: typeof ReviewerConfig
+) {
+  if (payload.review.state != "approved") {
+    return;
+  }
+
+  const pullNumber = payload.issue?.number || payload.pull_request?.number;
+  if (!(await areReviewersAssigned(pullNumber, stateClient))) {
+    return;
+  }
+
+  let prState = await stateClient.getPrState(pullNumber);
+  // TODO(damccorm) - also check if the author is a committer, if they are 
don't auto-assign a committer
+  if (await prState.isAnyAssignedReviewerCommitter()) {
+    return;
+  }
+
+  let labelOfReviewer = prState.getLabelForReviewer(payload.sender.login);
+  if (labelOfReviewer) {
+    let reviewersState = await stateClient.getReviewersForLabelState(
+      labelOfReviewer
+    );
+    let availableReviewers =
+      reviewerConfig.getReviewersForLabel(labelOfReviewer);
+    let chosenCommitter = await reviewersState.assignNextCommitter(
+      availableReviewers
+    );
+    prState.reviewersAssignedForLabels[labelOfReviewer] = chosenCommitter;
+
+    // Set next action to committer
+    await addPrComment(
+      pullNumber,
+      commentStrings.assignCommitter(chosenCommitter)
+    );
+    const existingLabels =
+      payload.issue?.labels || payload.pull_request?.labels;
+    await nextActionReviewers(pullNumber, existingLabels);
+    prState.nextAction = "Reviewers";
+
+    // Persist state
+    await stateClient.writePrState(pullNumber, prState);
+    await stateClient.writeReviewersForLabelState(
+      labelOfReviewer,
+      reviewersState
+    );
+  }
+}
+
+// On pr push or author comment, we should put the attention set back on the 
reviewers
+async function setNextActionReviewers(
+  payload,
+  stateClient: typeof PersistentState
+) {
+  const pullNumber = payload.issue?.number || payload.pull_request?.number;
+  if (!(await areReviewersAssigned(pullNumber, stateClient))) {
+    console.log("No reviewers assigned, dont need to manipulate attention 
set");
+    return;
+  }
+  const existingLabels = payload.issue?.labels || payload.pull_request?.labels;
+  await nextActionReviewers(pullNumber, existingLabels);
+  let prState = await stateClient.getPrState(pullNumber);
+  prState.nextAction = "Reviewers";
+  await stateClient.writePrState(pullNumber, 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;
+
+  // TODO(damccorm) - remove this when we roll out to more than go
+  const existingLabels = payload.issue?.labels || payload.pull_request?.labels;
+  if (!existingLabels.find((label) => label.name.toLowerCase() == "go")) {
+    console.log("Does not contain the go label - skipping");
+    return;
+  }
+
+  if (!payload.issue?.pull_request && !payload.pull_request) {
+    console.log("Issue, not pull request - returning");
+    return;
+  }
+  const pullNumber = payload.issue?.number || payload.pull_request?.number;

Review comment:
       The pullNumber access pattern is used 3 times in this PR. Please add a 
helper method.

##########
File path: scripts/ci/pr-bot/processPrUpdate.ts
##########
@@ -0,0 +1,198 @@
+/*
+ * 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, nextActionReviewers } = require("./shared/githubUtils");
+const { PersistentState } = require("./shared/persistentState");
+const { ReviewerConfig } = require("./shared/reviewerConfig");
+const { PATH_TO_CONFIG_FILE } = require("./shared/constants");
+
+async function areReviewersAssigned(
+  pullNumber: number,
+  stateClient: typeof PersistentState
+): Promise<boolean> {
+  const prState = await stateClient.getPrState(pullNumber);
+  return Object.values(prState.reviewersAssignedForLabels).length > 0;
+}
+
+async function processPrComment(
+  payload,
+  stateClient: typeof PersistentState,
+  reviewerConfig: typeof ReviewerConfig
+) {
+  const commentContents = payload.comment.body;
+  const commentAuthor = payload.sender.login;
+  console.log(commentContents);
+  if (
+    await processCommand(
+      payload,
+      commentAuthor,
+      commentContents,
+      stateClient,
+      reviewerConfig
+    )
+  ) {
+    // 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"
+  );
+  const pullAuthor =
+    payload.issue?.user?.login || payload.pull_request?.user?.login;
+  if (pullAuthor == commentAuthor) {
+    await setNextActionReviewers(payload, stateClient);
+  } else {
+    console.log(
+      `Comment was from ${commentAuthor}, not author: ${pullAuthor}. No action 
to take.`
+    );
+  }
+}
+
+// On approval from a reviewer we have assigned, assign committer if one not 
already assigned
+async function processPrReview(
+  payload,
+  stateClient: typeof PersistentState,
+  reviewerConfig: typeof ReviewerConfig
+) {
+  if (payload.review.state != "approved") {
+    return;
+  }
+
+  const pullNumber = payload.issue?.number || payload.pull_request?.number;
+  if (!(await areReviewersAssigned(pullNumber, stateClient))) {
+    return;
+  }
+
+  let prState = await stateClient.getPrState(pullNumber);
+  // TODO(damccorm) - also check if the author is a committer, if they are 
don't auto-assign a committer
+  if (await prState.isAnyAssignedReviewerCommitter()) {
+    return;
+  }
+
+  let labelOfReviewer = prState.getLabelForReviewer(payload.sender.login);
+  if (labelOfReviewer) {
+    let reviewersState = await stateClient.getReviewersForLabelState(
+      labelOfReviewer
+    );
+    let availableReviewers =
+      reviewerConfig.getReviewersForLabel(labelOfReviewer);
+    let chosenCommitter = await reviewersState.assignNextCommitter(
+      availableReviewers
+    );
+    prState.reviewersAssignedForLabels[labelOfReviewer] = chosenCommitter;
+
+    // Set next action to committer
+    await addPrComment(
+      pullNumber,
+      commentStrings.assignCommitter(chosenCommitter)
+    );
+    const existingLabels =
+      payload.issue?.labels || payload.pull_request?.labels;
+    await nextActionReviewers(pullNumber, existingLabels);
+    prState.nextAction = "Reviewers";
+
+    // Persist state
+    await stateClient.writePrState(pullNumber, prState);
+    await stateClient.writeReviewersForLabelState(
+      labelOfReviewer,
+      reviewersState
+    );
+  }
+}
+
+// On pr push or author comment, we should put the attention set back on the 
reviewers
+async function setNextActionReviewers(
+  payload,
+  stateClient: typeof PersistentState
+) {
+  const pullNumber = payload.issue?.number || payload.pull_request?.number;
+  if (!(await areReviewersAssigned(pullNumber, stateClient))) {
+    console.log("No reviewers assigned, dont need to manipulate attention 
set");
+    return;
+  }
+  const existingLabels = payload.issue?.labels || payload.pull_request?.labels;
+  await nextActionReviewers(pullNumber, existingLabels);
+  let prState = await stateClient.getPrState(pullNumber);
+  prState.nextAction = "Reviewers";
+  await stateClient.writePrState(pullNumber, 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;
+
+  // TODO(damccorm) - remove this when we roll out to more than go
+  const existingLabels = payload.issue?.labels || payload.pull_request?.labels;
+  if (!existingLabels.find((label) => label.name.toLowerCase() == "go")) {

Review comment:
       1. `===`
   2. `"go"` is used multiple time. Please save it into a constant.

##########
File path: scripts/ci/pr-bot/processPrUpdate.ts
##########
@@ -0,0 +1,198 @@
+/*
+ * 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, nextActionReviewers } = require("./shared/githubUtils");
+const { PersistentState } = require("./shared/persistentState");
+const { ReviewerConfig } = require("./shared/reviewerConfig");
+const { PATH_TO_CONFIG_FILE } = require("./shared/constants");
+
+async function areReviewersAssigned(
+  pullNumber: number,
+  stateClient: typeof PersistentState
+): Promise<boolean> {
+  const prState = await stateClient.getPrState(pullNumber);
+  return Object.values(prState.reviewersAssignedForLabels).length > 0;
+}
+
+async function processPrComment(
+  payload,
+  stateClient: typeof PersistentState,
+  reviewerConfig: typeof ReviewerConfig
+) {
+  const commentContents = payload.comment.body;
+  const commentAuthor = payload.sender.login;
+  console.log(commentContents);
+  if (
+    await processCommand(
+      payload,
+      commentAuthor,
+      commentContents,
+      stateClient,
+      reviewerConfig
+    )
+  ) {
+    // 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"
+  );
+  const pullAuthor =
+    payload.issue?.user?.login || payload.pull_request?.user?.login;
+  if (pullAuthor == commentAuthor) {
+    await setNextActionReviewers(payload, stateClient);
+  } else {
+    console.log(
+      `Comment was from ${commentAuthor}, not author: ${pullAuthor}. No action 
to take.`
+    );
+  }
+}
+
+// On approval from a reviewer we have assigned, assign committer if one not 
already assigned
+async function processPrReview(
+  payload,
+  stateClient: typeof PersistentState,
+  reviewerConfig: typeof ReviewerConfig
+) {
+  if (payload.review.state != "approved") {
+    return;
+  }
+
+  const pullNumber = payload.issue?.number || payload.pull_request?.number;
+  if (!(await areReviewersAssigned(pullNumber, stateClient))) {
+    return;
+  }
+
+  let prState = await stateClient.getPrState(pullNumber);
+  // TODO(damccorm) - also check if the author is a committer, if they are 
don't auto-assign a committer
+  if (await prState.isAnyAssignedReviewerCommitter()) {
+    return;
+  }
+
+  let labelOfReviewer = prState.getLabelForReviewer(payload.sender.login);
+  if (labelOfReviewer) {
+    let reviewersState = await stateClient.getReviewersForLabelState(
+      labelOfReviewer
+    );
+    let availableReviewers =
+      reviewerConfig.getReviewersForLabel(labelOfReviewer);
+    let chosenCommitter = await reviewersState.assignNextCommitter(
+      availableReviewers
+    );
+    prState.reviewersAssignedForLabels[labelOfReviewer] = chosenCommitter;
+
+    // Set next action to committer
+    await addPrComment(
+      pullNumber,
+      commentStrings.assignCommitter(chosenCommitter)
+    );
+    const existingLabels =
+      payload.issue?.labels || payload.pull_request?.labels;
+    await nextActionReviewers(pullNumber, existingLabels);
+    prState.nextAction = "Reviewers";
+
+    // Persist state
+    await stateClient.writePrState(pullNumber, prState);
+    await stateClient.writeReviewersForLabelState(
+      labelOfReviewer,
+      reviewersState
+    );
+  }
+}
+
+// On pr push or author comment, we should put the attention set back on the 
reviewers
+async function setNextActionReviewers(
+  payload,
+  stateClient: typeof PersistentState
+) {
+  const pullNumber = payload.issue?.number || payload.pull_request?.number;
+  if (!(await areReviewersAssigned(pullNumber, stateClient))) {
+    console.log("No reviewers assigned, dont need to manipulate attention 
set");
+    return;
+  }
+  const existingLabels = payload.issue?.labels || payload.pull_request?.labels;
+  await nextActionReviewers(pullNumber, existingLabels);
+  let prState = await stateClient.getPrState(pullNumber);
+  prState.nextAction = "Reviewers";
+  await stateClient.writePrState(pullNumber, 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;
+
+  // TODO(damccorm) - remove this when we roll out to more than go
+  const existingLabels = payload.issue?.labels || payload.pull_request?.labels;
+  if (!existingLabels.find((label) => label.name.toLowerCase() == "go")) {
+    console.log("Does not contain the go label - skipping");
+    return;
+  }
+
+  if (!payload.issue?.pull_request && !payload.pull_request) {
+    console.log("Issue, not pull request - returning");
+    return;
+  }
+  const pullNumber = payload.issue?.number || payload.pull_request?.number;
+
+  let stateClient = new PersistentState();
+  let prState = await stateClient.getPrState(pullNumber);
+  if (prState.stopReviewerNotifications) {
+    console.log("Notifications have been paused for this pull - skipping");
+    return;
+  }
+
+  switch (github.context.eventName) {
+    case "pull_request_review_comment":
+    case "issue_comment":
+      console.log("Processing comment event");
+      if (payload.action != "created") {
+        console.log("Comment wasnt just created, skipping");
+        return;
+      }
+      await processPrComment(payload, stateClient, reviewerConfig);
+      break;
+    case "pull_request_review":
+      console.log("Processing PR review event");
+      await processPrReview(payload, stateClient, reviewerConfig);
+      break;
+    case "pull_request_target":
+      if (payload.action == "synchronize") {
+        console.log("Processing synchronize action");
+        await setNextActionReviewers(payload, 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, push, or review, doing nothing");
+  }
+}
+
+processPrUpdate();
+
+export {};

Review comment:
       I've never seen this self executing pattern before. I think exporting 
`processNewPrs` and then calling it in the index.ts file would be more 
explicit. Hiding execution under an import could make debugging more difficult 
later.

##########
File path: scripts/ci/pr-bot/processPrUpdate.ts
##########
@@ -0,0 +1,198 @@
+/*
+ * 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, nextActionReviewers } = require("./shared/githubUtils");
+const { PersistentState } = require("./shared/persistentState");
+const { ReviewerConfig } = require("./shared/reviewerConfig");
+const { PATH_TO_CONFIG_FILE } = require("./shared/constants");
+
+async function areReviewersAssigned(
+  pullNumber: number,
+  stateClient: typeof PersistentState
+): Promise<boolean> {
+  const prState = await stateClient.getPrState(pullNumber);
+  return Object.values(prState.reviewersAssignedForLabels).length > 0;
+}
+
+async function processPrComment(
+  payload,
+  stateClient: typeof PersistentState,
+  reviewerConfig: typeof ReviewerConfig
+) {
+  const commentContents = payload.comment.body;
+  const commentAuthor = payload.sender.login;
+  console.log(commentContents);
+  if (
+    await processCommand(
+      payload,
+      commentAuthor,
+      commentContents,
+      stateClient,
+      reviewerConfig
+    )
+  ) {
+    // 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"
+  );
+  const pullAuthor =
+    payload.issue?.user?.login || payload.pull_request?.user?.login;
+  if (pullAuthor == commentAuthor) {
+    await setNextActionReviewers(payload, stateClient);
+  } else {
+    console.log(
+      `Comment was from ${commentAuthor}, not author: ${pullAuthor}. No action 
to take.`
+    );
+  }
+}
+
+// On approval from a reviewer we have assigned, assign committer if one not 
already assigned
+async function processPrReview(
+  payload,
+  stateClient: typeof PersistentState,
+  reviewerConfig: typeof ReviewerConfig
+) {
+  if (payload.review.state != "approved") {
+    return;
+  }
+
+  const pullNumber = payload.issue?.number || payload.pull_request?.number;
+  if (!(await areReviewersAssigned(pullNumber, stateClient))) {
+    return;
+  }
+
+  let prState = await stateClient.getPrState(pullNumber);
+  // TODO(damccorm) - also check if the author is a committer, if they are 
don't auto-assign a committer
+  if (await prState.isAnyAssignedReviewerCommitter()) {
+    return;
+  }
+
+  let labelOfReviewer = prState.getLabelForReviewer(payload.sender.login);
+  if (labelOfReviewer) {
+    let reviewersState = await stateClient.getReviewersForLabelState(

Review comment:
       All these `let` should be `const`

##########
File path: scripts/ci/pr-bot/processPrUpdate.ts
##########
@@ -0,0 +1,198 @@
+/*
+ * 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, nextActionReviewers } = require("./shared/githubUtils");
+const { PersistentState } = require("./shared/persistentState");
+const { ReviewerConfig } = require("./shared/reviewerConfig");
+const { PATH_TO_CONFIG_FILE } = require("./shared/constants");
+
+async function areReviewersAssigned(
+  pullNumber: number,
+  stateClient: typeof PersistentState
+): Promise<boolean> {
+  const prState = await stateClient.getPrState(pullNumber);
+  return Object.values(prState.reviewersAssignedForLabels).length > 0;
+}
+
+async function processPrComment(
+  payload,
+  stateClient: typeof PersistentState,
+  reviewerConfig: typeof ReviewerConfig
+) {
+  const commentContents = payload.comment.body;
+  const commentAuthor = payload.sender.login;
+  console.log(commentContents);
+  if (
+    await processCommand(
+      payload,
+      commentAuthor,
+      commentContents,
+      stateClient,
+      reviewerConfig
+    )
+  ) {
+    // 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"
+  );
+  const pullAuthor =
+    payload.issue?.user?.login || payload.pull_request?.user?.login;
+  if (pullAuthor == commentAuthor) {
+    await setNextActionReviewers(payload, stateClient);
+  } else {
+    console.log(
+      `Comment was from ${commentAuthor}, not author: ${pullAuthor}. No action 
to take.`
+    );
+  }
+}
+
+// On approval from a reviewer we have assigned, assign committer if one not 
already assigned
+async function processPrReview(
+  payload,
+  stateClient: typeof PersistentState,
+  reviewerConfig: typeof ReviewerConfig
+) {
+  if (payload.review.state != "approved") {

Review comment:
       `!==`

##########
File path: scripts/ci/pr-bot/processNewPrs.ts
##########
@@ -0,0 +1,220 @@
+/*
+ * 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 } = 
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
+// 7) The pr doesn't contain the go label (temporary). TODO(damccorm) - remove 
this when we're ready to roll this out to everyone.
+// unless we're supposed to remind the user after tests pass
+// (in which case that's all we need to do).
+function needsProcessed(pull, prState: typeof Pr): boolean {

Review comment:
       Is the parameter `pull` missing a type?

##########
File path: scripts/ci/pr-bot/processNewPrs.ts
##########
@@ -0,0 +1,220 @@
+/*
+ * 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 } = 
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
+// 7) The pr doesn't contain the go label (temporary). TODO(damccorm) - remove 
this when we're ready to roll this out to everyone.
+// unless we're supposed to remind the user after tests pass
+// (in which case that's all we need to do).
+function needsProcessed(pull, prState: typeof Pr): boolean {
+  if (!pull.labels.find((label) => label.name.toLowerCase() == "go")) {
+    console.log(
+      `Skipping PR ${pull.number} because it doesn't contain the go label`
+    );
+    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 (Object.keys(prState.reviewersAssignedForLabels).length > 0) {
+    console.log(
+      `Skipping PR ${pull.number} because it already has been assigned`
+    );
+    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,
+  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) {
+    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,
+  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);
+}
+
+// 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,
+  reviewerConfig: typeof ReviewerConfig,
+  stateClient: typeof PersistentState
+) {
+  let prState = await stateClient.getPrState(pull.number);
+  if (!needsProcessed(pull, prState)) {
+    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) {
+    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 = {};
+  const reviewersForLabels: { [key: string]: string[] } =
+    reviewerConfig.getReviewersForLabels(pull.labels, [pull.user.login]);
+  var labels = Object.keys(reviewersForLabels);
+  if (!labels || labels.length === 0) {
+    return;
+  }
+  for (let i = 0; i < labels.length; i++) {
+    let label = labels[i];
+    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 (let i = 0; i < labelsToUpdate.length; i++) {
+    let label = labelsToUpdate[i];
+    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 (let i = 0; i < openPulls.length; i++) {
+    let pull = openPulls[i];
+    await processPull(pull, reviewerConfig, stateClient);
+  }
+}

Review comment:
       Use a `for...of` loop when the index is not required.
   ```
   for (const pull of openPulls) {
      await processPull(pull, reviewerConfig, stateClient);
   }
   ```




-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to