mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 06:20:44 +00:00
Fix PR gate trusting author_association for org membership
author_association comes back as CONTRIBUTOR or NONE for team-based contributors (e.g. members of the Contributor team), so the gate was auto-closing PRs from people who clearly have write access. Replace the author_association check with a live permission lookup via repos.getCollaboratorPermissionLevel, which resolves direct, team, and org access in one call. PRs from anyone with write/maintain/admin now bypass the gate.
This commit is contained in:
@@ -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;
|
||||
|
||||
|
||||
@@ -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<string> {
|
||||
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,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getIssue,
|
||||
getPR,
|
||||
getPRFiles,
|
||||
getRepoPermission,
|
||||
makeOctokit,
|
||||
postComment,
|
||||
} from "./github";
|
||||
@@ -57,7 +58,12 @@ async function main(): Promise<void> {
|
||||
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") {
|
||||
|
||||
+15
-10
@@ -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<string>;
|
||||
|
||||
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<RuleResult> {
|
||||
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<RuleResult> {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user