blob: 96eaa6e22f636d83903cfc2fd5619869ac833b8d [file] [log] [blame]
/*
* 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 exec = require("@actions/exec");
const github = require("./shared/githubUtils");
const commentStrings = require("./shared/commentStrings");
const {
REPO_OWNER,
REPO,
BOT_NAME,
PATH_TO_CONFIG_FILE,
} = require("./shared/constants");
const { ReviewerConfig } = require("./shared/reviewerConfig");
async function getPullsFromLastThreeMonths(): Promise<any[]> {
const cutoffDate = new Date();
cutoffDate.setMonth(cutoffDate.getMonth() - 3);
console.log(`Getting PRs newer than ${cutoffDate}`);
const githubClient = github.getGitHubClient();
let result = await githubClient.rest.pulls.list({
owner: REPO_OWNER,
repo: REPO,
state: "closed",
per_page: 100, // max allowed
});
let page = 2;
let retries = 0;
let pulls = result.data;
while (
result.data.length > 0 &&
new Date(result.data[result.data.length - 1].created_at) > cutoffDate
) {
if (retries === 0) {
console.log(`Getting PRs, page: ${page}`);
console.log(
`Current oldest PR = ${new Date(
result.data[result.data.length - 1].created_at
)}`
);
}
try {
result = await githubClient.rest.pulls.list({
owner: REPO_OWNER,
repo: REPO,
state: "closed",
per_page: 100, // max allowed
page: page,
});
pulls = pulls.concat(result.data);
page++;
retries = 0;
} catch (err) {
if (retries >= 3) {
throw err;
}
retries++;
}
}
return pulls;
}
interface pullActivity {
reviews: number;
pullsAuthored: number;
}
interface reviewerActivity {
reviewers: { [reviewer: string]: pullActivity };
}
async function getReviewersForPull(pull: any): Promise<string[]> {
let reviewers = new Set<string>();
const githubClient = github.getGitHubClient();
let comments = (
await githubClient.rest.issues.listComments({
owner: REPO_OWNER,
repo: REPO,
issue_number: pull.number,
})
).data;
const reviewComments = (
await githubClient.rest.pulls.listReviewComments({
owner: REPO_OWNER,
repo: REPO,
pull_number: pull.number,
})
).data;
comments = comments.concat(reviewComments);
for (const comment of comments) {
if (
comment.user &&
comment.user.login &&
comment.user.login !== pull.user.login &&
comment.user.login !== BOT_NAME
) {
reviewers.add(comment.user.login.toLowerCase());
}
}
return [...reviewers];
}
function addReviewerActivity(
reviewerActivity: reviewerActivity,
reviewers: string[],
author: string
): reviewerActivity {
if (!reviewerActivity) {
reviewerActivity = {
reviewers: {},
};
}
if (author in reviewerActivity.reviewers) {
reviewerActivity.reviewers[author].pullsAuthored++;
} else {
reviewerActivity.reviewers[author] = {
reviews: 0,
pullsAuthored: 1,
};
}
for (const reviewer of reviewers) {
if (reviewer !== author) {
if (reviewer in reviewerActivity.reviewers) {
reviewerActivity.reviewers[reviewer].reviews++;
} else {
reviewerActivity.reviewers[reviewer] = {
reviews: 1,
pullsAuthored: 0,
};
}
}
}
return reviewerActivity;
}
async function getReviewerActivityByLabel(
pulls: any[]
): Promise<{ [label: string]: reviewerActivity }> {
let reviewerActivityByLabel: { [label: string]: reviewerActivity } = {};
for (const pull of pulls) {
console.log(`Processing PR ${pull.number}`);
const author = pull.user.login.toLowerCase();
if (author !== BOT_NAME) {
const reviewers = await getReviewersForPull(pull);
const labels = pull.labels;
for (const label of labels) {
const labelName = label.name.toLowerCase();
reviewerActivityByLabel[labelName] = addReviewerActivity(
reviewerActivityByLabel[labelName],
reviewers,
author
);
}
}
}
return reviewerActivityByLabel;
}
interface configUpdates {
reviewersAddedForLabels: { [reviewer: string]: string[] };
reviewersRemovedForLabels: { [reviewer: string]: string[] };
}
function reviewerIsBot(reviewer: string): boolean {
if (
["codecov", "github-actions"].find(
(bot) => reviewer.toLowerCase().indexOf(bot) != -1
)
) {
return true;
}
return false;
}
function updateReviewerConfig(
reviewerActivityByLabel: { [label: string]: reviewerActivity },
reviewerConfig: typeof ReviewerConfig
): configUpdates {
let updates: configUpdates = {
reviewersAddedForLabels: {},
reviewersRemovedForLabels: {},
};
const currentReviewersForLabels = reviewerConfig.getReviewersForAllLabels();
for (const label of Object.keys(currentReviewersForLabels)) {
// Remove any reviewers with no reviews or pulls created
let reviewers = currentReviewersForLabels[label];
let updatedReviewers: string[] = [];
const exclusionList = reviewerConfig.getExclusionListForLabel(label);
for (const reviewer of reviewers) {
if (reviewerActivityByLabel[label].reviewers[reviewer]) {
updatedReviewers.push(reviewer);
} else {
if (reviewer in updates.reviewersRemovedForLabels) {
updates.reviewersRemovedForLabels[reviewer].push(label);
} else {
updates.reviewersRemovedForLabels[reviewer] = [label];
}
}
}
// Add any reviewers who have at least 5 combined reviews + pulls authored
for (const reviewer of Object.keys(
reviewerActivityByLabel[label].reviewers
)) {
const reviewerContributions =
reviewerActivityByLabel[label].reviewers[reviewer].reviews +
reviewerActivityByLabel[label].reviewers[reviewer].pullsAuthored;
if (
reviewerContributions >= 5 &&
updatedReviewers.indexOf(reviewer) < 0 &&
exclusionList.indexOf(reviewer) < 0 &&
!reviewerIsBot(reviewer)
) {
updatedReviewers.push(reviewer);
if (reviewer in updates.reviewersAddedForLabels) {
updates.reviewersAddedForLabels[reviewer].push(label);
} else {
updates.reviewersAddedForLabels[reviewer] = [label];
}
}
}
console.log(
`Updated reviewers for label ${label}: ${updatedReviewers.join(",")}`
);
reviewerConfig.updateReviewerForLabel(label, updatedReviewers);
}
return updates;
}
async function openPull(updates: configUpdates) {
const curDate = new Date();
const branch = `pr-bot-${
curDate.getMonth() + 1
}-${curDate.getDay()}-${curDate.getFullYear()}-${curDate.getHours()}-${curDate.getMinutes()}-${curDate.getSeconds()}`;
await exec.exec(`git config user.email ${BOT_NAME}@github.com`);
await exec.exec("git config pull.rebase false");
await exec.exec(`git checkout -b ${branch}`);
await exec.exec(`git add ${PATH_TO_CONFIG_FILE}`);
await exec.exec(
`git commit -m "Updating reviewer config based on historical trends"`
);
await exec.exec(`git push origin ${branch}`);
const prBody = commentStrings.updateReviewerConfig(
updates.reviewersAddedForLabels,
updates.reviewersRemovedForLabels
);
await github.getGitHubClient().rest.pulls.create({
owner: REPO_OWNER,
repo: REPO,
head: branch,
base: "master",
title: "Update reviewer config to reviewers with activity in repo areas",
body: prBody,
maintainer_can_modify: true,
});
}
async function updateReviewers() {
const pulls = await getPullsFromLastThreeMonths();
console.log("Got all PRs, moving to the processing stage");
const reviewerActivityByLabel = await getReviewerActivityByLabel(pulls);
console.log("Processed all PRs to determine reviewer activity");
let reviewerConfig = new ReviewerConfig(PATH_TO_CONFIG_FILE);
const updates = updateReviewerConfig(reviewerActivityByLabel, reviewerConfig);
if (
Object.keys(updates.reviewersAddedForLabels).length > 0 ||
Object.keys(updates.reviewersRemovedForLabels).length > 0
) {
console.log(`Suggested updates to ${PATH_TO_CONFIG_FILE}`);
await openPull(updates);
} else {
console.log("No updates to suggest");
}
}
updateReviewers();
export {};