-
Notifications
You must be signed in to change notification settings - Fork 0
ci(governance): bot-allowlist for low-risk dep/CI chore PRs #131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,8 @@ on: | |
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
| checks: read | ||
| statuses: read | ||
|
|
||
| jobs: | ||
| governance: | ||
|
|
@@ -138,15 +140,88 @@ jobs: | |
| uses: actions/github-script@v7 | ||
| with: | ||
| script: | | ||
| const pr = context.payload.pull_request; | ||
| const owner = context.repo.owner; | ||
| const repo = context.repo.repo; | ||
| const pull_number = pr.number; | ||
|
|
||
| // Bot allowlist: low-risk dependency / CI chore PRs from trusted authors | ||
| // touching only lockfiles / dep manifests / specific governance files | ||
| // may satisfy the gate without a human approval, provided all other | ||
| // required checks pass and no changes_requested reviews exist. | ||
| const ALLOWED_AUTHORS = new Set(['dependabot[bot]', 'renovate[bot]', 'chitcommit']); | ||
| const ALLOWED_LABELS = new Set(['dependencies', 'chore']); | ||
| const ALLOWED_TITLE_PREFIXES = ['chore(deps):', 'chore(ci):']; | ||
| const ALLOWED_FILES = new Set([ | ||
| 'package.json', | ||
| 'pnpm-lock.yaml', | ||
| 'package-lock.json', | ||
| '.github/workflows/security-gates.yml', | ||
| '.github/dependabot.yml', | ||
| ]); | ||
|
|
||
| const author = pr.user && pr.user.login; | ||
| const title = pr.title || ''; | ||
| const labels = (pr.labels || []).map(l => l.name); | ||
|
|
||
| const authorOk = ALLOWED_AUTHORS.has(author); | ||
| const labelOk = labels.some(l => ALLOWED_LABELS.has(l)); | ||
| const titleOk = ALLOWED_TITLE_PREFIXES.some(p => title.startsWith(p)); | ||
| const labelOrTitleOk = labelOk || titleOk; | ||
|
|
||
| const files = await github.paginate(github.rest.pulls.listFiles, { | ||
| owner, repo, pull_number, per_page: 100, | ||
| }); | ||
| const filenames = files.map(f => f.filename); | ||
| const filesOk = filenames.length > 0 && filenames.every(f => ALLOWED_FILES.has(f)); | ||
|
|
||
| const { data: reviews } = await github.rest.pulls.listReviews({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| pull_number: context.payload.pull_request.number | ||
| owner, repo, pull_number, | ||
| }); | ||
| const humanApprovals = reviews.filter(r => r.state === 'APPROVED' && !r.user.login.includes('[bot]')); | ||
| const required = 1; | ||
| // Latest review state per reviewer | ||
| const latestByReviewer = new Map(); | ||
| for (const r of reviews) { | ||
| const key = r.user && r.user.login; | ||
| if (!key) continue; | ||
| latestByReviewer.set(key, r); | ||
| } | ||
| const latestReviews = [...latestByReviewer.values()]; | ||
| const hasChangesRequested = latestReviews.some(r => r.state === 'CHANGES_REQUESTED'); | ||
| const humanApprovals = latestReviews.filter(r => | ||
| r.state === 'APPROVED' && !(r.user && r.user.login && r.user.login.includes('[bot]')) | ||
| ); | ||
|
|
||
| // Other required checks must be green before bot-allowlist applies. | ||
| // Look at check runs + commit statuses on the head sha, exclude this job. | ||
| const headSha = pr.head.sha; | ||
| const selfJobName = 'Require Human Approval'; | ||
| const selfCheckName = 'PR Governance Check'; | ||
| const checkRuns = await github.paginate(github.rest.checks.listForRef, { | ||
| owner, repo, ref: headSha, per_page: 100, | ||
| }); | ||
|
Comment on lines
+199
to
+201
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an allowlisted bot PR has required legacy commit status contexts (for example an external CI or coverage service), this only reads GitHub Check Runs and never reads commit statuses despite the comment and new Useful? React with πΒ / π. |
||
| const otherChecks = checkRuns.filter(c => | ||
| c.name !== selfJobName && c.name !== selfCheckName | ||
| ); | ||
| const checksAllGreen = otherChecks.length > 0 && otherChecks.every(c => | ||
| c.status === 'completed' && (c.conclusion === 'success' || c.conclusion === 'neutral' || c.conclusion === 'skipped') | ||
|
Comment on lines
+205
to
+206
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For an otherwise allowlisted bot PR, this evaluates Useful? React with πΒ / π. |
||
| ); | ||
|
|
||
| const allowlistMatch = | ||
| authorOk && labelOrTitleOk && filesOk && !hasChangesRequested && checksAllGreen; | ||
|
|
||
| core.info(`Author: ${author} (ok=${authorOk})`); | ||
| core.info(`Labels: ${labels.join(',')} | TitlePrefix ok=${titleOk} | LabelOrTitle ok=${labelOrTitleOk}`); | ||
| core.info(`Files (${filenames.length}): ${filenames.join(', ')} (ok=${filesOk})`); | ||
| core.info(`Changes requested: ${hasChangesRequested}`); | ||
| core.info(`Other checks all green: ${checksAllGreen} (count=${otherChecks.length})`); | ||
| core.info(`Human approvals: ${humanApprovals.length}`); | ||
| core.info(`Bot-allowlist match: ${allowlistMatch}`); | ||
|
|
||
| const required = allowlistMatch ? 0 : 1; | ||
| if (humanApprovals.length < required) { | ||
| core.setFailed(`Requires at least ${required} human approval(s). Current: ${humanApprovals.length}`); | ||
| } else if (allowlistMatch) { | ||
| core.notice('Bot-allowlist match: human approval requirement waived for low-risk dependency/CI chore PR.'); | ||
| } | ||
|
|
||
| hardening: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On bot PRs with more than the first page of reviews, this single
listReviewscall can miss later review records, including a laterCHANGES_REQUESTEDfrom a reviewer. Because the allowlist then treatshasChangesRequestedas false, a dependency/CI PR can have the human approval waived even though a reviewer has an outstanding change request.Useful? React with πΒ / π.