Files
OpenFrontIO/scripts/pr-gate/github.ts
T
Evan b56e9438d1 github PR gate (#4070)
## Description:

## Summary

Adds a GitHub Action that auto-closes PRs which don't follow the
contribution workflow, so maintainer review time goes to legitimate
contributions instead of off-roadmap or AI-generated submissions.

Triggered on `pull_request_target: [opened, reopened]` and **defaults to
dry-run** so it's safe to merge before flipping live.

## Gate logic (first match wins)

1. **Maintainer bypass** — PR carries `bypass-pr-check` label → pass.
2. **Org/repo member** — `author_association` is `OWNER` / `MEMBER` /
`COLLABORATOR` → pass.
3. **Approved work** — PR body links an issue (`Closes/Fixes/Resolves
#N`) that carries the `approved` label and the PR author is in the
issue's assignees → pass.
4. **Small fix** — `additions + deletions ≤ 50` → pass + apply
`small-fix` label.
5. **Otherwise** — apply `auto-closed-needs-issue` label, post rejection
comment, close.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

evan
2026-05-29 18:28:52 -07:00

125 lines
2.9 KiB
TypeScript

import { Octokit } from "@octokit/rest";
import { LABEL_COLORS, LABEL_DESCRIPTIONS, REPO } from "./config";
import type { IssueMetadata, PRFile, PRMetadata } from "./rules";
export function makeOctokit(token: string): Octokit {
return new Octokit({ auth: token });
}
export async function getPR(
octokit: Octokit,
prNumber: number,
): Promise<PRMetadata> {
const { data } = await octokit.rest.pulls.get({
...REPO,
pull_number: prNumber,
});
return {
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),
};
}
export async function getPRFiles(
octokit: Octokit,
prNumber: number,
): Promise<PRFile[]> {
const files = await octokit.paginate(octokit.rest.pulls.listFiles, {
...REPO,
pull_number: prNumber,
per_page: 100,
});
return files.map((f) => ({ additions: f.additions, deletions: f.deletions }));
}
export async function getIssue(
octokit: Octokit,
issueNumber: number,
): Promise<IssueMetadata | null> {
try {
const { data } = await octokit.rest.issues.get({
...REPO,
issue_number: issueNumber,
});
return {
number: data.number,
labels: (data.labels ?? [])
.map((l) => (typeof l === "string" ? l : (l.name ?? "")))
.filter((name) => name.length > 0),
assignees: (data.assignees ?? []).map((a) => ({ login: a.login })),
};
} catch (err) {
if (isStatus(err, 404)) {
console.warn(`[pr-gate] Issue #${issueNumber} not found, skipping`);
return null;
}
throw err;
}
}
export async function ensureLabel(
octokit: Octokit,
name: string,
): Promise<void> {
try {
await octokit.rest.issues.getLabel({ ...REPO, name });
} catch (err) {
if (!isStatus(err, 404)) throw err;
await octokit.rest.issues.createLabel({
...REPO,
name,
color: LABEL_COLORS[name],
description: LABEL_DESCRIPTIONS[name],
});
}
}
export async function addLabel(
octokit: Octokit,
prNumber: number,
label: string,
): Promise<void> {
await octokit.rest.issues.addLabels({
...REPO,
issue_number: prNumber,
labels: [label],
});
}
export async function postComment(
octokit: Octokit,
prNumber: number,
body: string,
): Promise<void> {
await octokit.rest.issues.createComment({
...REPO,
issue_number: prNumber,
body,
});
}
export async function closePR(
octokit: Octokit,
prNumber: number,
): Promise<void> {
await octokit.rest.pulls.update({
...REPO,
pull_number: prNumber,
state: "closed",
});
}
function isStatus(err: unknown, status: number): boolean {
return (
typeof err === "object" &&
err !== null &&
"status" in err &&
(err as { status: unknown }).status === status
);
}