From d3056a1f32011438abcf2bca7549f9d1b3422e9f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 17:41:58 +0900 Subject: [PATCH] fix(scheduler): bound scan-pr-queue to timeout-minutes: 30 scan-pr-queue in pr-review-merge-scheduler.yml had no job-level timeout-minutes, so a stuck run (rate-limited GitHub API, a hung gh invocation) falls back to GitHub's 360-minute platform default. Live evidence (2026-09-02, 69 queued runs for this workflow, several schedule/push/workflow_run runs queued for hours) shows this contributing to the org-wide Actions capacity incident, alongside the sibling opencode-review.yml unbounded verdict-polling fix. Bound it to 30 minutes: shorter than org-queue-sweep's existing timeout-minutes: 60 precedent, since scan-pr-queue only scans this one repository's PR queue (paginated GraphQL reads, page size 25, plus at most one review dispatch and one branch update per run) rather than walking every target repository in the organization. Left cancel-in-progress as-is for the workflow_run/push/schedule paths: cancelling scan-pr-queue mid-mutation (mid-merge, mid-branch-update) risks leaving a PR/branch partially updated, and there is no evidence in hand that this is safe. The new timeout bound alone converts an unbounded job into a bounded-but-serial one, which is the minimal safe fix. Adds test_scan_pr_queue_has_a_bounded_runtime asserting the job declares timeout-minutes in (1, 45] and strictly less than org-queue-sweep's 60. Co-Authored-By: Claude Sonnet 5 --- .../workflows/pr-review-merge-scheduler.yml | 7 +++++++ .../test_required_workflow_queue_contract.py | 21 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/.github/workflows/pr-review-merge-scheduler.yml b/.github/workflows/pr-review-merge-scheduler.yml index e4d6b8737a..2237d59675 100644 --- a/.github/workflows/pr-review-merge-scheduler.yml +++ b/.github/workflows/pr-review-merge-scheduler.yml @@ -137,6 +137,13 @@ jobs: github.event.client_payload.org_sweep != true ) runs-on: ubuntu-24.04 + # Bound scan-pr-queue to a wall-clock ceiling well short of GitHub's + # 360-minute platform default. This is a single-repository queue scan + # (paginated GraphQL reads plus at most one review dispatch and one + # branch update per run) -- much lighter than org-queue-sweep's full + # organization walk below, so it gets a shorter bound than that job's + # timeout-minutes: 60. + timeout-minutes: 30 permissions: actions: write checks: read diff --git a/tests/test_required_workflow_queue_contract.py b/tests/test_required_workflow_queue_contract.py index 056e47a678..85d13c2cf9 100644 --- a/tests/test_required_workflow_queue_contract.py +++ b/tests/test_required_workflow_queue_contract.py @@ -2,6 +2,7 @@ import json import os +import re import shlex import shutil import subprocess @@ -977,6 +978,26 @@ def test_review_events_can_dispatch_after_threads_are_resolved() -> None: )[1].splitlines()[0] +def test_scan_pr_queue_has_a_bounded_runtime() -> None: + """scan-pr-queue must not fall back to GitHub's 360-minute platform default. + + Without a job-level timeout-minutes, a stuck run (rate-limited GitHub API, + a hung gh invocation) can occupy a shared runner for up to six hours, + contributing to org-wide Actions capacity saturation. The bound must be + shorter than org-queue-sweep's timeout-minutes: 60, since scan-pr-queue + only scans this one repository's queue while org-queue-sweep walks every + target repository in the organization. + """ + workflow = workflow_text("pr-review-merge-scheduler.yml") + scan_job = workflow.split(" scan-pr-queue:", 1)[1].split(" org-queue-sweep:", 1)[0] + + match = re.search(r"^ timeout-minutes: (\d+)$", scan_job, flags=re.MULTILINE) + assert match is not None, "scan-pr-queue must declare a job-level timeout-minutes" + scan_timeout = int(match.group(1)) + assert 1 <= scan_timeout <= 45 + assert scan_timeout < 60 + + def test_org_queue_sweep_covers_target_repositories_on_a_heartbeat() -> None: """Guard the org-wide approved-PR fallback sweep contract.