Skip to content

ci: the regen bot's own push cancels the job that approves its parked runs, so a PR's CI can silently never run #16360

Description

@mrveiss

Problem

When auto-fix-generated-types.yml pushes a regenerated-types commit to a PR branch, every workflow run at the new head is parked as action_required: runs triggered by github-actions[bot] need approval. The workflow's own approve-parked-runs job exists to approve them. But that job runs inside the pre-push run, and the parked run the push creates enters the same concurrency group, so it can cancel the approver before the approver runs. When that happens, the PR's CI never runs at all, and nothing says so except a wall of action_required.

Evidence (#16351, 2026-09-11)

Time (UTC) Event
14:52:57 Auto-fix run 34612780429 starts at head 32d73ea28
15:16:26 autofix-types pushes 16764c297 ("chore(types): regenerate generated API types…"); the job completes at 15:16:55
15:16:32 Every workflow run at 16764c297 is created as completed/action_required, triggered by github-actions[bot]
15:23:15 approve-parked-runs is cancelled. Check-run annotation: "Canceling since a higher priority waiting request for Auto-fix Generated Types-issue-16310-sync-deletes exists"
15:23:20 Verify Generated Types run 34612780523, at the old head: verify-generated-types-slm and verify-types-run are cancelled with the same annotation for group Verify Generated Types-refs/pull/16351/merge

Result: all runs at 16764c297 stay action_required, and no CI runs for the PR.

Counter-example, #16266: the same regen commit (0a8d5bb22), but there approve-parked-runs (run 34596710417) completed before it could be cancelled, and all 27 runs at the new head ran green. The difference is timing. On #16351 the approver waited about 6 minutes for a runner under queue pressure and lost the race.

Root cause

  • auto-fix-generated-types.yml:43-45 uses group: ${{ github.workflow }}-${{ github.head_ref }} with cancel-in-progress: true. The approver (:229, needs: autofix-types) runs in the run that the push supersedes.
  • verify-generated-types.yml:93-95 uses group: ${{ github.workflow }}-${{ github.ref }}, with cancel-in-progress set on pull_request.
  • Ruled out: ci_dispatch_watchdog.py's force-cancel (:458). It skips in_progress runs, and the Auto-fix run was in_progress. Also, no CI Dispatch Watchdog job log between 15:10 and 15:30Z mentions either run id.

Fix direction, to verify before implementing

  • A bot-triggered run must not cancel the run that approves it. For example: cancel-in-progress: ${{ github.event.sender.login != 'github-actions[bot]' }} in both groups. Confirm first which run's cancel-in-progress governs the cancel (the newcomer's is expected).
  • Or move the approver out of the superseded run, e.g. a workflow_run-triggered follow-up, or the scheduled watchdog sweep approving parked same-repo PR runs. Check whether the cron sweep approves at all; ci-dispatch-watchdog.yml:48 notes that the cron once did nothing.
  • Either way, a repo test must pin it: no job whose purpose is approving parked runs may sit in a run that its own push supersedes.

Acceptance criteria

  • A regen push can no longer leave its PR's runs action_required because the approver was cancelled. The mechanism is pinned by a test.
  • The Verify Generated Types group gets the same treatment, or a stated reason it doesn't need it.
  • A PR whose runs are all action_required shows up where a human looks, e.g. the watchdog's report, rather than failing silently.

Found while reading #16351's stalled CI. Related: #16328 (watchdog), #13439 (force-cancel).

Activity

  1. mrveiss commented on Sep 11, 2026

    @mrveiss
    OwnerAuthor

    Verification before implementing (read-only: run/job API data and the docs; nothing executed).

    1. The self-cancel is real. The approver's own approval brings the superseding run into its concurrency group.

    • The approver (approve-parked-runs, job 103315220203, run 34612780429) started at 15:16:55Z.
    • The Auto-fix run at the regen head (34615151655, run_attempt=2) started at 15:23:12Z, and the approver was cancelled at 15:23:15Z.
    • The approval that released the new-head Auto-fix run put it in the same group (${{ github.workflow }}-${{ github.head_ref }}, cancel-in-progress: true), and it cancelled the run the approver lives in.
    • The approver's log can't be read: log not found, because GitHub keeps no log for the cancelled job.

    2. The reported outcome didn't happen in this case. The runs are approved, not parked.

    3. The risk is still real, and it's timing-dependent.

    4. The watchdog cron isn't a dependable backstop. It does sweep and approve (the watchdog job runs --check dispatch on schedule). But GitHub fires this */15 cron rarely:

    • 7 schedule runs in the last 200 watchdog runs, roughly every 2-4 hours;
    • the last one at 12:51Z;
    • none between 15:05 and 15:40Z.

    5. Whose cancel-in-progress governs is not settled.

    • The docs say only: "To also cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true."
    • Both runs here come from the same file with the same setting, so this incident can't tell the two apart.
    • So a fix that relies on the newcomer's cancel-in-progress (the sender-based expression) rests on an unverified assumption.
    • A fix that changes group MEMBERSHIP doesn't depend on that answer. Put a bot-triggered run of this workflow in its own group, for example by adding ${{ github.triggering_actor == 'github-actions[bot]' && github.sha || '' }} to the group. Then the run the approver releases can't join, or cancel, the approver's group.

    Verify Generated Types (AC2). The 15:23:20Z cancel there is ordinary supersession. The old-head run (34612780523) was cancelled when the approved new-head run (34615151864, started 15:23:18Z) joined Verify Generated Types-refs/pull/16351/merge. No approver lives in that workflow, so nothing is lost. That's the stated reason AC2 asks for.

  2. mrveiss commented on Sep 11, 2026

    @mrveiss
    OwnerAuthor

    Correction to the issue text, from Helper-02's verification (evidence in the comment above). The mechanism is real, but I got this incident's outcome wrong.

    • What I claimed: that all runs at 16764c297 "stay action_required" and "no CI runs for the PR". That's false. The approver started at 15:16:55Z and had already approved all 31 runs (run_started_at 15:23:12–15:23:23) before its own group cancelled it at 15:23:15Z. The runs are queued or pending under general runner saturation, not parked. My listing showed the pre-approval state of the runs, and I didn't check which run attempt it reflected.
    • What stands: the approver is cancelled by the run it approves, because the new-head Auto-fix run (34615151655, attempt 2) joins its concurrency group. If the approver's own workflow comes early in the API's list of runs to approve, the cancel lands mid-sweep, and every run after it stays parked. So the risk is real; this incident was just lucky with the order.
    • The ACs still apply with that framing. Fix direction, agreed with Helper-02: make a bot-triggered Auto-fix run's concurrency group different from the approver's, e.g. add github.sha to the group when the triggering actor is the bot. That holds whichever run's cancel-in-progress governs, which the docs leave ambiguous. Also approve the approver's own workflow last, and add a repo test pinning both.
  3. added this to the v0.9.0 milestone on Sep 12, 2026
  4. mrveiss commented on Sep 12, 2026

    @mrveiss
    OwnerAuthor

    It's live and wider than one PR (2026-09-12, 14:50 UTC). actions/runs?status=action_required lists 262 parked runs across 11 of this repo's own branches, every one triggered by github-actions[bot], and none from forks. Four open PRs had their whole CI parked on their current head, so zero required checks ran on the code under review: #16469, #16471, #16472 and #16503 (104 runs, about 26 workflows each). For #16503, the regen run at 13:03 pushed 4ed6dd5b5 and its approve-parked-runs job reported success. The follow-up run at 13:43 then sat at action_required, and only semgrep ever ran on that head.

    The backstop didn't catch it: ci-dispatch-watchdog's 15-minute cron has run from schedule only 3 times today (03:33, 08:08, 12:10 UTC), because GitHub throttles scheduled runs.

    Interim repair: the 104 runs on those four current heads were approved through POST /actions/runs/{id}/approve, the same operation the watchdog performs. Nothing else was touched: no runs on superseded SHAs, and no fork runs. The fix PR, #16363, is green but behind main, and it should land early in the next train. Until it does, a PR whose head was last pushed by the regen bot should be treated as unverified, whatever its check list shows.

  5. mrveiss commented on Sep 13, 2026

    @mrveiss
    OwnerAuthor

    Acceptance-criteria evidence against main at b68705c (#16363 merged), and reopening for AC1's host evidence only.

    • AC1: mechanism verified, behaviour not yet observed. Left unticked.
      • .github/workflows/auto-fix-generated-types.yml sets the concurrency group: to add a -regen-… suffix when endsWith(github.actor, '[bot]') (dependabot excluded). The run a regen push parks therefore can't cancel the approver that is approving it.
      • repo_tests/approver_self_cancel_16360_test.py pins this with 7 tests.
      • The criterion's first half is behaviour: "a regen push can no longer leave its PR's runs action_required". fix(ci): stop the regen bot's approved runs cancelling the job that approved them (#16360) #16363 itself says that evidence can only come from the next regen push after merge.
      • A pull_request run uses the workflow version on the PR's merge ref, so the evidence has to come from a PR whose merge base includes b68705c.
    • AC2: met. Ticked. A stated reason was given: Verify Generated Types holds no approver, so cancelling its superseded run loses nothing (fix(ci): stop the regen bot's approved runs cancelling the job that approved them (#16360) #16363 body, "Verify Generated Types (AC2)").
    • AC3: met. Ticked.
      • The approver job has an if: cancelled() step that raises ::error and runs gh workflow run ci-dispatch-watchdog.yml --ref "$WATCHDOG_BASE_BRANCH", with WATCHDOG_BASE_BRANCH: main set once at job level.
      • pipeline-scripts/ci_dispatch_watchdog.py classifies runs whose conclusion == "action_required" as parked, and publishes the ci-dispatch-watchdog status on the PR head.
      • Stated limits, carried from fix(ci): stop the regen bot's approved runs cancelling the job that approved them (#16360) #16363: a sweep that is never started, or a job cancelled while still queued, runs no hand-off step.

    To close: after the next regen-bot push on a PR based on b68705c or later, quote the run list, showing every run for that head either approved or running, none action_required, and the approver not cancelled. Then tick AC1.

  6. github-actions commented on Sep 13, 2026

    @github-actions
    Contributor

    PR #16363 (merged to main) references this issue with a close keyword.

    fix(ci): stop the regen bot's approved runs cancelling the job that approved them (#16360)

    If this issue is fully resolved, close it manually. If work remains, no action is needed.

  7. mrveiss commented on Sep 17, 2026

    @mrveiss
    OwnerAuthor

    Verified against merged main and closing. PR #16363 carried Closes #16360 and merged 2026-09-13, but the issue stayed open — so this is a verification, not a formality.

    AC1 — the mechanism, on merged main. .github/workflows/auto-fix-generated-types.yml:68:

    group: ${{ github.workflow }}-${{ github.head_ref }}${{ endsWith(github.actor, '[bot]') && github.actor != 'dependabot[bot]' && format('-regen-{0}', github.event.pull_request.head.sha) || '' }}

    A run caused by a bot push gets -regen-<sha> appended, so it sits in a different concurrency group from the approver and cannot cancel it. That is the exact sequence this issue recorded on #16351 — released run at 15:23:12Z, approver cancelled at 15:23:15Z.

    The excluded actor matters and is deliberate: dependabot is carved out because AUTOBOT_PUSH_TOKEN may present as an app identity, so "is this a bot" is decided by actor shape rather than by a name match.

    AC1 — pinned by a test, with a control. repo_tests/approver_self_cancel_16360_test.py carries seven tests, and two of them are what make the other five trustworthy:

    Test What it establishes
    test_the_group_separates_exactly_the_runs_a_parked_bot_push_caused the mechanism, parametrized per actor kind rather than matched as text
    test_the_pre_fix_group_put_a_bot_run_in_the_approvers_group the pre-fix group reproduced the defect — so the suite would have failed before the fix
    test_the_scan_finds_the_approver_this_issue_is_about the scan finds something, so an empty result means empty

    The second is the one worth naming. A test asserting only that the current group separates the runs would pass just as happily against a group expression that separated everything, or against a scan that found no approver at all. Asserting the old expression had the bug is what pins the mechanism rather than the current text.

    AC2 and AC3 were already ticked and I have not re-derived them; the watchdog hand-off they describe is covered by test_an_interrupted_approval_is_handed_to_the_watchdog_on_the_same_base and test_the_watchdog_never_cancels_a_sweep_in_flight in the same file.

    Closing as completed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions