Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions github-actions/post-approval-changes/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
load("@devinfra_npm//:defs.bzl", "npm_link_all_packages")
load("//tools:defaults.bzl", "esbuild_checked_in", "ts_project")
load("//tools:defaults.bzl", "esbuild_checked_in", "jasmine_test", "ts_project")

package(default_visibility = ["//github-actions/post-approval-changes:__subpackages__"])

npm_link_all_packages()

ts_project(
name = "lib",
srcs = glob(["lib/*.ts"]),
srcs = glob(
["lib/*.ts"],
exclude = ["lib/*.spec.ts"],
),
tsconfig = "//github-actions:tsconfig",
deps = [
":node_modules/@actions/core",
Expand All @@ -19,6 +22,28 @@ ts_project(
],
)

ts_project(
name = "test_lib",
testonly = True,
srcs = glob([
"lib/*.spec.ts",
]),
tsconfig = "//github-actions:tsconfig_test",
visibility = [
"//github-actions/post-approval-changes:__subpackages__",
],
deps = [
":lib",
":node_modules/@actions/core",
":node_modules/@actions/github",
":node_modules/@octokit/rest",
":node_modules/@octokit/webhooks-types",
":node_modules/@types/node",
"//github-actions:node_modules/@types/jasmine",
"//github-actions:utils",
],
)

esbuild_checked_in(
name = "main",
srcs = [
Expand All @@ -29,3 +54,10 @@ esbuild_checked_in(
platform = "node",
target = "node24",
)

jasmine_test(
name = "test",
data = [
":test_lib",
],
)
149 changes: 2 additions & 147 deletions github-actions/post-approval-changes/lib/main.ts
Original file line number Diff line number Diff line change
@@ -1,157 +1,12 @@
import * as core from '@actions/core';
import {context} from '@actions/github';
import {PullRequestEvent} from '@octokit/webhooks-types';
import {Octokit, RestEndpointMethodTypes} from '@octokit/rest';
import {
ANGULAR_ROBOT,
getAuthTokenFor,
isGooglerOrgMember,
revokeActiveInstallationToken,
} from '../../utils.js';

/** Allowlist of known Google owned robot accounts. */
const googleOwnedRobots = ['angular-robot'];

async function main() {
let repoClient: Octokit | null = null;
let googlersOrgClient: Octokit | null = null;

try {
const repoToken = await getAuthTokenFor(ANGULAR_ROBOT, context.repo);
const googlersOrgToken = await getGooglersOrgInstallationToken();

repoClient = new Octokit({auth: repoToken});

if (googlersOrgToken !== null) {
googlersOrgClient = new Octokit({auth: googlersOrgToken});
}

await runPostApprovalChangesAction(googlersOrgClient ?? repoClient, repoClient);
} finally {
if (googlersOrgClient !== null) {
await revokeActiveInstallationToken(googlersOrgClient);
}
if (repoClient !== null) {
await revokeActiveInstallationToken(repoClient);
}
}
}

async function getGooglersOrgInstallationToken(): Promise<string | null> {
try {
// Use the `.github` repo from googlers to get an installation that has access to the googlers
// user membership.
return await getAuthTokenFor(ANGULAR_ROBOT, {
org: 'googlers',
});
} catch (e) {
console.error('Could not retrieve installation token for `googlers` org.');
console.error(e);
}

return null;
}

async function runPostApprovalChangesAction(
membershipCheckClient: Octokit,
repoClient: Octokit,
): Promise<void> {
if (context.eventName !== 'pull_request_target') {
throw Error('This action can only run for with pull_request_target events');
}
const {pull_request: pr} = context.payload as PullRequestEvent;

const actionUser = context.actor;

if (await isGooglerOrgMember(actionUser, membershipCheckClient)) {
core.info(
'Action performed by an account in the Googler Github Org, skipping as post approval changes are allowed.',
);
return;
}

if (googleOwnedRobots.includes(actionUser)) {
core.info(
'Action performed by a robot owned by Google, skipping as post approval changes are allowed.',
);
return;
}

console.debug(`Requested Reviewers: ${pr.requested_reviewers.join(', ')}`);
console.debug(`Requested Teams: ${pr.requested_teams.join(', ')}`);

if ([...pr.requested_reviewers, ...pr.requested_teams].length > 0) {
core.info('Skipping check as there are still pending reviews.');
return;
}

/** The repository and owner for the pull request. */
const {repo, owner} = context.issue;
/** The number of the pull request. */
const pull_number = context.issue.number;

/** List of reviews for the pull request. */
const allReviews = await repoClient.paginate(repoClient.pulls.listReviews, {
owner,
pull_number,
repo,
});
/** Set of reviewers whose latest review has already been processed. */
const knownReviewers = new Set<string>();
/** The latest approving reviews for each reviewer on the pull request. */
const reviews: RestEndpointMethodTypes['pulls']['listReviews']['response']['data'] = [];

// Use new instance of array before reversing it.
for (let review of allReviews.concat().reverse()) {
/** The username of the reviewer, since all reviewers are users this should always exist. */
const user = review.user!.login;
if (knownReviewers.has(user)) {
continue;
}
// Only consider reviews by Googlers for this check.
if (!(await isGooglerOrgMember(user, membershipCheckClient))) {
continue;
}
knownReviewers.add(user);
reviews.push(review);
}

console.group('Latest Reviews by Reviewer:');
for (let review of reviews) {
console.log(`${review.user?.login} - ${review.state}`);
}
console.groupEnd();

if (reviews.length === 0) {
core.info('Skipping check as their are no reviews on the pull request.');
return;
}

if (reviews.find((review) => review.state !== 'APPROVED')) {
core.info('Skipping check as there are still non-approved review states.');
return;
}

if (reviews.find((review) => review.commit_id === pr.head.sha)) {
core.info(`Passing check as at least one reviews is for the latest commit on the pull request`);
return;
}

const reviewToRerequest = reviews[0];
core.info(`Requesting a new review from ${reviewToRerequest.user!.login}`);
await repoClient.pulls.requestReviewers({
owner,
pull_number,
repo,
reviewers: [reviewToRerequest.user!.login],
});
}
import {postApprovalChangesMain} from './post-approval-changes.js';

// Only run if the action is executed in a repository with is in the Angular org. This is in place
// to prevent the action from actually running in a fork of a repository with this action set up.
// Runs triggered via 'workflow_dispatch' are also allowed to run.
if (context.repo.owner === 'angular') {
main().catch((e: Error) => {
postApprovalChangesMain().catch((e: Error) => {
console.error(e);
console.error(e.stack);
core.setFailed(e.message);
Expand Down
Loading
Loading