chore(github): reject AI co-authored commits from untrusted PRs

This commit is contained in:
maxrave-dev 2026-08-24 17:10:24 +07:00
parent fe5c59a53a
commit 7ad9e5ed23
No known key found for this signature in database
2 changed files with 44 additions and 2 deletions

View file

@ -1,12 +1,16 @@
# Auto-triage for unattended AI-generated pull requests.
#
# Two tiers, both applying ONLY to authors with no history here (everyone but
# Three tiers, all applying ONLY to authors with no history here (everyone but
# OWNER / MEMBER / COLLABORATOR / CONTRIBUTOR):
#
# 1. HARD: the title carries an unattended-agent fingerprint — an agent's name, a
# 12+ digit session id, or a bare commit-hash — the PR is closed on sight,
# labelled `ai-slop`, and pointed at CONTRIBUTING.md.
# 2. SOFT: the PR template's personal-review checkbox is not ticked — the PR stays
# 2. HARD: any commit in the PR carries an AI co-author trailer ("Co-Authored-By:
# Claude/Copilot/…"), a "Generated with …" marker, or is authored by a known
# coding-agent bot account. A leftover trailer is the tell that nobody proofread
# the output before shipping it. Closed with instructions to squash and reopen.
# 3. SOFT: the PR template's personal-review checkbox is not ticked — the PR stays
# OPEN, gets `needs-template` and one comment asking to complete it. Real
# first-time contributors get a nudge, not a door in the face.
#
@ -60,6 +64,41 @@ jobs:
return;
}
// AI marks inside the COMMITS. Named agents only — a plain `[bot]` match would
// also shoot dependabot/renovate, which are welcome here. "gpt" alone is left
// out on purpose (it substring-matches human names); chatgpt/codex cover it.
const aiNames = '(claude|copilot|chatgpt|openai|gemini|jules|devin|cursor|windsurf|aider|codex|sweep|openhands|codegen|anthropic)';
const aiCoauthor = new RegExp('co-authored-by:[^\\n]*\\b' + aiNames + '\\b', 'i');
const generatedWith = new RegExp('generated (with|by)[^\\n]*\\b' + aiNames + '\\b', 'i');
const agentAccount = new RegExp('\\b' + aiNames + '[^\\s]*\\[bot\\]', 'i');
const commits = await github.paginate(github.rest.pulls.listCommits, {
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
per_page: 100,
});
const marked = commits.find((c) => {
const message = c.commit?.message || '';
const identities = [
c.commit?.author?.name, c.commit?.author?.email,
c.commit?.committer?.name, c.commit?.committer?.email,
c.author?.login, c.committer?.login,
].filter(Boolean).join(' ');
return aiCoauthor.test(message) || generatedWith.test(message) || agentAccount.test(identities);
});
if (marked) {
await github.rest.issues.createComment({ ...issue, body:
'One or more commits in this PR carry an AI co-author trailer or agent marker ' +
'(e.g. `Co-Authored-By: Claude/Copilot/…`), which this project does not accept — ' +
'a leftover trailer usually means the output was never personally reviewed. If a ' +
'human does stand behind this change: squash your commits without the AI trailers, ' +
'force-push, complete the PR template, and reopen. See CONTRIBUTING.md.' });
await github.rest.issues.addLabels({ ...issue, labels: ['ai-slop'] });
await github.rest.issues.update({ ...issue, state: 'closed' });
return;
}
// Must match the checkbox text in .github/PULL_REQUEST_TEMPLATE.md verbatim.
const reviewedByHuman = /-\s*\[x\]\s*I wrote or personally reviewed every line/i.test(body);
if (!reviewedByHuman) {

View file

@ -25,6 +25,9 @@ AI-assisted contributions are welcome. AI-*driven* ones are not:
result — are **closed automatically** by the triage workflow, on sight, without
individual discussion. Their fingerprints (agent names, session ids, or bare commit
hashes in the title) are matched by `.github/workflows/pr-triage.yml`.
- **Commits carrying AI co-author trailers** (`Co-Authored-By: Claude/Copilot/…`) or
"Generated with …" markers are rejected the same way — a leftover trailer is the tell
that nobody proofread the output. Squash them out before opening the PR.
- Repeat offenders get blocked from the repository.
This is not hostility toward AI tooling — half this project is built with it. It is the