gh-merge-green: fast gate by default for feat-cmux-next, --full keeps the full wait (no bead)
This is a merge-policy change. The coordinator approved it on 2026-10-19, and leoli-f6 decided the two policy calls listed under Policy. It is reversible: pass --full, or revert this PR.
gh-merge-green waits for every check before merging, which slows feat-cmux-next iteration (median 25.9 min from head push to merge over the last 60 merged PRs). For PRs into feat-cmux-next it now waits only for a fast gate by default and merges once that is green. The full suites still run after the merge, and the culprit bot and base sheriff revert or fix forward. --full keeps the old wait. Main and every other base always get the full wait.
What the fast gate waits for
scripts/ci/merge_fast_gate.py polls the exact head once per 30 s with one GraphQL gh pr view --json headRefOid,baseRefName,statusCheckRollup,files read, for up to --wait seconds (default 2400). It waits for:
cmux-next path routing, when the change is inside cmux-next.yml'spull_requestpaths, read from the base so a PR can't drop itself from the filter;cmux-next fast tier;- each touched compile that routing started:
cmux-next Swift canaryandcmux app scheme compile (Debug). A check reported under the raw expression name (inputs.dev_daily == true && ... || 'cmux app scheme compile (Debug)', as on #21981) counts as the Debug compile; react-apps-check, whenscripts/ci/detect_ci_change_areas.py(the helper checkout's copy, which ci.yml'swebjob routes by) sends a changed file to web (judged on the helper's paginated REST file list, notgh pr view --json files, which stops at 100), includingconfig/**andResources/markdown-viewer/**, or when thewebcaller job is already running. The wait ends when the caller finishes without one; a red caller is a red;- god-file, l10n and concurrency lint (today
cmux-next checks (god files, concurrency, crash safety, l10n)) andrisk evidence, when reported. A red one refuses outright, with no--overrideand no base-red excuse, as in main's full gate (#21692).
It refuses:
- a completed red in those lanes, in the web, app size and crash families, or in
ci-status,cmux-next Release compile (Xcode 26)orcmux-next swift test, unless the base has the same red (base_red_excuse.py) or--overridewaives it; - a cancelled, timed-out, startup-failed or stale fast lane, or a skipped
cmux-next fast tieron a change inside the paths filter, even with--override, because it has no verdict to waive; - a pull request from an untrusted fork without
--trust-fork-head <sha>, before the gate reads or waits for anything (#21587); - a head or base that changed during the wait;
- a lane it waits for that is no longer a job name in the head's
cmux-next.yml,ci.ymlorci-web.yml, so a rename can't silently drop a check. An unreadable workflow file also refuses.
A pending check outside the waited set does not block. Of several runs of one check, the latest started wins.
What stays the same, and what both gates now share
Conflict markers, merge-freeze issues and the freeze-ok label (#21957), the GraphQL-to-REST metadata and merge fallback (#21529), fork-head trust and --trust-fork-head, and --main-fix are unchanged. The full gate body is main's code; optional-workflow 404 handling and advisory Vercel statuses still apply there. The GraphQL and Git readers now live outside full_gate, so the fast path's changed-file listing, conflict-marker read and freeze check keep their fallbacks.
New on both gates: right before gh pr merge, the helper re-reads head, base and state (GraphQL, REST on a rate limit) and refuses if the head or base moved. --match-head-commit pins only the head, so before this a PR retargeted from feat-cmux-next to main during the fast wait would have merged into main on the fast gate. This replaces the --override path's own head-only re-check.
The fast gate skips the local merged-tree checks except on protected gate paths (see Policy). outcome=merged is set only after a real merge, including the REST path. One line prints at exit, merged or refused, for example:
gh-merge-green: merged manaflow-ai/cmux#42 (fast gate: cmux-next Swift canary, cmux-next fast tier, cmux-next path routing, conflict markers, freezes; waited 3m10s, 214s in all)
docs/ci-runners.md describes both gates.
Evidence
Merge wait, measured on the last 60 PRs merged into feat-cmux-next before this PR was written (head push time is the earliest check suite):
| median | p90 | |
|---|---|---|
| merged under the full wait | 25.9 min | 94.8 min |
| fast gate lanes done on the same heads | 15.2 min | 53.8 min |
That is when the fast lanes finished on those heads, not a measured merge time under the new gate.
Review fixes. Each has its test committed first, and each test failed before its fix:
| Bug | Test | Red at | Fixed in |
|---|---|---|---|
| An untrusted fork merged on green fast checks | test_the_fast_gate_refuses_an_untrusted_fork | 3d63242b9438 (#22108) | 0f11f22d6eef |
| A skipped fast tier counted as a pass | test_a_skipped_fast_tier_has_no_verdict | 3d63242b9438 | 0f11f22d6eef |
| Web routing read only the first 100 files | test_the_callers_full_file_list_routes_web; through the helper, test_a_web_file_past_the_first_100_still_waits_for_react_apps_check | 3d63242b9438 | 0f11f22d6eef |
| The paths filter came from the PR head | test_the_fast_gate_reads_the_paths_filter_from_the_base | 2d7ddfeee357 | ad3393c92c33 |
| Protected gate paths skipped the merged-tree checks | test_the_fast_gate_runs_the_merged_tree_checks_for_a_pr_touching_gate_paths (3 subtests); test_the_fast_gate_skips_the_merged_tree_checks_for_an_ordinary_cmux_next_pr | 2d7ddfeee357 | ad3393c92c33 |
The 100-file test that goes through the helper was added after the fix. Dropping --files-from from gh-merge-green makes it fail.
The fork and freeze ordering tests passed on the code before the fix, so they guard behavior rather than prove a fix: test_an_untrusted_fork_refuses_before_the_fast_wait (pending lanes, --wait 0, --override: the fork is refused, not the wait) and test_a_reviewed_fork_on_the_fast_gate_still_stops_at_a_freeze.
Freezes (#21957): the merge_freeze.py check runs after either gate on every base. test_the_fast_gate_still_refuses_a_frozen_path (with and without --override) fails if the fast gate skips that check. The other fast-gate freeze tests are test_the_fast_gate_merges_an_unfrozen_path_and_the_owners_freeze_ok and test_a_main_merge_without_full_checks_freezes_on_the_full_gate.
The four required behaviors:
- feat-cmux-next defaults to the fast gate:
test_feat_next_merges_on_the_fast_gate_without_waiting_for_the_full_suites. --fullkeeps the full wait:test_full_keeps_the_wait_for_every_check.- A frozen path is still refused on the fast gate: the freeze tests above.
- main is unchanged:
test_main_keeps_the_full_wait_without_full.
Run on Linux (huge-blue) at ad3393c92c33, plus the 100-file helper test at 3e1360f40a11:
test_gh_merge_green: 131 passed.test_merge_fast_gate: 21 passed.test_merge_freeze: 16 passed.test_base_red_excuse: 26 passed.test_gh_merge_green_revert: 5 passed.
The earlier evidence for the gate still holds: the merge-wait table above and the red-first commits 71661076d553 and 24b05719d6f6. Not run: shellcheck and actionlint. No live merge has been made with the fast gate.
tests/test_merge_fast_gate.py is registered in tests/test-execution.toml (linux-guard) and runs in a ci-guards.yml ci step.
Policy (decided by leoli-f6, 2026-10-19)
- Changes outside cmux-next.yml's paths and web routing merge with no checks to wait for. That covers
backend/**,docs/and other non-web paths. This stays as written. The freeze check, conflict markers and the lint and risk-evidence refusals still apply. - The fast gate skips the merged-tree checks (merge cleanliness, the god-file check and
vp check), except for a PR that touches a path listed inscripts/ci/merge-fast-gate-protected-paths.txt:.github/workflows/,scripts/ci/(shared CI scripts,merge_freeze.py, the routing configdetect_ci_change_areas.pyand this list itself) andscripts/gh-merge-green. Such a PR gets every merged-tree check back. The list is read from the base. Until the base carries the file, the helper checkout's copy (origin/main) applies, and if neither can be read, every changed file counts as protected.
Remaining known gaps
- On unlabeled PRs the gate is effectively path routing, the fast tier and lint. cmux-next.yml skips the Swift canary, the Release compile, swift test and the Debug compile on those PRs.
- A queued fast tier that never starts waits the full 40 minutes before the gate refuses.
- Web routing uses main's copy of
detect_ci_change_areas.py. feat-cmux-next's copy has extra exact web inputs. - The job-name check still reads the head's workflows. That is deliberate: it asks whether the lanes the gate waits on still exist on this head.
Changelog
none
🤖 Generated with Claude Code