mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 02:36:01 -10:00
ci: limit pull requests to maintainers and an allowlist of invited authors
This commit is contained in:
1 parent
c43bd2bdc3
commit
60caacd887
5 files changed
+140
No files matched your search
@@ -0,0 +1,7 @@
|
||||
# Logins allowed to open pull requests, one per line, matched case-insensitively.
|
||||
# Repository owners, members and collaborators, and bots, are always allowed.
|
||||
lancy69
|
||||
chuiwenwei
|
||||
xxoingr
|
||||
kevin9327
|
||||
imcreazy
|
||||
@@ -2,6 +2,9 @@
|
||||
Target branch: 2.x features and fixes go to `studio`. 1.x takes bug fixes,
|
||||
provider/API compatibility, release/updater, security and platform stability
|
||||
only, on `main-v2`.
|
||||
|
||||
Pull requests are limited to maintainers and invited contributors; one from
|
||||
anyone else is closed automatically. Open an issue instead.
|
||||
-->
|
||||
|
||||
## Summary
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
name: PR author gate
|
||||
|
||||
# Pull requests are limited to maintainers and the logins in .github/pr-authors.txt.
|
||||
# This runs in the base repository's context, checks out the base branch only, and
|
||||
# never executes pull-request code; the allowlist it reads is the base branch's.
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
concurrency:
|
||||
group: pr-author-gate-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
gate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- run: node scripts/pr-author-gate.mjs
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env node
|
||||
// Closes a newly opened pull request whose author is not on the allowlist.
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { pathToFileURL } from "node:url";
|
||||
|
||||
const MAINTAINERS = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
|
||||
|
||||
export function parseAllowlist(text) {
|
||||
return new Set(text.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith("#"))
|
||||
.map((login) => login.toLowerCase()));
|
||||
}
|
||||
|
||||
export function decide({ association, authorType, login, allowed }) {
|
||||
if (authorType === "Bot" || MAINTAINERS.has(association)) return "allow";
|
||||
return allowed.has(login.toLowerCase()) ? "allow" : "close";
|
||||
}
|
||||
|
||||
export const CLOSING_COMMENT = `Thanks for wanting to contribute. Pull requests are currently limited to the maintainers and a small set of invited contributors, so this one has been closed automatically; nothing is wrong with it.
|
||||
|
||||
Please open an issue instead (a bug report with the steps to reproduce is the most useful). A maintainer will look at it, and if you would like to fix it yourself, say so there: if a maintainer agrees, this pull request can be reopened.`;
|
||||
|
||||
function gh(args, input) {
|
||||
const result = spawnSync("gh", args, { encoding: "utf8", input, maxBuffer: 4 * 1024 * 1024 });
|
||||
if (result.status !== 0) throw new Error(`gh ${args[0]} ${args[1]} failed: ${result.stderr}`);
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
export function run({ event, repository, allowlist, exec = gh, log = console.log }) {
|
||||
const pr = event.pull_request;
|
||||
const verdict = decide({
|
||||
association: pr.author_association, authorType: pr.user.type, login: pr.user.login, allowed: parseAllowlist(allowlist),
|
||||
});
|
||||
log(`#${pr.number} by ${pr.user.login} (${pr.author_association}): ${verdict}`);
|
||||
if (verdict === "allow") return verdict;
|
||||
exec(["pr", "comment", String(pr.number), "--repo", repository, "--body-file", "-"], CLOSING_COMMENT);
|
||||
exec(["pr", "close", String(pr.number), "--repo", repository]);
|
||||
return verdict;
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
const event = JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH, "utf8"));
|
||||
const allowlist = readFileSync(new URL("../.github/pr-authors.txt", import.meta.url), "utf8");
|
||||
run({ event, repository: process.env.GITHUB_REPOSITORY, allowlist });
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
import { CLOSING_COMMENT, decide, parseAllowlist, run } from "./pr-author-gate.mjs";
|
||||
|
||||
const allowlist = "# comment\nLancy69\n\n kevin9327 \n";
|
||||
const event = (login, association = "NONE", type = "User") => ({
|
||||
pull_request: { number: 7, author_association: association, user: { login, type } },
|
||||
});
|
||||
const harness = () => {
|
||||
const calls = [];
|
||||
return { calls, exec: (args, input) => { calls.push({ args, input }); return ""; } };
|
||||
};
|
||||
const go = (e, h) => run({ event: e, repository: "o/r", allowlist, exec: h.exec, log: () => {} });
|
||||
|
||||
test("the allowlist ignores comments and blanks and matches case-insensitively", () => {
|
||||
assert.deepEqual([...parseAllowlist(allowlist)], ["lancy69", "kevin9327"]);
|
||||
assert.equal(go(event("LANCY69", "CONTRIBUTOR"), harness()), "allow");
|
||||
assert.equal(go(event("kevin9327", "FIRST_TIME_CONTRIBUTOR"), harness()), "allow");
|
||||
});
|
||||
|
||||
for (const association of ["OWNER", "MEMBER", "COLLABORATOR"]) {
|
||||
test(`${association} is never gated`, () => {
|
||||
const h = harness();
|
||||
assert.equal(go(event("maintainer", association), h), "allow");
|
||||
assert.deepEqual(h.calls, []);
|
||||
});
|
||||
}
|
||||
|
||||
test("a bot such as dependabot is never gated", () => {
|
||||
const h = harness();
|
||||
assert.equal(go(event("dependabot[bot]", "NONE", "Bot"), h), "allow");
|
||||
assert.deepEqual(h.calls, []);
|
||||
});
|
||||
|
||||
for (const association of ["CONTRIBUTOR", "FIRST_TIME_CONTRIBUTOR", "FIRST_TIMER", "NONE", "MANNEQUIN"]) {
|
||||
test(`${association} who is not on the list is commented and closed, whatever their history`, () => {
|
||||
const h = harness();
|
||||
assert.equal(go(event("someone-else", association), h), "close");
|
||||
assert.deepEqual(h.calls.map((c) => c.args.slice(0, 3)), [["pr", "comment", "7"], ["pr", "close", "7"]]);
|
||||
assert.equal(h.calls[0].input, CLOSING_COMMENT);
|
||||
});
|
||||
}
|
||||
|
||||
test("decide admits only maintainers, bots and listed logins", () => {
|
||||
const allowed = new Set(["a"]);
|
||||
assert.equal(decide({ association: "NONE", authorType: "User", login: "A", allowed }), "allow");
|
||||
assert.equal(decide({ association: "CONTRIBUTOR", authorType: "User", login: "b", allowed }), "close");
|
||||
});
|
||||
|
||||
test("the shipped allowlist parses and the workflow is safe", () => {
|
||||
assert.ok(parseAllowlist(readFileSync(new URL("../.github/pr-authors.txt", import.meta.url), "utf8")).size >= 1);
|
||||
const text = readFileSync(new URL("../.github/workflows/pr-author-gate.yml", import.meta.url), "utf8");
|
||||
assert.match(text, /pull_request_target:\n\s+types: \[opened\]/);
|
||||
assert.doesNotMatch(text, /head\.(sha|ref)|refs\/pull/);
|
||||
assert.match(text, /permissions:\n\s+contents: read\n\s+pull-requests: write/);
|
||||
assert.match(text, /node scripts\/pr-author-gate\.mjs/);
|
||||
});
|
||||
Reference in new issue
Block a user