From 63f2b164fbe933a8eb4387326c8e64175541649d Mon Sep 17 00:00:00 2001 From: Bryan Thompson Date: Thu, 25 Jun 2026 21:42:26 -0500 Subject: [PATCH] Exempt the bump bot from the external-PR scope guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The External PR Scope Guard (#3353) and the auto-closer both look up the PR author's collaborator permission and, for anyone who is not write/admin, require the PR to ADD marketplace.json entries (additions-only). Internal bump PRs are authored by github-actions[bot], which is not reported as a member, so a SHA-bump — a legitimate MODIFY of an existing entry — fails the guard (e.g. #3391 "modifies existing entry: astronomer-data-agents"). Add a shared isExemptAuthor() helper that exempts both org members and the repo's own automation bot, and route both workflows through it. Safe under pull_request_target: a fork PR cannot author as github-actions[bot] (only the org's own GITHUB_TOKEN workflow can), and the member path is still a real permission lookup. The helper also wraps getCollaboratorPermissionLevel in try/catch — previously a non-collaborator/unknown-user lookup threw and errored the job instead of falling through to scope evaluation. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/scripts/external-pr-scope.js | 31 ++++++++++++++++++- .github/workflows/close-external-prs.yml | 14 ++++----- .github/workflows/external-pr-scope-guard.yml | 13 ++++---- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/.github/scripts/external-pr-scope.js b/.github/scripts/external-pr-scope.js index 5438a1d5..705a18a3 100644 --- a/.github/scripts/external-pr-scope.js +++ b/.github/scripts/external-pr-scope.js @@ -121,4 +121,33 @@ async function evaluate({ github, context }) { return analyze({ changedFiles, before, after, liveRepos: liveReposOf(liveBase) }); } -module.exports = { normalizeRepo, liveReposOf, analyze, readPlugins, evaluate, MARKETPLACE }; +// Authors that are NOT subject to the external-contributor scope rules: +// - the repo's own automation bot — its bump PRs legitimately MODIFY existing entries +// (SHA bumps), which the additions-only external-contributor rule forbids; AND +// - org members (write/admin). +// Safe under pull_request_target: a fork PR cannot set its author to github-actions[bot] +// (that login is only ever the org's own GITHUB_TOKEN workflow), and the member path is a +// real permission lookup. Wrapped in try/catch because getCollaboratorPermissionLevel throws +// for a non-collaborator/unknown user — without this, both callers would error the job rather +// than fall through to scope evaluation. +const EXEMPT_BOTS = new Set(['github-actions[bot]']); + +async function isExemptAuthor({ github, context }) { + const author = context.payload.pull_request.user.login; + if (EXEMPT_BOTS.has(author)) { + return { exempt: true, reason: `${author} is the trusted automation bot` }; + } + try { + const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, repo: context.repo.repo, username: author, + }); + if (['admin', 'write'].includes(data.permission)) { + return { exempt: true, reason: `${author} is ${data.permission} (member)` }; + } + } catch (e) { + // not a collaborator / lookup failed → not exempt; fall through to scope evaluation + } + return { exempt: false }; +} + +module.exports = { normalizeRepo, liveReposOf, analyze, readPlugins, evaluate, isExemptAuthor, MARKETPLACE }; diff --git a/.github/workflows/close-external-prs.yml b/.github/workflows/close-external-prs.yml index b1fc2808..2e7db830 100644 --- a/.github/workflows/close-external-prs.yml +++ b/.github/workflows/close-external-prs.yml @@ -23,14 +23,13 @@ jobs: script: | const author = context.payload.pull_request.user.login; - const { data } = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, - repo: context.repo.repo, - username: author - }); + const { evaluate, isExemptAuthor } = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/external-pr-scope.js`); - if (['admin', 'write'].includes(data.permission)) { - console.log(`${author} has ${data.permission} access, allowing PR`); + // Members (write/admin) and the repo's own automation bot (bump SHA PRs) are never + // auto-closed. + const ex = await isExemptAuthor({ github, context }); + if (ex.exempt) { + console.log(`${ex.reason} — allowing PR`); return; } @@ -40,7 +39,6 @@ jobs: // of allowed repos is derived from the live marketplace.) This grants only the // right to open a reviewable PR; the External PR Scope Guard required check and a // maintainer approval still gate the merge. - const { evaluate } = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/external-pr-scope.js`); const result = await evaluate({ github, context }); if (result.ok && result.added.length > 0) { console.log(`In-scope external contribution (adds: ${result.added.join(', ')}) — allowing PR.`); diff --git a/.github/workflows/external-pr-scope-guard.yml b/.github/workflows/external-pr-scope-guard.yml index 58270854..10448756 100644 --- a/.github/workflows/external-pr-scope-guard.yml +++ b/.github/workflows/external-pr-scope-guard.yml @@ -29,17 +29,16 @@ jobs: - uses: actions/github-script@v7 with: script: | - const author = context.payload.pull_request.user.login; + const { evaluate, isExemptAuthor } = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/external-pr-scope.js`); - const { data: perm } = await github.rest.repos.getCollaboratorPermissionLevel({ - owner: context.repo.owner, repo: context.repo.repo, username: author, - }); - if (['admin', 'write'].includes(perm.permission)) { - console.log(`${author} is ${perm.permission} (member) — scope guard not applicable.`); + // Members (write/admin) and the repo's own automation bot (bump SHA PRs) are + // unrestricted; only genuinely external contributions are scope-checked. + const ex = await isExemptAuthor({ github, context }); + if (ex.exempt) { + console.log(`${ex.reason} — scope guard not applicable.`); return; } - const { evaluate } = require(`${process.env.GITHUB_WORKSPACE}/.github/scripts/external-pr-scope.js`); const result = await evaluate({ github, context }); if (!result.ok) {