#350 made ci-trigger.yml dispatch ci.yml with the built-in GITHUB_TOKEN, on the claim that a workflow_dispatch is anti-recursion-exempt so no PAT is needed. In practice a GITHUB_TOKEN-triggered run is held in `action_required` awaiting manual approval and never runs un-attended, so auto-updated PRs' CI never ran (stalled #285). The original #349 design was right: dispatch with a PAT so the run executes as the authorized owner with no approval gate. - ci-trigger.yml: the trigger step's GH_TOKEN is now `${{ secrets.AUTOUPDATE_TOKEN || github.token }}` (was `${{ github.token }}`). AUTOUPDATE_TOKEN (the PAT) is REQUIRED for the scheduler; the `|| github.token` fallback stays fail-open but only starts CI if repo settings don't gate GITHUB_TOKEN-triggered runs. - autoupdate.yml: branch update stays on GITHUB_TOKEN (must NOT retrigger CI -- that would re-introduce the cascade). Clarified that AUTOUPDATE_TOKEN is still required by the repo (by ci-trigger.yml) so the secret isn't deleted. - Corrected the now-wrong "no PAT needed / workflow_dispatch anti-recursion-exempt" comments in ci-trigger.yml and the traffic_control.py docstrings. updates = GITHUB_TOKEN, triggering = PAT. Validation: all three workflow YAMLs parse clean; traffic-control unit tests still pass (59 tests) -- the change is workflow-env only, script logic unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
101 lines
6.0 KiB
YAML
101 lines
6.0 KiB
YAML
# SPDX-License-Identifier: GPL-3.0-or-later
|
||
name: CI trigger (traffic-controller)
|
||
|
||
# The traffic-controller SCHEDULER (issue #349). It OWNS CI *triggering*. After main advances,
|
||
# autoupdate.yml updates every behind PR's branch with the built-in GITHUB_TOKEN which, by
|
||
# GitHub's anti-recursion rule, does NOT start CI — so those PRs sit with absent/stale required
|
||
# checks on their new head SHA and cannot merge. This workflow then (re-)triggers CI for the
|
||
# highest-priority such PR(s), a few at a time (an inflight cap), in the existing P0–P9 /
|
||
# broken-draft priority order — a poor-man's merge queue that replaces the old "every merge
|
||
# re-runs every PR" thundering herd (the cascade; see the ci-merge-cascade note + issue #349).
|
||
#
|
||
# HOW IT TRIGGERS: `traffic_control.py --mode trigger` runs `gh workflow run ci.yml --ref
|
||
# <pr-head-branch>`, dispatching with the AUTOUPDATE_TOKEN PAT — NOT the built-in GITHUB_TOKEN.
|
||
# A workflow run triggered by GITHUB_TOKEN is held in the `action_required` state waiting on
|
||
# MANUAL approval and never runs un-attended (confirmed empirically on #285 / #350: it sits
|
||
# `action_required`, while the same dispatch by an authorized user runs immediately) — which
|
||
# would defeat the whole scheduler. A PAT dispatch runs AS the authorized token owner, so the
|
||
# run starts immediately with no approval gate (this is the original #349 design; #350's "no
|
||
# PAT needed / workflow_dispatch is anti-recursion-exempt" claim was WRONG — see #351).
|
||
# AUTOUPDATE_TOKEN is therefore REQUIRED for this scheduler. The dispatched run executes on the
|
||
# PR's head branch, so its checks land on the PR head SHA and satisfy branch protection.
|
||
#
|
||
# WHEN IT RUNS:
|
||
# • workflow_run, after "Auto-update PR branches" completes — the race-free moment: autoupdate
|
||
# has finished moving branches to their new (checkless) head SHAs, so this pass sees exactly
|
||
# the PRs that now need a run. (A bare `push: main` trigger would race autoupdate and often
|
||
# read the pre-update SHAs, missing them until the next pass.)
|
||
# • schedule (cron) — a backstop so no PR is ever permanently un-triggered even if a
|
||
# workflow_run is missed/skipped (part of the fail-open guarantee), and so a brand-new PR
|
||
# whose first `on: pull_request` run got cancelled is still picked up.
|
||
# • workflow_dispatch — manual kick.
|
||
#
|
||
# FAIL-OPEN: the script guards every gh call and always exits 0; and structurally, ci.yml keeps
|
||
# its `on: pull_request` trigger, so a human push (and a brand-new PR) always triggers CI
|
||
# regardless of this scheduler — CI can never become permanently un-triggerable. Fork PRs (no
|
||
# token/secret access) are skipped here and left to `on: pull_request`, so they are never wedged.
|
||
|
||
on:
|
||
workflow_run:
|
||
workflows: ["Auto-update PR branches"]
|
||
types: [completed]
|
||
schedule:
|
||
# Backstop cadence (UTC). GitHub may delay scheduled runs under load; that is fine — this
|
||
# is only a safety net behind the immediate workflow_run trigger above.
|
||
- cron: "*/15 * * * *"
|
||
workflow_dispatch:
|
||
|
||
# Trigger-only; this workflow never gates a merge. The gh calls run as GH_TOKEN, which is
|
||
# normally the AUTOUPDATE_TOKEN PAT (see the step below). These permissions govern the built-in
|
||
# GITHUB_TOKEN, used only on the fail-open fallback path when AUTOUPDATE_TOKEN is absent:
|
||
# `actions: write` lets it dispatch ci.yml (workflow_dispatch) and cancel strictly-lower runs
|
||
# when a P0 emergency preempts; `pull-requests: read` + `contents: read` cover the PR/label
|
||
# enumeration. (A GITHUB_TOKEN dispatch needs manual approval, so that fallback only actually
|
||
# starts CI if repo settings don't gate GITHUB_TOKEN-triggered runs — the PAT is the real path.)
|
||
permissions:
|
||
contents: read
|
||
pull-requests: read
|
||
actions: write
|
||
|
||
# One trigger pass at a time. Do NOT cancel an in-flight pass (cancel-in-progress: false):
|
||
# a half-finished pass could leave some needy PRs un-triggered until the next pass.
|
||
concurrency:
|
||
group: ci-trigger
|
||
cancel-in-progress: false
|
||
|
||
jobs:
|
||
trigger:
|
||
name: Trigger CI by priority
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 10 # generous backstop; the script only enumerates + dispatches, no waits
|
||
steps:
|
||
- name: Check out source
|
||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||
|
||
- name: Set up Python
|
||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||
with:
|
||
python-version: "3.x"
|
||
|
||
# gh is auto-configured from GH_TOKEN / GH_REPO. The script guards every gh call and
|
||
# always exits 0, so a hiccup (API error, missing permission, fork PR) can never wedge CI
|
||
# — and even a total failure here leaves ci.yml's `on: pull_request` path intact.
|
||
- name: Trigger CI for the highest-priority PR(s) needing a run
|
||
env:
|
||
# AUTOUPDATE_TOKEN (a PAT) is REQUIRED here: a CI run dispatched by the built-in
|
||
# GITHUB_TOKEN is held for MANUAL approval (`action_required`) and never runs
|
||
# un-attended, so the scheduler must dispatch AS the PAT's authorized owner to start
|
||
# runs with no approval gate. `|| github.token` keeps this fail-open when the secret is
|
||
# absent, but that GITHUB_TOKEN fallback only actually starts CI if repo settings don't
|
||
# gate GITHUB_TOKEN-triggered runs — the PAT is the intended path (see #351).
|
||
GH_TOKEN: ${{ secrets.AUTOUPDATE_TOKEN || github.token }}
|
||
GH_REPO: ${{ github.repository }}
|
||
# Poor-man's merge-queue width: at most this many PRs run CI concurrently under the
|
||
# scheduler (a P0 emergency bypasses this cap). Kept conservative because each PR
|
||
# fans out to the whole E2E matrix (~8 API levels + preview); this is the main knob
|
||
# to raise for throughput vs runner budget. The coordinator drives runner allocation.
|
||
MAX_INFLIGHT_RUNS: "2"
|
||
# The workflow file the scheduler enumerates runs for and dispatches.
|
||
CI_WORKFLOW_FILE: "ci.yml"
|
||
run: python3 .github/scripts/traffic_control.py --mode trigger
|