Skip to content

Commit

Permalink
update pr-review.cjs to grab only PRs with 'needs-review'
Browse files Browse the repository at this point in the history
  • Loading branch information
wexxlee committed Mar 10, 2024
1 parent 36d58a3 commit 5bc3a8d
Showing 1 changed file with 86 additions and 69 deletions.
155 changes: 86 additions & 69 deletions .github/scripts/pr-review.cjs
Original file line number Diff line number Diff line change
@@ -1,111 +1,128 @@
/**
* This script will automatically check all PRs in the repository, and for any that have
* the 'needs-review' label *and* a review from a contributor posted after
* the date of the most recent commit, it will remove the 'needs-review' label.
* This script will automatically check all PRs in the repository with the 'needs-review' label,
* and will remove the label for any that have a contributor review after the most recent commit.
*
* This can be tested locally with the Github CLI installed, with:
* set GH_TOKEN=**** GH_REPO=OverlayPlugin/cactbot
* set GH_TOKEN=**** GITHUB_REPOSITORY=OverlayPlugin/cactbot
* node ./.github/scripts/pr-review.cjs
*/
'use strict';

const github = require('@actions/github');
const { execSync } = require('child_process');

const label = 'needs-review';
const validReviewerRoles = ['COLLABORATOR', 'OWNER'];

/**
* @typedef {ReturnType<typeof import("@actions/github").getOctokit>} GitHub
* @typedef {{ owner: string, repo: string, pull_number: number }} identifier
*/

/**
* @param {GitHub} github
* @param {string} owner
* @param {string} repo
* @returns {Promise<void>}
* @returns {Promise<number[]>}
*/
const checkAllPRs = async (github, owner, repo) => {
// Start by grabbing all PRs (including closed), as the workflow that fires this script
// will have latency and otherwise wouldn't pick up PRs that are approved and merged
// before the script has time to complete.
const iterator = github.paginate.iterator(github.rest.pulls.list, {
owner: owner,
repo: repo,
per_page: 100, // eslint-disable-line camelcase
state: 'all',
});
const getPRsByLabel = async (github, owner, repo) => {
// Get all PRs (including closed) that have the requisite label, as the triggering workflow
// has some latency for setup tasks, and otherwise wouldn't pick up approved PRs that are merged
// before this script has time to complete.

// iterate through each response
console.log('Fetching PRs...');
for await (const { data: prs } of iterator) {
/**
* @type {number[]}
*/
const matchingPRs = [];
const iterator = github.paginate.iterator(
github.rest.search.issuesAndPullRequests,
{
q: `type:pr+repo:${owner}/${repo}+label:${label}`,
per_page: 100, // eslint-disable-line camelcase
},
);
for await (const page of iterator) {
const prs = page.data;
if (prs.length === 0)
break;
for (const pr of prs) {
const prNumber = pr.number;

// check if the PR has a `needs-review` label; if not, skip for efficiency
const prLabels = pr.labels;
const hasNeedsReviewLabel = prLabels
.map((label) => label.name)
.includes('needs-review');
if (!hasNeedsReviewLabel)
continue;
matchingPRs.push(pr.number);
}
}
return matchingPRs;
};

console.log(`PR #${pr.number} has 'needs-review' label. Checking...`);
/**
* @param {GitHub} github
* @param {string} owner
* @param {string} repo
* @param {number[]} prs
* @returns {Promise<void>}
*/
const checkAndRelabelPRs = async (github, owner, repo, prs) => {
for (const prNumber of prs) {
/**
* @type identifier
*/
const prIdentifier = { 'owner': owner, 'repo': repo, 'pull_number': prNumber };
const { data: pr } = await github.rest.pulls.get(prIdentifier);
console.log(`Evaluating PR #${prNumber} (state: ${pr.state})...`);

// use the PR created date as the starting point, in case all commits
// are from before the PR was opened.
console.log(`PR created on: ${pr.created_at}`);
let latestCommitDate = new Date(pr.created_at).valueOf();
let latestReviewDate = 0;
// use the PR create-date as the starting point, in case all commits
// are from before the PR was opened.
console.log(`PR #${prNumber} created on: ${pr.created_at}`);
let latestCommitTimestamp = new Date(pr.created_at).valueOf();

const { data: prCommits } = await github.rest.pulls.listCommits({
owner: owner,
repo: repo,
pull_number: prNumber, // eslint-disable-line camelcase
const { data: prCommits } = await github.rest.pulls.listCommits(prIdentifier);
if (prCommits)
prCommits.forEach((commit) => {
console.log(`Found commit ${commit.sha} (date: ${commit.commit.author.date})`);
const commitTimestamp = new Date(commit.commit.author.date).valueOf();
latestCommitTimestamp = Math.max(latestCommitTimestamp, commitTimestamp);
});
console.log(`Using ${new Date(latestCommitTimestamp).toISOString()} as last commit date.`);

if (prCommits)
prCommits.forEach((commit) => {
console.log(`Found commit ${commit.sha} (date: ${commit.commit.author.date})`);
const commitDate = new Date(commit.commit.author.date).valueOf();
latestCommitDate = Math.max(latestCommitDate, commitDate);
});
console.log(`Using latest commit date: ${new Date(latestCommitDate).toISOString()}`);

const { data: prReviews } = await github.rest.pulls.listReviews({
owner: owner,
repo: repo,
pull_number: prNumber, // eslint-disable-line camelcase
console.log(`Checking for valid contributor reviews...`);
let latestReviewTimestamp = 0;
const { data: prReviews } = await github.rest.pulls.listReviews(prIdentifier);
if (prReviews)
prReviews.forEach((review) => {
const reviewTimestamp = new Date(review.submitted_at).valueOf();
if (validReviewerRoles.includes(review.author_association)) {
console.log(`Found valid review ${review.id} (date: ${review.submitted_at})`);
latestReviewTimestamp = Math.max(latestReviewTimestamp, reviewTimestamp);
}
});
if (prReviews)
prReviews.forEach((review) => {
const reviewDate = new Date(review.submitted_at).valueOf();
const reviewerRole = review.author_association;
if (reviewerRole === 'COLLABORATOR' || reviewerRole === 'OWNER') {
console.log(`Found valid review ${review.id} (date: ${review.submitted_at})`);
latestReviewDate = Math.max(latestReviewDate, reviewDate);
}
});

if (latestReviewDate > 0) {
console.log(`Using latest review date: ${new Date(latestReviewDate).toISOString()}`);
if (latestReviewDate > latestCommitDate) {
console.log(`PR #${prNumber} has a post-commit review; removing 'needs-review' label.`);
execSync(`gh pr edit ${prNumber} --remove-label "needs-review"`);
} else {
console.log(`PR #${prNumber} has no review after the latest commit; skipping.`);
}
if (latestReviewTimestamp > 0) {
console.log(`Using ${new Date(latestReviewTimestamp).toISOString()} as last review date.`);
if (latestReviewTimestamp > latestCommitTimestamp) {
console.log(`PR #${prNumber} has a post-commit review; removing 'needs-review' label.`);
execSync(`gh pr edit ${prNumber} --remove-label "needs-review"`);
} else {
console.log(`PR #${prNumber} has no reviews; skipping.`);
console.log(`PR #${prNumber} has no review after the latest commit; skipping.`);
}
} else {
console.log(`PR #${prNumber} has no reviews; skipping.`);
}
console.log(`Evaluation of PR #${prNumber} complete.`);
}
console.log('Update complete.');
};

const run = async () => {
const owner = github.context.repo.owner;
const repo = github.context.repo.repo;
const octokit = github.getOctokit(process.env.GH_TOKEN);
const prs = await getPRsByLabel(octokit, owner, repo);

if (prs.length === 0) {
console.log(`No PRs found with the required label. Job complete.`);
return;
}
console.log(`Found ${prs.length} PRs with the required label. Checking each...`);

await checkAllPRs(octokit, owner, repo);
await checkAndRelabelPRs(octokit, owner, repo, prs);
console.log(`Labeling update complete.`);
};

run().catch((e) => {
Expand Down

0 comments on commit 5bc3a8d

Please sign in to comment.