diff --git a/.github/workflows/label-pr-review-state.yml b/.github/workflows/label-pr-review-state.yml index 459dcf50bd..4d2a5d1532 100644 --- a/.github/workflows/label-pr-review-state.yml +++ b/.github/workflows/label-pr-review-state.yml @@ -47,12 +47,15 @@ on: permissions: # This privileged workflow only reads PR/check metadata and writes issue labels, - # comments, and commit statuses. All unspecified permissions, including contents, - # are none; no fork code or configuration is checked out or executed. + # comments, and commit statuses. All unspecified permissions are none; contents is + # read-only and used only for the commit-parent lookup below. No fork code or + # configuration is checked out or executed. pull-requests: write issues: write checks: read statuses: write + # Reading a commit's parents is metadata only; no PR code is checked out or executed. + contents: read # Concurrency is keyed at two levels (issue #1707): # @@ -100,6 +103,7 @@ jobs: runs-on: ubuntu-latest outputs: pr_numbers: ${{ steps.resolve.outputs.pr_numbers }} + open_pr_heads: ${{ steps.resolve.outputs.open_pr_heads }} env: # Mirrors the workflow-level concurrency group so the run's queueing # identity is visible in the logs (kept in sync by the workflow tests). @@ -146,6 +150,7 @@ jobs: // pull_request_review runs report the base repository as // head_repository (issue #2133). let prNumbers = []; + let openPrsCache = null; if (context.payload.pull_request?.number) { prNumbers = [context.payload.pull_request.number]; } else if (context.eventName === 'issue_comment') { @@ -178,14 +183,14 @@ jobs: ); } } else { - const openPrs = await github.paginate(github.rest.pulls.list, { + openPrsCache = await github.paginate(github.rest.pulls.list, { owner, repo, state: 'open', per_page: 100, }); // GitHub rejects matrices over 256 jobs, so the sweep pages the // sorted open-PR list: consecutive runs rotate through // fixed-size windows by run number, covering every open PR on a // bounded cadence without ever exceeding the limit. - const allNumbers = openPrs.map(pr => pr.number).sort((a, b) => a - b); + const allNumbers = openPrsCache.map(pr => pr.number).sort((a, b) => a - b); const sweepPageSize = 200; const pageCount = Math.max(1, Math.ceil(allNumbers.length / sweepPageSize)); const page = context.runNumber % pageCount; @@ -204,6 +209,50 @@ jobs: ); core.setOutput('pr_numbers', JSON.stringify(prNumbers)); + // Build the open-PR head map once here for every shard: a sweep would otherwise + // multiply the paginated open-PR read by every shard against the Actions token + // budget. Sweep triggers already hold the full open list, so the map costs them + // no extra read. + // + // The map only describes which commits are open pull request heads, so it can + // only change when a pull request's head is (re)published: opened, reopened, or + // synchronize. Reviews, comments, workflow_run completions, and manual dispatch + // cannot change it, so they skip the paginated read entirely and pass a sentinel + // that tells shards the label was not evaluated. Reading it on every direct event + // would add one full open-PR listing to each review and status-comment run. + // + // An empty output means the map was unavailable this run; shards then skip the + // stacked label rather than deciding from an empty map. + const stackMayHaveChanged = + context.eventName === 'pull_request_target' && + ['opened', 'reopened', 'synchronize'].includes(context.payload.action); + let openPrHeadsOutput = ''; + if (prNumbers.length > 0 && (openPrsCache || stackMayHaveChanged)) { + try { + const openPrsForMap = openPrsCache ?? (await github.paginate(github.rest.pulls.list, { + owner, repo, state: 'open', per_page: 100, + })); + const heads = {}; + for (const candidate of openPrsForMap) { + if (candidate.head?.sha) heads[candidate.head.sha.toLowerCase()] = candidate.number; + } + // An open-PR list with no entries is anomalous (the reconciliation + // target itself is open), so treat it like an unavailable map: + // shards must not decide from a map that looks empty. + if (Object.keys(heads).length > 0) openPrHeadsOutput = JSON.stringify(heads); + } catch (error) { + core.warning(`Could not read the open pull request map: ${error.message}; shards will skip the stacked label this run`); + } + } else if (prNumbers.length > 0) { + // Sentinel: this event cannot change a stacked relationship, so the label was + // not evaluated rather than found unavailable. Shards skip it silently. + openPrHeadsOutput = 'not-evaluated'; + core.info( + `Open-PR head map not read: ${context.eventName} cannot change a stacked unit; shards skip the stacked label.` + ); + } + core.setOutput('open_pr_heads', openPrHeadsOutput); + reconcile: name: Zoo Code / reconcile PR review state needs: resolve @@ -245,6 +294,7 @@ jobs: runs-on: ubuntu-latest env: PR_NUMBER: ${{ matrix.pr_number }} + OPEN_PR_HEADS: ${{ needs.resolve.outputs.open_pr_heads }} # Mirrors this job's concurrency group so a superseded shard's queueing # identity is visible in the logs (kept in sync by the workflow tests). CONCURRENCY_GROUP: >- @@ -307,6 +357,11 @@ jobs: color: 'c2e0c6', description: 'Fresh community approval on the current head; maintainer review still required', }, + { + name: 'stacked', + color: 'c2c2c2', + description: 'Head commit sits on another open PR, so its diff is only this unit', + }, ]; const guideMarker = ''; const codeRabbitLabelMarkerPrefix = '