diff --git a/scripts/pr-gate/config.ts b/scripts/pr-gate/config.ts index 3779dabb9..b92e52515 100644 --- a/scripts/pr-gate/config.ts +++ b/scripts/pr-gate/config.ts @@ -1,10 +1,6 @@ export const REPO = { owner: "openfrontio", repo: "OpenFrontIO" } as const; -export const TRUSTED_AUTHOR_ASSOCIATIONS = [ - "OWNER", - "MEMBER", - "COLLABORATOR", -] as const; +export const TRUSTED_REPO_PERMISSIONS = ["admin", "maintain", "write"] as const; export const SMALL_FIX_LINE_THRESHOLD = 50; diff --git a/scripts/pr-gate/github.ts b/scripts/pr-gate/github.ts index 3d589b603..79adf55f8 100644 --- a/scripts/pr-gate/github.ts +++ b/scripts/pr-gate/github.ts @@ -18,7 +18,6 @@ export async function getPR( number: data.number, body: data.body ?? null, user: { login: data.user?.login ?? "" }, - author_association: data.author_association, labels: (data.labels ?? []) .map((l) => l.name ?? "") .filter((name) => name.length > 0), @@ -37,6 +36,22 @@ export async function getPRFiles( return files.map((f) => ({ additions: f.additions, deletions: f.deletions })); } +export async function getRepoPermission( + octokit: Octokit, + username: string, +): Promise { + try { + const { data } = await octokit.rest.repos.getCollaboratorPermissionLevel({ + ...REPO, + username, + }); + return data.permission; + } catch (err) { + if (isStatus(err, 404)) return "none"; + throw err; + } +} + export async function getIssue( octokit: Octokit, issueNumber: number, diff --git a/scripts/pr-gate/index.ts b/scripts/pr-gate/index.ts index 981c61939..234cbd00c 100644 --- a/scripts/pr-gate/index.ts +++ b/scripts/pr-gate/index.ts @@ -6,6 +6,7 @@ import { getIssue, getPR, getPRFiles, + getRepoPermission, makeOctokit, postComment, } from "./github"; @@ -57,7 +58,12 @@ async function main(): Promise { const pr = await getPR(octokit, prNumber); const files = await getPRFiles(octokit, prNumber); - const decision = await evaluate(pr, files, (n) => getIssue(octokit, n)); + const decision = await evaluate( + pr, + files, + (n) => getIssue(octokit, n), + (u) => getRepoPermission(octokit, u), + ); const prefix = `[pr-gate] PR #${prNumber}`; if (decision.action === "pass") { diff --git a/scripts/pr-gate/rules.ts b/scripts/pr-gate/rules.ts index 229c73934..7c5385c60 100644 --- a/scripts/pr-gate/rules.ts +++ b/scripts/pr-gate/rules.ts @@ -2,17 +2,18 @@ import { APPROVED_ISSUE_LABEL, LABELS, SMALL_FIX_LINE_THRESHOLD, - TRUSTED_AUTHOR_ASSOCIATIONS, + TRUSTED_REPO_PERMISSIONS, } from "./config"; export type PRMetadata = { number: number; body: string | null; user: { login: string }; - author_association: string; labels: string[]; }; +export type GetRepoPermission = (username: string) => Promise; + export type PRFile = { additions: number; deletions: number; @@ -53,13 +54,16 @@ export function checkBypass(pr: PRMetadata): RuleResult { return { action: "next" }; } -export function checkOrgMember(pr: PRMetadata): RuleResult { - if ( - (TRUSTED_AUTHOR_ASSOCIATIONS as readonly string[]).includes( - pr.author_association, - ) - ) { - return { action: "pass", reason: `Author is ${pr.author_association}` }; +export async function checkRepoAccess( + pr: PRMetadata, + getRepoPermission: GetRepoPermission, +): Promise { + const permission = await getRepoPermission(pr.user.login); + if ((TRUSTED_REPO_PERMISSIONS as readonly string[]).includes(permission)) { + return { + action: "pass", + reason: `Author has "${permission}" permission on the repo`, + }; } return { action: "next" }; } @@ -104,11 +108,12 @@ export async function evaluate( pr: PRMetadata, files: PRFile[], getIssue: GetIssue, + getRepoPermission: GetRepoPermission, ): Promise { const r0 = checkBypass(pr); if (r0.action !== "next") return r0; - const r1 = checkOrgMember(pr); + const r1 = await checkRepoAccess(pr, getRepoPermission); if (r1.action !== "next") return r1; const r2 = await checkApprovedWork(pr, getIssue);