From 4e916e99f3b52bf4ebe4eddaba800b42dacec203 Mon Sep 17 00:00:00 2001 From: "swu-echo[bot]" <344778617+swu-echo[bot]@users.noreply.github.com> Date: Wed, 21 Oct 2026 08:20:09 -0500 Subject: [PATCH 1/2] Grant the auto-merge caller the reusable gate's read scopes The in-flight gate added to the shared auto-merge reusable (AFC #822) reads check-runs, workflow runs, and PR comments. A called workflow's GITHUB_TOKEN is capped by the caller's grants, and a called workflow that declares scopes the caller omits fails at startup, so grant them here before the reusable pin next advances. --- .github/workflows/auto-merge-prs.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/auto-merge-prs.yml b/.github/workflows/auto-merge-prs.yml index 0ccd73192..23f50bd2a 100644 --- a/.github/workflows/auto-merge-prs.yml +++ b/.github/workflows/auto-merge-prs.yml @@ -55,9 +55,15 @@ on: pull_request: types: [opened, synchronize, reopened, labeled, ready_for_review, unlabeled] +# checks/actions/issues reads feed the reusable's in-flight gate (a called +# workflow's GITHUB_TOKEN is capped by the caller's grants, and a called +# workflow requesting scopes the caller omits fails at startup). permissions: contents: write pull-requests: write + checks: read + actions: read + issues: read jobs: auto-merge: From 0d98fc556630869594d8b6edc1e9a0c9561b5f38 Mon Sep 17 00:00:00 2001 From: Jay Wedgeworth <12656028+jaywedgeworth22@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:24:48 -0500 Subject: [PATCH 2/2] Guard the auto-merge caller's token grants with a test The reusable's in-flight gate reads the head commit's check runs and the head branch's queued workflow runs, and a called workflow cannot elevate the caller's token. A scope the caller omits fails the reusable at startup and silently disables auto-merge arming for every pull request in this repo. These grants are the contract, so assert them in the existing pinned-AFC case instead of trusting them to stay put. Verified: npx vitest run test/branch-protection-gate.test.ts -> 11 passed, and the case fails when checks: read is removed from the workflow. --- test/branch-protection-gate.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/branch-protection-gate.test.ts b/test/branch-protection-gate.test.ts index 78f2b20dd..6ca40c7ab 100644 --- a/test/branch-protection-gate.test.ts +++ b/test/branch-protection-gate.test.ts @@ -265,6 +265,22 @@ describe("required contexts are not renamed out from under the ruleset", () => { expect(auto).not.toMatch(/fetch-depth:\s*0/); const gate = readFileSync(join(repoRoot, "scripts", "pr-touches-codeowners-paths.sh"), "utf8"); expect(gate).toContain("CODEOWNERS"); + + // The reusable's in-flight gate looks up the head commit's check runs and the head + // branch's queued workflow runs. A called workflow cannot elevate the caller's token: + // it fails at startup with "The workflow is requesting 'checks: read', but is only + // allowed 'checks: none'" when the caller omits a scope. No job in this workflow + // declares its own permissions block, so the workflow-level block below is the + // effective one and these lines are the whole contract. A later edit that drops one + // of them breaks arming for every pull request in this repo, so assert them here + // rather than trusting the grant to stay put (Kody review, PR #4908). + const callerPermissions = + withoutComments(auto).match(/^permissions:\n((?:[ \t]+\S+: \S+\n)+)/m)?.[1] ?? ""; + expect(callerPermissions).toMatch(/^[ \t]+contents: write$/m); + expect(callerPermissions).toMatch(/^[ \t]+pull-requests: write$/m); + expect(callerPermissions).toMatch(/^[ \t]+checks: read$/m); + expect(callerPermissions).toMatch(/^[ \t]+actions: read$/m); + expect(callerPermissions).toMatch(/^[ \t]+issues: read$/m); }); it("does not ask the armer to delete the branch while the merge queue is on", () => {