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 REPO = { owner: "openfrontio", repo: "OpenFrontIO" } as const;
|
||||||
|
|
||||||
export const TRUSTED_AUTHOR_ASSOCIATIONS = [
|
export const TRUSTED_REPO_PERMISSIONS = ["admin", "maintain", "write"] as const;
|
||||||
"OWNER",
|
|
||||||
"MEMBER",
|
|
||||||
"COLLABORATOR",
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
export const SMALL_FIX_LINE_THRESHOLD = 50;
|
export const SMALL_FIX_LINE_THRESHOLD = 50;
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export async function getPR(
|
|||||||
number: data.number,
|
number: data.number,
|
||||||
body: data.body ?? null,
|
body: data.body ?? null,
|
||||||
user: { login: data.user?.login ?? "" },
|
user: { login: data.user?.login ?? "" },
|
||||||
author_association: data.author_association,
|
|
||||||
labels: (data.labels ?? [])
|
labels: (data.labels ?? [])
|
||||||
.map((l) => l.name ?? "")
|
.map((l) => l.name ?? "")
|
||||||
.filter((name) => name.length > 0),
|
.filter((name) => name.length > 0),
|
||||||
@@ -37,6 +36,22 @@ export async function getPRFiles(
|
|||||||
return files.map((f) => ({ additions: f.additions, deletions: f.deletions }));
|
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(
|
export async function getIssue(
|
||||||
octokit: Octokit,
|
octokit: Octokit,
|
||||||
issueNumber: number,
|
issueNumber: number,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
getIssue,
|
getIssue,
|
||||||
getPR,
|
getPR,
|
||||||
getPRFiles,
|
getPRFiles,
|
||||||
|
getRepoPermission,
|
||||||
makeOctokit,
|
makeOctokit,
|
||||||
postComment,
|
postComment,
|
||||||
} from "./github";
|
} from "./github";
|
||||||
@@ -57,7 +58,12 @@ async function main(): Promise<void> {
|
|||||||
const pr = await getPR(octokit, prNumber);
|
const pr = await getPR(octokit, prNumber);
|
||||||
const files = await getPRFiles(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}`;
|
const prefix = `[pr-gate] PR #${prNumber}`;
|
||||||
if (decision.action === "pass") {
|
if (decision.action === "pass") {
|
||||||
|
|||||||
+15
-10
@@ -2,17 +2,18 @@ import {
|
|||||||
APPROVED_ISSUE_LABEL,
|
APPROVED_ISSUE_LABEL,
|
||||||
LABELS,
|
LABELS,
|
||||||
SMALL_FIX_LINE_THRESHOLD,
|
SMALL_FIX_LINE_THRESHOLD,
|
||||||
TRUSTED_AUTHOR_ASSOCIATIONS,
|
TRUSTED_REPO_PERMISSIONS,
|
||||||
} from "./config";
|
} from "./config";
|
||||||
|
|
||||||
export type PRMetadata = {
|
export type PRMetadata = {
|
||||||
number: number;
|
number: number;
|
||||||
body: string | null;
|
body: string | null;
|
||||||
user: { login: string };
|
user: { login: string };
|
||||||
author_association: string;
|
|
||||||
labels: string[];
|
labels: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetRepoPermission = (username: string) => Promise<string>;
|
||||||
|
|
||||||
export type PRFile = {
|
export type PRFile = {
|
||||||
additions: number;
|
additions: number;
|
||||||
deletions: number;
|
deletions: number;
|
||||||
@@ -53,13 +54,16 @@ export function checkBypass(pr: PRMetadata): RuleResult {
|
|||||||
return { action: "next" };
|
return { action: "next" };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkOrgMember(pr: PRMetadata): RuleResult {
|
export async function checkRepoAccess(
|
||||||
if (
|
pr: PRMetadata,
|
||||||
(TRUSTED_AUTHOR_ASSOCIATIONS as readonly string[]).includes(
|
getRepoPermission: GetRepoPermission,
|
||||||
pr.author_association,
|
): Promise<RuleResult> {
|
||||||
)
|
const permission = await getRepoPermission(pr.user.login);
|
||||||
) {
|
if ((TRUSTED_REPO_PERMISSIONS as readonly string[]).includes(permission)) {
|
||||||
return { action: "pass", reason: `Author is ${pr.author_association}` };
|
return {
|
||||||
|
action: "pass",
|
||||||
|
reason: `Author has "${permission}" permission on the repo`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return { action: "next" };
|
return { action: "next" };
|
||||||
}
|
}
|
||||||
@@ -104,11 +108,12 @@ export async function evaluate(
|
|||||||
pr: PRMetadata,
|
pr: PRMetadata,
|
||||||
files: PRFile[],
|
files: PRFile[],
|
||||||
getIssue: GetIssue,
|
getIssue: GetIssue,
|
||||||
|
getRepoPermission: GetRepoPermission,
|
||||||
): Promise<RuleResult> {
|
): Promise<RuleResult> {
|
||||||
const r0 = checkBypass(pr);
|
const r0 = checkBypass(pr);
|
||||||
if (r0.action !== "next") return r0;
|
if (r0.action !== "next") return r0;
|
||||||
|
|
||||||
const r1 = checkOrgMember(pr);
|
const r1 = await checkRepoAccess(pr, getRepoPermission);
|
||||||
if (r1.action !== "next") return r1;
|
if (r1.action !== "next") return r1;
|
||||||
|
|
||||||
const r2 = await checkApprovedWork(pr, getIssue);
|
const r2 = await checkApprovedWork(pr, getIssue);
|
||||||
|
|||||||
Reference in New Issue
Block a user