Skip to content

Commit

Permalink
Adds auto approval and fixes some issues with test deploy (#2177)
Browse files Browse the repository at this point in the history
  • Loading branch information
ravenac95 authored Sep 19, 2024
1 parent 451a97c commit a8ffe2d
Show file tree
Hide file tree
Showing 5 changed files with 170 additions and 93 deletions.
18 changes: 12 additions & 6 deletions .github/workflows/test-deploy-owners.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_TOOLS_GITHUB_APP_PRIVATE_KEY: ${{ secrets.PR_TOOLS_GITHUB_APP_PRIVATE_KEY }}
PR_TOOLS_GITHUB_APP_ID: ${{ secrets.PR_TOOLS_GITHUB_APP_ID }}
PR_TOOLS_ADMIN_TEAM_NAME: ${{ secrets.PR_TOOLS_ADMIN_TEAM_NAME }}

# should not be set to a legitimate value for testing. This will use up API
# quota otherwise
Expand Down Expand Up @@ -43,16 +44,23 @@ jobs:
cd ops/external-prs &&
pnpm tools initialize-check ${{ github.event.pull_request.head.sha }} ${{ github.event.pull_request.user.login }} test-deploy
- name: Author association debug
- name: Check if the user is an admin
id: prs_permissions
run: |
echo "${{ github.event.pull_request.author_association }}"
cd ops/external-prs &&
pnpm tools common is-repo-admin ${{ github.event.pull_request.user.login }} --output-file $GITHUB_OUTPUT
- name: Auto-approve PR if conditions are met
run: |
cd ops/external-prs &&
pnpm tools common attempt-auto-approve ${{ github.event.pull_request.number }}
- name: Login to google
uses: "google-github-actions/auth@v2"
with:
credentials_json: "${{ secrets.GOOGLE_BQ_ADMIN_CREDENTIALS_JSON }}"
create_credentials_file: true
if: ${{ contains(fromJson('["OWNER", "MEMBER", "COLLABORATOR" ]'), github.event.pull_request.author_association) }}
if: ${{ steps.prs_permissions.outputs.is_admin == '1' }}

- name: Run test-deploy
uses: ./.github/workflows/test-deploy
Expand All @@ -64,6 +72,4 @@ jobs:
gcp_service_account_path: ${{ env.GOOGLE_APPLICATION_CREDENTIALS }}
google_project_id: ${{ vars.GOOGLE_PROJECT_ID }}

# This check isn't for security it's mostly a convenience so this won't
# fail and muddy up the actions UI
if: ${{ contains(fromJson('["OWNER", "MEMBER", "COLLABORATOR" ]'), github.event.pull_request.author_association) }}
if: ${{ steps.prs_permissions.outputs.is_admin == '1' }}
31 changes: 30 additions & 1 deletion ops/external-prs/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export interface BaseArgs {
repo: Repo;
app: App;
appUtils: GHAppUtils;
adminTeamName: string;
}

export interface CommentCommand {
Expand Down Expand Up @@ -51,13 +52,20 @@ export type CommmentCommandHandler<T> = (command: CommentCommand) => Promise<T>;
export class GHAppUtils {
private app: App;
private repo: Repo;
private adminTeamName: string;
private octo: Octokit;
private appMeta: AppMeta;

constructor(app: App, repo: Repo, octoAndMeta: OctokitAndAppMeta) {
constructor(
app: App,
repo: Repo,
adminTeamName: string,
octoAndMeta: OctokitAndAppMeta,
) {
this.app = app;
this.repo = repo;
this.octo = octoAndMeta.octo;
this.adminTeamName = adminTeamName;
this.appMeta = octoAndMeta.meta;
}

Expand Down Expand Up @@ -162,6 +170,27 @@ export class GHAppUtils {
});
}

async isLoginOnTeam(login: string, team: string) {
const teamMembers = await this.octo.rest.teams.listMembersInOrg({
team_slug: team,
org: this.repo.owner,
});

const teamLogins = teamMembers.data.map((member) =>
member.login.toLowerCase(),
);

// Check the user's membership on the team
return teamLogins.indexOf(login.toLowerCase()) !== -1;
}

async isLoginOnAdminTeam(login: string) {
logger.info({
message: "checking admin team membership",
});
return this.isLoginOnTeam(login, this.adminTeamName);
}

async setCheckStatus(request: CheckRequest) {
return setCheckStatus(this.octo, this.repo.owner, this.repo.name, request);
}
Expand Down
18 changes: 17 additions & 1 deletion ops/external-prs/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@ import { Repo, getOctokitFor, getRepoPermissions } from "./github.js";
import { osoSubcommands } from "./oso/index.js";
import { BaseArgs, GHAppUtils } from "./base.js";
import { ossdSubcommands } from "./ossd/index.js";
import { commonSubcommands } from "./common/index.js";

dotenv.config();

type BeforeClientArgs = ArgumentsCamelCase<{
"github-app-private-key": unknown;
"github-app-id": unknown;
"admin-team-name": string;
}>;

interface InitializePRCheck extends BaseArgs {
Expand Down Expand Up @@ -104,6 +106,12 @@ const cli = yargs(hideBin(process.argv))
type: "string",
demandOption: true,
})
.option("admin-team-name", {
description: "The admin team within the repo's organization",
type: "string",
default: "",
demandOption: false,
})
.middleware(async (args: BeforeClientArgs) => {
// Get base64-encoded private key from the environment
const buf = Buffer.from(args.githubAppPrivateKey as string, "base64"); // Ta-da
Expand All @@ -115,11 +123,19 @@ const cli = yargs(hideBin(process.argv))
args.app = app;
const repo = args.repo as Repo;
const octokitAndAppMeta = await getOctokitFor(app, repo);
args.appUtils = new GHAppUtils(app, repo, octokitAndAppMeta);
args.appUtils = new GHAppUtils(
app,
repo,
args.adminTeamName,
octokitAndAppMeta,
);

const { data } = await app.octokit.request("/app");
logger.debug(`Authenticated as ${data.name}`);
})
.command("common", "common commands", (yags) => {
commonSubcommands(yags);
})
.command("oso", "oso related commands", (yags) => {
osoSubcommands(yags);
})
Expand Down
111 changes: 111 additions & 0 deletions ops/external-prs/src/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { Argv } from "yargs";

import { logger } from "../utils/logger.js";
import { handleError } from "../utils/error.js";
import { getOctokitFor } from "../github.js";
import { BaseArgs } from "../base.js";
import * as fsPromise from "fs/promises";

interface AttemptAutoApproveArgs extends BaseArgs {
pr: number;
}

interface IsRepoAdminArgs extends BaseArgs {
login: string;
outputFile: string;
}

export function commonSubcommands(yargs: Argv) {
yargs
.command<AttemptAutoApproveArgs>(
"attempt-auto-approve <pr>",
"Attempts to auto approve a PR",
(yags) => {
yags.positional("pr", {
type: "string",
description: "The pr number",
});
},
(args) => handleError(attemptAutoApprove(args)),
)
.command<IsRepoAdminArgs>(
"is-repo-admin <login>",
"checks if the current user is the repo admin",
(yags) => {
yags.positional("login", {
type: "string",
description: "The login to check for admin status",
});
yags.option("output-file", {
type: "string",
description: "optional output file",
});
},
(args) => handleError(isRepoAdmin(args)),
)
.demandCommand();
}

async function isRepoAdmin(args: IsRepoAdminArgs) {
if (!(await args.appUtils.isLoginOnAdminTeam(args.login))) {
if (args.outputFile) {
await fsPromise.appendFile(args.outputFile, "is_admin=0\nteam=none");
}
logger.info("user is not admin");
} else {
if (args.outputFile) {
await fsPromise.appendFile(args.outputFile, "is_admin=1\nteam=admin");
}
logger.info("user is an admin");
}
}

async function attemptAutoApprove(args: AttemptAutoApproveArgs) {
logger.info({
message: "loading the pr",
pr: args.pr,
});

const app = args.app;
const { octo } = await getOctokitFor(app, args.repo);
if (!octo) {
throw new Error("No repo found");
}

const pr = await octo.rest.pulls.get({
repo: args.repo.name,
owner: args.repo.owner,
pull_number: args.pr,
});

// check if the PR is already approved. If so, do nothing
const currentReviews = await octo.rest.pulls.listReviews({
repo: args.repo.name,
owner: args.repo.owner,
pull_number: args.pr,
});

const approvedReviews = currentReviews.data.filter(
(review) => review.state == "APPROVED",
);
if (approvedReviews.length > 0) {
logger.info("pr already approved");
return;
}

const login = pr.data.user.login;

if (!(await args.appUtils.isLoginOnAdminTeam(login))) {
logger.info({ message: "creator is not an admin", creator: login });
return;
}
logger.info({ message: "approving the pr" });

await octo.rest.pulls.createReview({
owner: args.repo.owner,
repo: args.repo.name,
pull_number: args.pr,
event: "APPROVE",
body: "Auto-approved! please merge responsibly :smile:",
});
}
85 changes: 0 additions & 85 deletions ops/external-prs/src/ossd/compare.ts

This file was deleted.

0 comments on commit a8ffe2d

Please sign in to comment.