diff --git a/.github/workflows/label-pr-review-state.yml b/.github/workflows/label-pr-review-state.yml index 459dcf50bd..302c8b1a1c 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,48 @@ 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 describes which commits are open pull request heads. It can change when a + // head is (re)published (opened, reopened, synchronize), and a labeled/unlabeled + // run is included for a different reason: it shares this PR's concurrency group, + // so it can replace a pending synchronize run for the same PR. A replacement run + // that skipped the map would skip the stacked label for the new head and leave it + // stale until the hourly sweep. Reviews, comments, workflow_run completions, and + // manual dispatch cannot change it (and cannot displace a synchronize run in a way + // that leaves the label stale), so they skip the paginated read entirely and pass + // a sentinel that tells shards the label was not evaluated. + // + // An empty output means the map was unavailable this run; shards then skip the + // stacked label rather than deciding from an empty map. + // Every execution that targets a PR reads the map: reviews, comments, manual + // dispatches and workflow_run completions share this PR's concurrency group with + // synchronize runs, so a displaced synchronize leaves its stacked label stale unless + // the replacement run evaluates it too. The sweep already holds the list in + // `openPrsCache`, so its cost is unchanged. + let openPrHeadsOutput = ''; + if (prNumbers.length > 0) { + 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`); + } + } + core.setOutput('open_pr_heads', openPrHeadsOutput); + reconcile: name: Zoo Code / reconcile PR review state needs: resolve @@ -245,6 +292,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 +355,11 @@ jobs: color: 'c2e0c6', description: 'Fresh community approval on the current head; maintainer review still required', }, + { + name: 'stacked', + color: 'c2c2c2', + description: 'Head commit sits directly on another open PR head (detected from the commit graph)', + }, ]; const guideMarker = ''; const codeRabbitLabelMarkerPrefix = '