This is an automated email from the ASF dual-hosted git repository.
Abacn pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/beam.git
The following commit(s) were added to refs/heads/master by this push:
new 12126d8942a [CI] Stop infinite reviewer reassignment, batch state
commits, clean up stale state (#40096)
12126d8942a is described below
commit 12126d8942aaf848030c478b4c6a28c6af861c66
Author: Yi Hu <[email protected]>
AuthorDate: Fri Sep 11 18:09:37 2026 -0400
[CI] Stop infinite reviewer reassignment, batch state commits, clean up
stale state (#40096)
* [CI] Stop infinite reviewer reassignment, batch state commits, and clean
up stale state
* Stop infinite reviewer reassignment loops: label with "awaiting triage"
if a PR has both "reassigned-reviewers" and "Next Action: Reviewers", and
review started >60 days ago.
* Track initial reviewer assignment timestamp (reviewersAssignedAt) in
persistent PR state, falling back to PR creation time for legacy PRs.
* Skip reviewer assignment for PRs labeled "awaiting triage" across new PR
processing and daily reminder workflows.
* shallow fetch pr-state branch to accelerate the workflow run
* Incrementally prune closed PR state files (oldest 100 per daily run) from
the pr-bot-state branch.
* Document core PR bot logic in scripts/ci/pr-bot/README.md.
---
scripts/ci/pr-bot/README.md | 27 ++++++++++++
scripts/ci/pr-bot/findPrsNeedingAttention.ts | 61 ++++++++++++++++++++-------
scripts/ci/pr-bot/processNewPrs.ts | 31 ++++++++++----
scripts/ci/pr-bot/shared/commentStrings.ts | 5 +--
scripts/ci/pr-bot/shared/constants.ts | 4 ++
scripts/ci/pr-bot/shared/githubUtils.ts | 8 ++++
scripts/ci/pr-bot/shared/persistentState.ts | 59 +++++++++++++++++++++++---
scripts/ci/pr-bot/shared/pr.ts | 5 +++
scripts/ci/pr-bot/shared/userCommand.ts | 4 +-
scripts/ci/pr-bot/test/githubUtilsTest.ts | 63 ++++++++++++++++++++++++++++
scripts/ci/pr-bot/test/prTest.ts | 13 ++++++
11 files changed, 248 insertions(+), 32 deletions(-)
diff --git a/scripts/ci/pr-bot/README.md b/scripts/ci/pr-bot/README.md
index d6a55d45e9f..6c2e944be78 100644
--- a/scripts/ci/pr-bot/README.md
+++ b/scripts/ci/pr-bot/README.md
@@ -23,6 +23,33 @@ This directory holds all the code (except for Actions
Workflows) for our PR bot
For a list of commands to use when interacting with the bot, see
[Commands.md](./Commands.md).
For a design doc explaining the design and implementation, see [Automate
Reviewer
Assignment](https://docs.google.com/document/d/1FhRPRD6VXkYlLAPhNfZB7y2Yese2FCWBzjx67d3TjBo/edit#)
+## PR Bot Logic
+
+The bot consists of three core workflows and a persistent state tracking
system:
+
+### 1. New PR Processing (`processNewPrs.ts`)
+* Runs periodically on a schedule (every 30 minutes).
+* Checks eligible open PRs (skips WIP, drafts, closed, PRs < 20 minutes old,
PRs with notifications silenced, or PRs labeled `awaiting triage`).
+* Once CI checks pass, assigns reviewers based on configured label mappings in
`.github/REVIEWERS.yml` (prioritizing least-recently-assigned reviewers).
+* If a non-committer reviewer approves, automatically assigns a committer for
final review and merge.
+* Sets `Next Action: Reviewers` label.
+
+### 2. PR Updates & Commands (`processPrUpdate.ts`)
+* Triggered on PR pushes (`synchronize`) and comments (`issue_comment:
created`).
+* Shifts attention back to reviewers (`Next Action: Reviewers`) when author
pushes new commits or posts comments.
+* Removes `slow-review` label upon receiving a comment from a non-author
reviewer.
+* Processes commands like `assign to next reviewer`, `waiting on author`,
`stop reviewer notifications`, `assign set of reviewers`, and `remind me after
tests pass`.
+
+### 3. Reviewer Reminders & Stale PRs (`findPrsNeedingAttention.ts`)
+* Runs daily to identify PRs needing action.
+* Flags PRs awaiting reviewer response as `slow-review` if inactive for ≥ 7
days (or ≥ 2 weekdays without comments).
+* If still no response after 2 more weekdays, reassigns to new reviewers,
removes `slow-review`, and adds `reassigned-reviewers`.
+* **Stale PR Cutoff**: If a PR has both `reassigned-reviewers` and `Next
Action: Reviewers` labels and review started > 60 days ago, it stops reviewer
assignment loops and adds `awaiting triage`. PRs labeled `awaiting triage` are
skipped.
+* **Stale State Cleanup**: Cleans up the oldest 100 state files for PRs that
are no longer open to incrementally prune closed PR metadata from the state
branch.
+
+### 4. Persistent State (`PersistentState`)
+* Stores PR review progress and label assignment rotations on the
`pr-bot-state` Git branch under `state/pr-state/pr-<number>.json` and
`state/reviewers-for-label-<label>.json`.
+
## Build/Test
To build, run:
diff --git a/scripts/ci/pr-bot/findPrsNeedingAttention.ts
b/scripts/ci/pr-bot/findPrsNeedingAttention.ts
index ebca0deba83..46233b4ce31 100644
--- a/scripts/ci/pr-bot/findPrsNeedingAttention.ts
+++ b/scripts/ci/pr-bot/findPrsNeedingAttention.ts
@@ -26,15 +26,13 @@ const {
REPO,
PATH_TO_CONFIG_FILE,
SLOW_REVIEW_LABEL,
+ REASSIGNED_REVIEWERS_LABEL,
+ AWAITING_TRIAGE_LABEL,
+ NEXT_ACTION_REVIEWERS_LABEL,
} = require("./shared/constants");
+const { hasLabel } = github;
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
-function hasLabel(pull: any, labelName: string): boolean {
- return pull.labels.some(
- (label) => label.name.toLowerCase() === labelName.toLowerCase()
- );
-}
-
function getTwoWeekdaysAgo(): Date {
const twoWeekDaysAgo = new Date(Date.now() - 2 * ONE_DAY_MS);
const currentDay = new Date(Date.now()).getDay();
@@ -49,7 +47,7 @@ function getTwoWeekdaysAgo(): Date {
}
async function isSlowReview(pull: any): Promise<boolean> {
- if (!hasLabel(pull, "Next Action: Reviewers")) {
+ if (!hasLabel(pull, NEXT_ACTION_REVIEWERS_LABEL)) {
return false;
}
const lastModified = new Date(pull.updated_at);
@@ -102,15 +100,18 @@ async function assignToNewReviewers(
let prState = await stateClient.getPrState(pull.number);
let reviewerStateToUpdate = {};
const labelObjects = pull.labels;
- let reviewersToExclude: string[] =
Object.values(prState.reviewersAssignedForLabels) as string[];
+ let reviewersToExclude: string[] = Object.values(
+ prState.reviewersAssignedForLabels
+ ) as string[];
if (pull.requested_reviewers) {
- reviewersToExclude =
reviewersToExclude.concat(pull.requested_reviewers.map((r: any) => r.login));
+ reviewersToExclude = reviewersToExclude.concat(
+ pull.requested_reviewers.map((r: any) => r.login)
+ );
}
reviewersToExclude.push(pull.user.login);
const reviewersForLabels: { [key: string]: string[] } =
reviewerConfig.getReviewersForLabels(labelObjects, reviewersToExclude);
- const fallbackReviewers =
- reviewerConfig.getFallbackReviewers();
+ const fallbackReviewers = reviewerConfig.getFallbackReviewers();
for (const labelObject of labelObjects) {
const label = labelObject.name;
let availableReviewers = reviewersForLabels[label];
@@ -156,6 +157,32 @@ async function processPull(
console.log(`Skipping PR ${pull.number} - notifications silenced`);
return;
}
+ if (hasLabel(pull, AWAITING_TRIAGE_LABEL)) {
+ console.log(`Skipping PR ${pull.number} - awaiting triage`);
+ return;
+ }
+
+ const sixtyDaysAgo = new Date(Date.now() - 60 * ONE_DAY_MS);
+ const initialReviewDate = prState.reviewersAssignedAt
+ ? new Date(prState.reviewersAssignedAt)
+ : new Date(pull.created_at);
+ if (
+ hasLabel(pull, REASSIGNED_REVIEWERS_LABEL) &&
+ hasLabel(pull, NEXT_ACTION_REVIEWERS_LABEL) &&
+ initialReviewDate.getTime() < sixtyDaysAgo.getTime()
+ ) {
+ console.log(
+ `PR ${pull.number} has reassigned-reviewers and Next Action: Reviewers
labels and review started >60 days ago - adding awaiting triage label`
+ );
+ await github.getGitHubClient().rest.issues.addLabels({
+ owner: REPO_OWNER,
+ repo: REPO,
+ issue_number: pull.number,
+ labels: [AWAITING_TRIAGE_LABEL],
+ });
+ return;
+ }
+
if (hasLabel(pull, SLOW_REVIEW_LABEL)) {
const lastModified = new Date(pull.updated_at);
const twoWeekDaysAgo = getTwoWeekdaysAgo();
@@ -177,7 +204,7 @@ async function processPull(
owner: REPO_OWNER,
repo: REPO,
issue_number: pull.number,
- labels: ["reassigned-reviewers"],
+ labels: [REASSIGNED_REVIEWERS_LABEL],
});
}
@@ -186,9 +213,13 @@ async function processPull(
if (await isSlowReview(pull)) {
const client = github.getGitHubClient();
- let reviewersToPing = Object.values(prState.reviewersAssignedForLabels ||
{});
+ let reviewersToPing = Object.values(
+ prState.reviewersAssignedForLabels || {}
+ );
if (pull.requested_reviewers) {
- reviewersToPing =
reviewersToPing.concat(pull.requested_reviewers.map((r: any) => r.login));
+ reviewersToPing = reviewersToPing.concat(
+ pull.requested_reviewers.map((r: any) => r.login)
+ );
}
reviewersToPing = [...new Set(reviewersToPing as string[])];
@@ -226,6 +257,8 @@ async function processOldPrs() {
for (const pull of openPulls) {
await processPull(pull, reviewerConfig, stateClient);
}
+
+ await stateClient.deleteStalePrStates(openPulls, 100);
}
processOldPrs();
diff --git a/scripts/ci/pr-bot/processNewPrs.ts
b/scripts/ci/pr-bot/processNewPrs.ts
index 9f0ed52a116..90957451acc 100644
--- a/scripts/ci/pr-bot/processNewPrs.ts
+++ b/scripts/ci/pr-bot/processNewPrs.ts
@@ -27,6 +27,7 @@ const {
REPO,
PATH_TO_CONFIG_FILE,
REVIEWERS_ACTION,
+ AWAITING_TRIAGE_LABEL,
} = require("./shared/constants");
import { CheckStatus } from "./shared/checks";
@@ -44,6 +45,12 @@ import { CheckStatus } from "./shared/checks";
* (in which case that's all we need to do).
*/
function needsProcessed(pull: any, prState: typeof Pr): boolean {
+ if (github.hasLabel(pull, AWAITING_TRIAGE_LABEL)) {
+ console.log(
+ `Skipping PR ${pull.number} because it has awaiting triage label`
+ );
+ return false;
+ }
const firstPythonPrToProcess = new Date(2022, 5, 16, 14); // June 16 2022,
14:00 UTC (note that JavaScript months are 0 indexed)
const firstPrToProcess = new Date(2022, 6, 15, 23); // July 15 2022, 23:00
UTC (note that JavaScript months are 0 indexed)
const createdAt = new Date(pull.created_at);
@@ -167,7 +174,9 @@ async function approvedBy(pull: any): Promise<string[]> {
async function isAnyGithubReviewerCommitter(pull: any): Promise<boolean> {
let reviewers: string[] = [];
if (pull.requested_reviewers && pull.requested_reviewers.length > 0) {
- reviewers = reviewers.concat(pull.requested_reviewers.map((r: any) =>
r.login));
+ reviewers = reviewers.concat(
+ pull.requested_reviewers.map((r: any) => r.login)
+ );
}
for (const reviewer of reviewers) {
if (await github.checkIfCommitter(reviewer)) {
@@ -194,8 +203,8 @@ async function processPull(
await github.addPrComment(
pull.number,
"Closing this PR because dependabot updates for container/** are not
allowed due to generated files " +
- "and excluded_paths is disabled due to
dependabot/dependabot-core#14408. " +
- "Once issue is resolved, please remove this step."
+ "and excluded_paths is disabled due to
dependabot/dependabot-core#14408. " +
+ "Once issue is resolved, please remove this step."
);
await github.closePr(pull.number);
return;
@@ -210,8 +219,10 @@ async function processPull(
console.log(`Processing PR ${pull.number}`);
// If reviewers are already assigned, we just need to check if we should
assign a committer.
- const hasReviewersAssignedForLabels =
Object.keys(prState.reviewersAssignedForLabels).length > 0;
- const hasGithubReviewers = pull.requested_reviewers &&
pull.requested_reviewers.length > 0;
+ const hasReviewersAssignedForLabels =
+ Object.keys(prState.reviewersAssignedForLabels).length > 0;
+ const hasGithubReviewers =
+ pull.requested_reviewers && pull.requested_reviewers.length > 0;
if (hasReviewersAssignedForLabels || hasGithubReviewers) {
if (prState.committerAssigned) {
@@ -237,7 +248,11 @@ async function processPull(
// we can try to guess a label from the PR to assign a committer to.
if (!labelOfReviewer) {
let isGithubReviewer = false;
- if (pull.requested_reviewers && pull.requested_reviewers.some((r: any)
=> r.login === approver)) isGithubReviewer = true;
+ if (
+ pull.requested_reviewers &&
+ pull.requested_reviewers.some((r: any) => r.login === approver)
+ )
+ isGithubReviewer = true;
if (isGithubReviewer && pull.labels && pull.labels.length > 0) {
const validLabels = reviewerConfig.getReviewersForAllLabels();
@@ -272,8 +287,7 @@ async function processPull(
);
const availableReviewers =
reviewerConfig.getReviewersForLabel(labelOfReviewer);
- const fallbackReviewers =
- reviewerConfig.getFallbackReviewers();
+ const fallbackReviewers = reviewerConfig.getFallbackReviewers();
const chosenCommitter = await reviewersState.assignNextCommitter(
availableReviewers,
fallbackReviewers
@@ -348,6 +362,7 @@ async function processPull(
github.nextActionReviewers(pull.number, pull.labels);
prState.nextAction = "Reviewers";
+ prState.reviewersAssignedAt = Date.now();
await stateClient.writePrState(pull.number, prState);
let labelsToUpdate = Object.keys(reviewerStateToUpdate);
diff --git a/scripts/ci/pr-bot/shared/commentStrings.ts
b/scripts/ci/pr-bot/shared/commentStrings.ts
index 272f185d4ec..f1a39be4c55 100644
--- a/scripts/ci/pr-bot/shared/commentStrings.ts
+++ b/scripts/ci/pr-bot/shared/commentStrings.ts
@@ -26,7 +26,7 @@ export interface AssignReviewerOptions {
// Custom notices for specific labels
const LABEL_NOTICES: Record<string, string> = {
- core: "This pull request likely touches a core component (\"core\" label).
Please review with scrutiny.",
+ core: 'This pull request likely touches a core component ("core" label).
Please review with scrutiny.',
};
function formatNotices(
@@ -59,8 +59,7 @@ export function assignReviewer(
labelToReviewerMapping: any,
options?: AssignReviewerOptions
): string {
- let commentString =
- "Assigning reviewers:\n\n";
+ let commentString = "Assigning reviewers:\n\n";
for (let label in labelToReviewerMapping) {
let reviewer = labelToReviewerMapping[label];
diff --git a/scripts/ci/pr-bot/shared/constants.ts
b/scripts/ci/pr-bot/shared/constants.ts
index 33a20baf6d3..9b4751b26ab 100644
--- a/scripts/ci/pr-bot/shared/constants.ts
+++ b/scripts/ci/pr-bot/shared/constants.ts
@@ -31,3 +31,7 @@ export const BOT_NAME = "github-actions";
export const REVIEWERS_ACTION = "Reviewers";
export const SLOW_REVIEW_LABEL = "slow-review";
export const NO_MATCHING_LABEL = "no-matching-label";
+export const REASSIGNED_REVIEWERS_LABEL = "reassigned-reviewers";
+export const AWAITING_TRIAGE_LABEL = "awaiting triage";
+export const NEXT_ACTION_REVIEWERS_LABEL = "Next Action: Reviewers";
+export const PR_STATE_DIR = "state/pr-state";
diff --git a/scripts/ci/pr-bot/shared/githubUtils.ts
b/scripts/ci/pr-bot/shared/githubUtils.ts
index 0d287bd0937..696f5f9c739 100644
--- a/scripts/ci/pr-bot/shared/githubUtils.ts
+++ b/scripts/ci/pr-bot/shared/githubUtils.ts
@@ -123,3 +123,11 @@ function removeNextActionLabel(existingLabels: Label[]):
string[] {
)
.map((label) => label.name);
}
+
+export function hasLabel(pull: any, labelName: string): boolean {
+ return (pull?.labels || []).some(
+ (label: any) =>
+ (typeof label === "string" ? label : label?.name || "").toLowerCase() ===
+ labelName.toLowerCase()
+ );
+}
diff --git a/scripts/ci/pr-bot/shared/persistentState.ts
b/scripts/ci/pr-bot/shared/persistentState.ts
index 9c277f14d4a..b579d68f5c8 100644
--- a/scripts/ci/pr-bot/shared/persistentState.ts
+++ b/scripts/ci/pr-bot/shared/persistentState.ts
@@ -21,7 +21,7 @@ const fs = require("fs");
const path = require("path");
const { Pr } = require("./pr");
const { ReviewersForLabel } = require("./reviewersForLabel");
-const { BOT_NAME } = require("./constants");
+const { BOT_NAME, PR_STATE_DIR } = require("./constants");
function getPrFileName(prNumber) {
return `pr-${prNumber}.json`.toLowerCase();
@@ -41,7 +41,7 @@ async function commitStateToRepo() {
}
// Print changes for observability
await exec.exec("git status", [], { ignoreReturnCode: true });
- await exec.exec("git add state/*");
+ await exec.exec("git add -A state");
const changes = await exec.exec(
"git diff --quiet --cached origin/pr-bot-state state",
[],
@@ -63,13 +63,13 @@ export class PersistentState {
// Returns a Pr object representing the current saved state of the pr.
async getPrState(prNumber: number): Promise<typeof Pr> {
var fileName = getPrFileName(prNumber);
- return new Pr(await this.getState(fileName, "state/pr-state"));
+ return new Pr(await this.getState(fileName, PR_STATE_DIR));
}
// Writes a Pr object representing the current saved state of the pr to
persistent storage.
async writePrState(prNumber: number, newState: any) {
var fileName = getPrFileName(prNumber);
- await this.writeState(fileName, "state/pr-state", new Pr(newState));
+ await this.writeState(fileName, PR_STATE_DIR, new Pr(newState));
}
// Returns a ReviewersForLabel object representing the current saved state
of which reviewers have reviewed recently.
@@ -90,6 +90,55 @@ export class PersistentState {
);
}
+ // Deletes up to maxToDelete state files for PRs that are no longer open,
starting from the oldest PRs.
+ async deleteStalePrStates(
+ openPulls: any[],
+ maxToDelete: number = 100
+ ): Promise<number> {
+ if (openPulls.length === 0) {
+ return 0;
+ }
+ await this.ensureCorrectBranch();
+ if (!fs.existsSync(PR_STATE_DIR)) {
+ return 0;
+ }
+ const openPrSet = new Set(openPulls.map((p) => p.number));
+ const files = fs.readdirSync(PR_STATE_DIR);
+ const stalePrs: { prNumber: number; filePath: string }[] = [];
+
+ for (const file of files) {
+ const match = file.match(/^pr-(\d+)\.json$/);
+ if (match) {
+ const prNumber = parseInt(match[1], 10);
+ if (!openPrSet.has(prNumber)) {
+ stalePrs.push({
+ prNumber,
+ filePath: path.join(PR_STATE_DIR, file),
+ });
+ }
+ }
+ }
+
+ // Sort by PR number ascending so the oldest PRs are deleted first
+ stalePrs.sort((a, b) => a.prNumber - b.prNumber);
+
+ const prsToDelete = stalePrs.slice(0, maxToDelete);
+ for (const pr of prsToDelete) {
+ fs.unlinkSync(pr.filePath);
+ }
+
+ if (prsToDelete.length > 0) {
+ console.log(
+ `Deleted ${prsToDelete.length} stale PR state files (oldest: PR ${
+ prsToDelete[0].prNumber
+ }, newest: PR ${prsToDelete[prsToDelete.length - 1].prNumber})`
+ );
+ await commitStateToRepo();
+ }
+
+ return prsToDelete.length;
+ }
+
private async getState(fileName, baseDirectory) {
await this.ensureCorrectBranch();
fileName = path.join(baseDirectory, fileName);
@@ -122,7 +171,7 @@ export class PersistentState {
await exec.exec(`git config user.name ${BOT_NAME}`);
await exec.exec(`git config user.email ${BOT_NAME}@github.com`);
await exec.exec("git config pull.rebase false");
- await exec.exec("git fetch origin pr-bot-state");
+ await exec.exec("git fetch origin pr-bot-state --depth=1");
await exec.exec("git checkout pr-bot-state");
} catch {
console.log(
diff --git a/scripts/ci/pr-bot/shared/pr.ts b/scripts/ci/pr-bot/shared/pr.ts
index 2f64572068e..9fee0c6cb9a 100644
--- a/scripts/ci/pr-bot/shared/pr.ts
+++ b/scripts/ci/pr-bot/shared/pr.ts
@@ -25,6 +25,7 @@ export class Pr {
public stopReviewerNotifications: boolean;
public remindAfterTestsPass: string[];
public committerAssigned: boolean;
+ public reviewersAssignedAt: number;
constructor(propertyDictionary) {
this.commentedAboutFailingChecks = false;
@@ -33,6 +34,7 @@ export class Pr {
this.stopReviewerNotifications = false;
this.remindAfterTestsPass = []; // List of handles
this.committerAssigned = false;
+ this.reviewersAssignedAt = 0;
if (!propertyDictionary) {
return;
@@ -59,6 +61,9 @@ export class Pr {
if ("committerAssigned" in propertyDictionary) {
this.committerAssigned = propertyDictionary["committerAssigned"];
}
+ if ("reviewersAssignedAt" in propertyDictionary) {
+ this.reviewersAssignedAt = propertyDictionary["reviewersAssignedAt"];
+ }
}
}
diff --git a/scripts/ci/pr-bot/shared/userCommand.ts
b/scripts/ci/pr-bot/shared/userCommand.ts
index 1c545f36dfd..e1bb6067903 100644
--- a/scripts/ci/pr-bot/shared/userCommand.ts
+++ b/scripts/ci/pr-bot/shared/userCommand.ts
@@ -41,7 +41,7 @@ export async function processCommand(
commentText = commentText.toLowerCase();
let prState = await stateClient.getPrState(pullNumber);
- if(prState.stopReviewerNotifications) {
+ if (prState.stopReviewerNotifications) {
// Notifications stopped, only "allow assign set of reviewers"
if (commentText.indexOf("assign set of reviewers") > -1) {
await assignReviewerSet(payload, pullNumber, stateClient,
reviewerConfig);
@@ -187,7 +187,7 @@ async function assignReviewerSet(
reviewerConfig: typeof ReviewerConfig
) {
let prState = await stateClient.getPrState(pullNumber);
- if(prState.stopReviewerNotifications) {
+ if (prState.stopReviewerNotifications) {
// Restore notifications, and clear any existing reviewer set to
// allow new reviewers to be assigned.
prState.stopReviewerNotifications = false;
diff --git a/scripts/ci/pr-bot/test/githubUtilsTest.ts
b/scripts/ci/pr-bot/test/githubUtilsTest.ts
new file mode 100644
index 00000000000..b6da236247a
--- /dev/null
+++ b/scripts/ci/pr-bot/test/githubUtilsTest.ts
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+var assert = require("assert");
+const { hasLabel } = require("../shared/githubUtils");
+
+describe("githubUtils", function () {
+ describe("hasLabel()", function () {
+ it("should return true when label object matches exactly", function () {
+ const pull = {
+ labels: [{ name: "awaiting triage" }, { name: "go" }],
+ };
+ assert.equal(hasLabel(pull, "awaiting triage"), true);
+ });
+
+ it("should return true when label object matches case-insensitively",
function () {
+ const pull = {
+ labels: [{ name: "Awaiting Triage" }],
+ };
+ assert.equal(hasLabel(pull, "awaiting triage"), true);
+ assert.equal(hasLabel(pull, "AWAITING TRIAGE"), true);
+ });
+
+ it("should return true when label is a string", function () {
+ const pull = {
+ labels: ["reassigned-reviewers", "go"],
+ };
+ assert.equal(hasLabel(pull, "reassigned-reviewers"), true);
+ assert.equal(hasLabel(pull, "REASSIGNED-REVIEWERS"), true);
+ });
+
+ it("should return false when label is not present", function () {
+ const pull = {
+ labels: [{ name: "go" }, { name: "python" }],
+ };
+ assert.equal(hasLabel(pull, "awaiting triage"), false);
+ assert.equal(hasLabel(pull, "reassigned-reviewers"), false);
+ });
+
+ it("should return false when pull or labels are empty or missing",
function () {
+ assert.equal(hasLabel({}, "awaiting triage"), false);
+ assert.equal(hasLabel({ labels: [] }, "awaiting triage"), false);
+ assert.equal(hasLabel(null, "awaiting triage"), false);
+ });
+ });
+});
+
+export {};
diff --git a/scripts/ci/pr-bot/test/prTest.ts b/scripts/ci/pr-bot/test/prTest.ts
index b771852584a..50ac28636a7 100644
--- a/scripts/ci/pr-bot/test/prTest.ts
+++ b/scripts/ci/pr-bot/test/prTest.ts
@@ -42,4 +42,17 @@ describe("Pr", function () {
assert.equal("", testPr.getLabelForReviewer("testReviewer4"));
});
});
+
+ describe("reviewersAssignedAt", function () {
+ it("should default to 0 when not provided", function () {
+ let testPr = new Pr({});
+ assert.equal(testPr.reviewersAssignedAt, 0);
+ });
+
+ it("should retain reviewersAssignedAt when provided in dictionary",
function () {
+ let timestamp = 1700000000000;
+ let testPr = new Pr({ reviewersAssignedAt: timestamp });
+ assert.equal(testPr.reviewersAssignedAt, timestamp);
+ });
+ });
});