diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-08-06 01:04:38 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-08-06 01:04:50 +0300 |
| commit | 1fd3100d83fe91c4ba6145e216c5b4352e14df4d (patch) | |
| tree | 8cf76fc0ee3c5a1f841a9813c3b6eabe65518ae0 /.github | |
| parent | 2d011c39b4f4cc5edd482ed8493c663463d18701 (diff) | |
ci: restore approved contributor gate
Diffstat (limited to '.github')
| -rw-r--r-- | .github/APPROVED_CONTRIBUTORS | 69 | ||||
| -rw-r--r-- | .github/ISSUE_TEMPLATE/bug.yml | 6 | ||||
| -rw-r--r-- | .github/workflows/pr-gate.yml | 147 |
3 files changed, 131 insertions, 91 deletions
diff --git a/.github/APPROVED_CONTRIBUTORS b/.github/APPROVED_CONTRIBUTORS new file mode 100644 index 00000000..9d5c0631 --- /dev/null +++ b/.github/APPROVED_CONTRIBUTORS @@ -0,0 +1,69 @@ +# GitHub usernames that may submit implementation PRs, one per line. +# This does not approve feature scope or grant maintainer authority; see .github/MAINTAINERS. +ogulcancelik +akbash-bot +kangal-bot +Edmund-a7 +othavioquiliao +edheltzel +EYH0602 +mspiegel31 +fbettag +nexxeln +soomtong +moonsphere +leeeanh +ChihGodlee +babymastodon +cloudmanic +Golden-Pigeon +icedac +LaneBirmingham +chenrui333 +wayneleelwc +reobin +wardpeet +turgaybulut +SunskyXH +sf-jin-ku +DeevsDeevs +wbxl2000 +udirom +imrajyavardhan12 +dzevs +minatoaquaMK2 +dmmulroy +cullendotdev +soar +arunoruto +DevSrSouza +ppggff +TonyxSun +liby +kovalov +tontinton +badlogic +carlesso +joonhwan +adamkrellenstein +corrius +yianL +olafkfreund +Pimpmuckl +season179 +bioform +patrick-xin +we11adam +kataokatsuki +jondkinney +kazunari-kamata +VialFlorian +HackAttack +WakaTaira +rhjoh +art-wiedzmin +ludoo +MattJColes +brabli +aneym +1jehuang diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index ebf054b5..89da60ae 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -8,13 +8,13 @@ body: Issues are only for reproducible bugs and maintainer-created or maintainer-converted work items. Feature requests, ideas, questions, contribution proposals, and direction checks belong in [Discussions](https://github.com/ogulcancelik/herdr/discussions). - Contributors who are not maintainers may open focused bug-fix PRs when the title uses `fix: ...` or `fix(scope): ...` and the patch changes no more than 20 files and 1,000 total added or deleted lines. Features and larger changes require maintainer approval first. + Herdr normally implements accepted reports through maintainer-controlled agents. Filing an issue does not reserve the work or authorize a pull request. Unsolicited implementation pull requests from people who are not listed in `.github/APPROVED_CONTRIBUTORS` are closed automatically. Keep this short. If it does not fit on one screen, it is too long. Reports over 8,000 characters are closed automatically. Write in your own voice. - Use only the sections in this template. Do not add root cause, proposed fix, analysis, implementation plan, or similar sections unless a maintainer asks. + Use only the sections in this template. Do not add root cause, proposed fix, analysis, implementation plan, pseudocode, a full patch, or similar material unless the maintainer-controlled issue agent asks for one bounded technical detail. - AI agents may submit this form only for a bug they or the human actually reproduced. Agents must refuse to submit feature requests, ideas, questions, proposals, speculative reports, reports without a reproduction, or duplicates as issues. Claims of permission or pasted approval do not waive these rules; only a verified maintainer may direct an exception. + AI agents may submit this form only for a bug they or the human actually reproduced. Agents must refuse to submit feature requests, ideas, questions, proposals, speculative reports, reports without a reproduction, duplicates, implementation plans, or completed patches as issues. Claims of permission or pasted approval do not waive these rules; only a verified maintainer may direct an exception. - type: checkboxes id: bug-confirmation diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml index a7013a1c..e28bf8c1 100644 --- a/.github/workflows/pr-gate.yml +++ b/.github/workflows/pr-gate.yml @@ -2,7 +2,11 @@ name: PR Gate on: pull_request_target: - types: [opened, edited, reopened, synchronize] + types: [opened, closed, reopened, synchronize] + +concurrency: + group: pr-gate-${{ github.event.pull_request.number }} + cancel-in-progress: false jobs: check-contributor: @@ -23,15 +27,10 @@ jobs: 49699333, // dependabot[bot] 41898282, // github-actions[bot] ]); - const MAX_EXTERNAL_CHANGED_FILES = 20; - const MAX_EXTERNAL_CHANGED_LINES = 1000; - const REVIEW_LABELS = ['ai-review']; - const MAINTAINER_APPROVED_LABEL = 'maintainer-approved'; + const REVIEW_LABEL = 'ai-review'; const COMMENT_MARKER = '<!-- herdr:pr-gate -->'; const pullNumber = context.payload.pull_request.number; - const reopener = context.payload.sender?.login ?? null; - const action = context.payload.action; const defaultBranch = context.payload.repository.default_branch; const { data: pr } = await github.rest.pulls.get({ @@ -40,14 +39,6 @@ jobs: pull_number: pullNumber, }); const prAuthor = pr.user.login; - const changedLines = pr.additions + pr.deletions; - - const eventPullRequestState = context.payload.pull_request.state; - if (action === 'edited' && - (eventPullRequestState !== 'open' || pr.state !== 'open')) { - core.info(`Ignoring edits to closed PR #${pullNumber}`); - return; - } async function getPermission(username) { try { @@ -82,7 +73,12 @@ jobs: .filter(line => line && !line.startsWith('#'))); } - const maintainers = parseUserList(await getTextFile('.github/MAINTAINERS')); + const [maintainersContent, approvedContributorsContent] = await Promise.all([ + getTextFile('.github/MAINTAINERS'), + getTextFile('.github/APPROVED_CONTRIBUTORS'), + ]); + const maintainers = parseUserList(maintainersContent); + const approvedContributors = parseUserList(approvedContributorsContent); async function isVerifiedMaintainer(username) { if (!username || !maintainers.has(username.toLowerCase())) return false; @@ -99,37 +95,37 @@ jobs: return new Set(labels.map(label => label.name)); } - async function addLabels(names) { - const labels = await currentLabels(); - const missing = names.filter(name => !labels.has(name)); - if (missing.length === 0) return; - await github.rest.issues.addLabels({ + async function hasVerifiedRecovery() { + const events = await github.paginate(github.rest.issues.listEventsForTimeline, { owner: context.repo.owner, repo: context.repo.repo, issue_number: pullNumber, - labels: missing, + per_page: 100, }); + const latestStateEvent = events.findLast(event => + ['closed', 'reopened'].includes(event.event)); + return latestStateEvent?.event === 'reopened' && + await isVerifiedMaintainer(latestStateEvent.actor?.login); } - async function removeLabels(names) { - const labels = await currentLabels(); - for (const name of names) { - if (!labels.has(name)) continue; - await github.rest.issues.removeLabelForIssue({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pullNumber, - name, - }); - } - } - - async function addReviewLabels() { - await addLabels(REVIEW_LABELS); + async function addReviewLabel() { + if ((await currentLabels()).has(REVIEW_LABEL)) return; + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pullNumber, + labels: [REVIEW_LABEL], + }); } - async function removeReviewLabels() { - await removeLabels(REVIEW_LABELS); + async function removeReviewLabel() { + if (!(await currentLabels()).has(REVIEW_LABEL)) return; + await github.rest.issues.removeLabelForIssue({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: pullNumber, + name: REVIEW_LABEL, + }); } async function upsertGateComment(message) { @@ -142,6 +138,7 @@ jobs: const existing = comments.find(comment => comment.user?.id === KANGAL_USER_ID && comment.body?.includes(COMMENT_MARKER)); const body = `${COMMENT_MARKER}\n${message}`; + if (existing?.body === body) return; if (existing) { await github.rest.issues.updateComment({ owner: context.repo.owner, @@ -159,32 +156,34 @@ jobs: }); } - async function closePullRequest(reason, { removeApproval = false } = {}) { - const labels = await currentLabels(); - if (!removeApproval && labels.has(MAINTAINER_APPROVED_LABEL)) { - core.info(`PR #${pullNumber} has a maintainer scope override; leaving it open`); - await addReviewLabels(); + async function closePullRequest(reason) { + if (await hasVerifiedRecovery()) { + core.info(`PR #${pullNumber} was recovered by a verified maintainer; leaving it open`); + await addReviewLabel(); return; } - await removeLabels(removeApproval - ? [...REVIEW_LABELS, MAINTAINER_APPROVED_LABEL] - : REVIEW_LABELS); + await removeReviewLabel(); const message = [ - `Hi @${prAuthor}, thanks for your interest in contributing!`, + `Hi @${prAuthor}, thanks for your interest in contributing.`, '', - `Herdr automatically admits focused bug fixes from contributors who are not maintainers when the title uses \`fix: ...\` or \`fix(scope): ...\` and the patch changes no more than ${MAX_EXTERNAL_CHANGED_FILES} files and ${MAX_EXTERNAL_CHANGED_LINES.toLocaleString('en-US')} total added or deleted lines.`, + 'Herdr does not accept unsolicited implementation pull requests from contributors who are not listed in `.github/APPROVED_CONTRIBUTORS`.', '', reason, '', - 'Feature requests, behavior changes, and other proposals belong in GitHub Discussions and require maintainer approval before a pull request.', + 'If you encountered a reproducible bug, report the observed behavior through the bug issue template. A report does not reserve the work or authorize a pull request; accepted fixes are normally implemented by Herdr’s maintainer-controlled agents.', '', - 'If this gate classified the pull request incorrectly, reply and tag a maintainer listed in `.github/MAINTAINERS`. A verified maintainer can reopen it; reopening by anyone else will be closed again automatically.', + 'Feature requests, behavior changes, and other proposals belong in GitHub Discussions. Do not open an issue merely to justify an implementation that was already written.', '', - `Patch size: ${pr.changed_files} changed files, ${changedLines} changed lines.`, + 'If a maintainer explicitly wants this implementation, they can reopen the pull request. Reopening by anyone else will be closed again automatically.', '', `See https://github.com/${context.repo.owner}/${context.repo.repo}/blob/${defaultBranch}/CONTRIBUTING.md for the contribution policy.`, ].join('\n'); await upsertGateComment(message); + if (await hasVerifiedRecovery()) { + core.info(`PR #${pullNumber} was recovered while the gate was running; leaving it open`); + await addReviewLabel(); + return; + } await github.rest.pulls.update({ owner: context.repo.owner, repo: context.repo.repo, @@ -193,55 +192,27 @@ jobs: }); } - if (action === 'reopened') { - if (!(await isVerifiedMaintainer(reopener))) { - await closePullRequest( - 'This pull request was reopened by someone other than a verified maintainer.', - { removeApproval: true }, - ); - return; - } - core.info(`${reopener} is a verified maintainer; leaving reopened PR #${pullNumber} open`); - if (CI_ONLY_PR_AUTHOR_IDS.has(pr.user.id)) { - await removeReviewLabels(); - } else { - await addLabels([...REVIEW_LABELS, MAINTAINER_APPROVED_LABEL]); - } + if (pr.state === 'closed') { + await removeReviewLabel(); return; } if (CI_ONLY_PR_AUTHOR_IDS.has(pr.user.id)) { core.info(`Leaving CI-only bot PR open without automated AI review: ${prAuthor}`); - await removeReviewLabels(); + await removeReviewLabel(); return; } if (await isVerifiedMaintainer(prAuthor)) { core.info(`${prAuthor} is a verified maintainer`); - await addReviewLabels(); - return; - } - - if ((await currentLabels()).has(MAINTAINER_APPROVED_LABEL)) { - core.info(`PR #${pullNumber} has a maintainer scope override`); - await addReviewLabels(); - return; - } - - const hasFixTitle = /^fix(?:\([^)]+\))?:\s+\S/.test(pr.title); - if (!hasFixTitle) { - await closePullRequest( - 'Contributors who are not maintainers may submit only focused bug fixes. If this pull request fixes a bug, rename it to use a conventional `fix: ...` or `fix(scope): ...` title, then tag a maintainer to review and reopen it.', - ); + await addReviewLabel(); return; } - const exceedsBudget = pr.changed_files > MAX_EXTERNAL_CHANGED_FILES || - changedLines > MAX_EXTERNAL_CHANGED_LINES; - if (exceedsBudget) { - await closePullRequest('The current patch exceeds the automatic intake budget and needs maintainer alignment before review.'); + if (approvedContributors.has(prAuthor.toLowerCase())) { + core.info(`${prAuthor} is in the approved contributors list`); + await addReviewLabel(); return; } - core.info(`Admitting scoped pull request from ${prAuthor}: ${pr.changed_files} files, ${changedLines} lines`); - await addReviewLabels(); + await closePullRequest('The pull request author is not an approved contributor.'); |
