ci: a label CI does not read no longer starts a full CI run

Merged
TE
6d ago
+238-22

Leo (2026-10-31): "JUST MERGE" "IT IS BINARY DONE OR NOT DONE".

Problem

ci.yml triggers on pull_request labeled/unlabeled, so every label change, risk:* swaps included, started a second full CI run at the same head. On #29618 the duplicate run 41241282080 sat queued for over an hour behind the first one (label runs share its concurrency group and don't cancel), and while it was pending the webview gate kept waiting. cmux-vm.yml had the same trigger with cancel-in-progress: true, so any label change cancelled and restarted its check.

What changed

  • ci.yml, for pull requests into feat-cmux-next: every job but ci-status gets one guard that is false only for a label event whose label is not full-ci or dev-build (the two labels it reads). ci-status still runs on those events and passes at once into feat-cmux-next, because a skipped ci-status newer than a red or pending one would read as passing to a newest-by-name gate like gh-merge-green (review finding at 336d0db). PRs into other bases (main) keep re-running as before, so main's required checks are never superseded by a skip. The concurrency group gets a -label-<run_id> suffix for those events, so the skipped run doesn't wait behind the real run, replace a pending one, or cancel anything. It finishes in seconds with every job skipped. full-ci and dev-build work as before.
  • scripts/ci/next_batch.py (risk:high, since it touches a merge enforcer): newest_checks now treats a skipped newest run like a cancelled one and falls back to the newest run that has a verdict. Without this, a label run's skipped jobs (VM check, web) would hide an earlier red from the batch queue (review finding at 9d793d0). merge_guard's newest already works this way. Red without the fix, green with it: tests/test_next_batch_groups.py::GroupCycle::test_newest_run_edge_cases.
  • cmux-vm.yml: same pattern (feat-cmux-next only), keyed on cmux-vm-breaking-change-ok.
  • Left alone because they're cheap and read labels on purpose: risk-tier, merge-freeze, pr-capture-check, pr-perf-section-check, backend-migrations, cmux-next-batch (debounce), cmux-next-media and cmux-next-motion-golden (already gated on their label).
  • The skipped duplicate doesn't hide an earlier verdict: the status rollup still lists the earlier suite's checks (checked on #29618), and merge-guard reads the newest check that has a verdict.

Test

tests/test_ci_delta_since_green.py drops the guard before its small evaluator, which has no !, runs. The new test also runs as a step in ci-guards.yml's ci group.

New tests/test_ci_label_events_skip_full_ci.py (linux-guard lane). Every label-triggered workflow must be either GUARDED (its labels listed; every job a label event could start ends with the guard; its concurrency group isolates other label events) or in REACTS with a reason. It also evaluates the guard: synchronize, opened, push, its own labels and a risk:high label into main give true; risk:high, risk:normal and ready-to-land give false. It fails on the base workflows (4+ job failures) and passes on the head. I updated one existing assertion (linux-preflight) that pinned the old if: text. test_ci_pr_runs_cancel_superseded.py, test_ci_linux_compile_runners.py and test_ci_linux_single_size.py pass, and actionlint output is unchanged.

Observed / Not run

  • Observed: the tests above, run locally. test_ci_workflow_run_sources::test_ci_failfast... and test_ci_change_areas::test_repository_helpers_route_by_where_they_run fail the same way on base (no tests job; owned_pool_rescue route).
  • Not run: a live label event on a PR. That's the first label swap after merge.

Changelog

none

Agent session: agent session

šŸ¤– Generated with Claude Code