Implements issue #409 — Mergify Phase 1: serial merge queue, per the spec in #408
(docs/ci/mergify-integration-spec.md + mergify.yml.proposed).
DO NOT auto-merge. Merging .mergify.yml to mainactivates Mergify on every PR.
This must be reviewed first, then merged by the maintainer to go live. No auto-merge is armed.
What this adds
A single file, .mergify.yml (repo root), defining a serial merge queue that replaces the
hand-rolled poor-man's queue and the manual serial-bump grind.
Piece
Value
Why
queue_rules[default].batch_size
1
Serial — one PR per merge, no batching (batching is Phase 2 / #410).
queue_rules[default].merge_method
merge
Merge commits — never squash/rebase/fast-forward (repo policy).
queue_rules[default].merge_conditions
check-success = CI passed, -draft, -conflict, label != broken
Merge only on the single required gate, when not draft/conflicting/broken.
merge_queue.max_parallel_checks
1
True serial validation (see note below).
priority_rules
P0→10000 … P9→1000
Maps the repo's P0–P9 labels; P0 highest. Unlabelled → Mergify default medium (2000).
pull_request_rules → queue action
conditions: base = main, -draft, -conflict, label != broken, check-success = CI passed
This is what actually queues a PR.
Queue behavior (end-to-end)
A PR that is green on CI passed, targets main, and is not draft/conflicting/broken is added to the queue by the pull_request_rulesqueue action. Mergify then updates it onto
the latest main, re-runs CI, and merges it with a merge commit when merge_conditions are met — one PR at a time, in P0–P9 priority order.
Hard constraints — honored
require-up-to-date STAYS ON. Branch protection is untouched. Phase 1 (batch_size: 1 + merge_method: merge) is the only trilemma combo that keeps GitHub's Require branches to be up
to dateliterally enabledand preserves merge commits. Mergify satisfies it by updating
each PR onto the latest main and re-running CI before merging. Confirmed against the live docs
that the "must disable up-to-date" rule is batches-only and does not apply to a serial
(batch_size: 1) queue.
No batching (Phase 2 / #410 — deliberately out of scope).
Single gate stays CI passed — the exact name: of the ci-passed job (verified: the required
context is CI passed, notci-passed).
Note — actual branch-protection state I verified (worth a look): the active repo ruleset
(main, id 18347032) is what enforces require-up-to-date (strict_required_status_checks_policy: true) and requires 0 approvals (required_approving_review_count: 0). Classic protection has strict:false and a stale required_pull_request_reviews echo of 1, but that isn't enforced
(a solo account can't self-approve, yet auto-merge works — so effective approvals = 0). I
therefore dropped the spec's illustrative #approved-reviews-by >= 1 — keeping it would wedge
the solo-maintainer flow (nothing could ever merge). This matches real branch protection.
Schema verification (live docs, 2026-07-06)
Verified every key against the current Mergify docs (the format evolves — not from memory): queue_rules / queue_conditions / merge_conditions, batch_size (int 1–128 or {min,max}), merge_method (enum incl. merge), merge_queue.max_parallel_checks, priority_rules
(name/conditions/priority; 1–10000; higher = first), and the pull_request_rulesqueue action.
Pages: merge-queue/rules, workflow/actions/queue, merge-queue/priority, merge-queue/parallel-checks,
merge-queue/setup, merge-queue/batches, merge-queue/lifecycle.
Also validated mechanically:.mergify.ymlparses and conforms to Mergify's published
JSON schema (mergify-configuration-schema.json) — with a negative control (an invalid merge_method is correctly rejected against ['merge','rebase','squash','fast-forward','merge-batch']),
so the pass is not vacuous.
Coordinator Q3 — does this move a PR pending → queued → merged?
Yes. The critical finding from merge-queue/lifecycle: queue_conditions alone do NOT
auto-queue — a queue action (or @mergifyio queue / auto_merge) is required, else the Mergify Merge Queue check sits permanently pending. That is exactly why this config uses a pull_request_rulesqueue action as the trigger. With it, a matching PR is embarked (check → queued), Mergify validates on the up-to-date base, and "if the validation is successful … the
pull request leaves the queue and is merged" — resolving the pending Mergify checks.
Disabled-workflow decision
autoupdate.yml, ci-trigger.yml, and traffic-control.yml are already disabled_manually
at the GitHub level (verified via the Actions API), and Mergify supersedes all three. Decision:
leave them as-is (disabled) — no YAML change in this PR. Rationale: they're already inert (zero
conflict with Mergify), keeping this PR minimal makes it easy to review, and gh workflow enable
gives a one-command rollback path if Mergify needs to be backed out during the first activation.
Recommend deleting the three files in a follow-up once Mergify is proven. traffic_control.py + test_traffic_control.py and the traffic-control-tests job stay (still needs:-ed by ci-passed in ci.yml) — untouched.
Go-live / transition
Mergify is already installed and active; open PRs already show pendingMergify Merge Queue
/ Mergify Merge Protections checks (currently non-required, so nothing is blocked). At merge
of this PR, the queue becomes functional.
Currently open cascade PRs (all mergeable, non-draft, GitHub native auto-merge armed): #406 (P2), #401 / #400 / #397 / #395 (P3). They all match the queue conditions, so once this lands
they get picked up and merged serially by priority (P2 first, then the P3s) — replacing the
manual bumping.
Coordinator Q1 — GitHub native auto-merge vs. Mergify
They double-handle: GitHub auto-merge would merge a PR the instant CI passed + up-to-date,
racing/bypassing Mergify's queue ordering. In serial Phase 1 the result is still correct (both
honor CI passed + up-to-date + merge-commit), but for a clean single merge path:
Recommended switchover (after this PR is reviewed + merged):
Merge .mergify.yml (this PR).
Confirm the queue works on one low-priority PR end-to-end (check goes queued → PR merges
with a merge commit on CI passed green).
Disarm GitHub native auto-merge on the remaining open PRs so Mergify is the sole merge path: gh pr merge <n> --disable-auto for #406/#401/#400/#397/#395.
Going forward, stop arming GitHub native auto-merge; let Mergify queue PRs (they auto-queue once
green via the queue action — no per-PR arming needed).
Coordinator Q2 — make the Mergify checks required?
No — not in Phase 1. Keep the single required context CI passed unchanged; leave Mergify Merge Queue / Mergify Merge Protectionsinformational. Making the queue check required risks a chicken-and-egg (the check only resolves when Mergify merges) and the "PRs that
can't merge" trap. Mergify functions without its checks being required — it merges when merge_conditions (incl. CI passed) are met. Sequencing that never deadlocks: merge config →
verify one PR end-to-end → disarm GitHub auto-merge → (only later / Phase 2, if you want to force
all merges through Mergify) require the Mergify check + restrict direct merge access, done carefully
per Mergify's "merge only via Mergify" guidance. Not now.
Design decisions to confirm (reviewer)
max_parallel_checks: 1 (the spec illustrated 2). I chose 1 for a first activation: it
is the strictest, unambiguously require-up-to-date-compatible setting (Mergify updates + tests the real PR branch — no speculative temp-branch/real-branch check mismatch), it embodies Phase 1's
"serial, no throughput multiplier yet," and it caps the expensive ~15-min E2E matrix at one
concurrent run. Trivially raised to 2+ later. Say the word if you'd prefer 2 from the start.
Auto-queue scope: with 0 required approvals, the queue merges any green, non-draft,
non-conflicting, non-broken PR targeting main (matches "auto-merge by default" + the 0-approval
ruleset). If you'd rather opt PRs in explicitly (closer to today's manual auto-merge arming), gate
the queue action on a label (e.g. require a P-label, or a dedicated queue label) — one-line
change, happy to make it before merge.
Implements **issue #409 — Mergify Phase 1: serial merge queue**, per the spec in #408
(`docs/ci/mergify-integration-spec.md` + `mergify.yml.proposed`).
> **DO NOT auto-merge.** Merging `.mergify.yml` to `main` **activates Mergify** on every PR.
> This must be **reviewed first**, then merged by the maintainer to go live. No auto-merge is armed.
## What this adds
A single file, `.mergify.yml` (repo root), defining a **serial** merge queue that replaces the
hand-rolled poor-man's queue and the manual serial-bump grind.
| Piece | Value | Why |
|---|---|---|
| `queue_rules[default].batch_size` | **1** | Serial — one PR per merge, **no batching** (batching is Phase 2 / #410). |
| `queue_rules[default].merge_method` | **merge** | Merge commits — never squash/rebase/fast-forward (repo policy). |
| `queue_rules[default].merge_conditions` | `check-success = CI passed`, `-draft`, `-conflict`, `label != broken` | Merge only on the single required gate, when not draft/conflicting/broken. |
| `merge_queue.max_parallel_checks` | **1** | True serial validation (see note below). |
| `priority_rules` | **P0→10000 … P9→1000** | Maps the repo's P0–P9 labels; P0 highest. Unlabelled → Mergify default `medium` (2000). |
| `pull_request_rules` → `queue` action | conditions: `base = main`, `-draft`, `-conflict`, `label != broken`, `check-success = CI passed` | **This is what actually queues a PR.** |
### Queue behavior (end-to-end)
A PR that is green on **`CI passed`**, targets `main`, and is not draft/conflicting/`broken` is
**added to the queue** by the `pull_request_rules` `queue` action. Mergify then **updates it onto
the latest `main`, re-runs CI, and merges it with a merge commit** when `merge_conditions` are met —
**one PR at a time, in P0–P9 priority order**.
## Hard constraints — honored
- **require-up-to-date STAYS ON.** Branch protection is **untouched.** Phase 1 (`batch_size: 1` +
`merge_method: merge`) is the *only* trilemma combo that keeps GitHub's *Require branches to be up
to date* **literally enabled** *and* preserves merge commits. Mergify satisfies it by updating
each PR onto the latest `main` and re-running CI before merging. Confirmed against the live docs
that the "must disable up-to-date" rule is **batches-only** and does **not** apply to a serial
(`batch_size: 1`) queue.
- **No batching** (Phase 2 / #410 — deliberately out of scope).
- **Single gate stays `CI passed`** — the exact `name:` of the `ci-passed` job (verified: the required
context is `CI passed`, **not** `ci-passed`).
> **Note — actual branch-protection state I verified (worth a look):** the active repo **ruleset**
> (`main`, id 18347032) is what enforces require-up-to-date (`strict_required_status_checks_policy:
> true`) and requires **0 approvals** (`required_approving_review_count: 0`). Classic protection has
> `strict:false` and a stale `required_pull_request_reviews` echo of `1`, but that isn't enforced
> (a solo account can't self-approve, yet auto-merge works — so effective approvals = **0**). I
> therefore **dropped the spec's illustrative `#approved-reviews-by >= 1`** — keeping it would wedge
> the solo-maintainer flow (nothing could ever merge). This matches real branch protection.
## Schema verification (live docs, 2026-07-06)
Verified every key against the current Mergify docs (the format evolves — not from memory):
`queue_rules` / `queue_conditions` / `merge_conditions`, `batch_size` (int 1–128 or `{min,max}`),
`merge_method` (enum incl. `merge`), `merge_queue.max_parallel_checks`, `priority_rules`
(name/conditions/priority; 1–10000; higher = first), and the `pull_request_rules` `queue` action.
Pages: merge-queue/rules, workflow/actions/queue, merge-queue/priority, merge-queue/parallel-checks,
merge-queue/setup, merge-queue/batches, merge-queue/lifecycle.
**Also validated mechanically:** `.mergify.yml` **parses** and **conforms to Mergify's published
JSON schema** (`mergify-configuration-schema.json`) — with a negative control (an invalid
`merge_method` is correctly rejected against `['merge','rebase','squash','fast-forward','merge-batch']`),
so the pass is not vacuous.
### Coordinator Q3 — does this move a PR *pending → queued → merged*?
Yes. The **critical** finding from `merge-queue/lifecycle`: **`queue_conditions` alone do NOT
auto-queue** — a `queue` action (or `@mergifyio queue` / `auto_merge`) is required, else the
`Mergify Merge Queue` check **sits permanently pending**. That is exactly why this config uses a
`pull_request_rules` `queue` action as the trigger. With it, a matching PR is embarked (check →
*queued*), Mergify validates on the up-to-date base, and *"if the validation is successful … the
pull request leaves the queue and is merged"* — resolving the pending Mergify checks.
## Disabled-workflow decision
`autoupdate.yml`, `ci-trigger.yml`, and `traffic-control.yml` are **already `disabled_manually`**
at the GitHub level (verified via the Actions API), and Mergify supersedes all three. **Decision:
leave them as-is (disabled) — no YAML change in this PR.** Rationale: they're already inert (zero
conflict with Mergify), keeping this PR minimal makes it easy to review, and `gh workflow enable`
gives a one-command **rollback path** if Mergify needs to be backed out during the first activation.
Recommend **deleting** the three files in a follow-up once Mergify is proven. `traffic_control.py` +
`test_traffic_control.py` and the **`traffic-control-tests` job stay** (still `needs:`-ed by
`ci-passed` in `ci.yml`) — untouched.
## Go-live / transition
Mergify is **already installed and active**; open PRs already show *pending* `Mergify Merge Queue`
/ `Mergify Merge Protections` checks (currently **non-required**, so nothing is blocked). At merge
of this PR, the queue becomes functional.
**Currently open cascade PRs** (all mergeable, non-draft, **GitHub native auto-merge armed**):
#406 (P2), #401 / #400 / #397 / #395 (P3). They all match the queue conditions, so once this lands
they get **picked up and merged serially by priority** (P2 first, then the P3s) — replacing the
manual bumping.
### Coordinator Q1 — GitHub native auto-merge vs. Mergify
They **double-handle**: GitHub auto-merge would merge a PR the instant `CI passed` + up-to-date,
racing/bypassing Mergify's queue ordering. In serial Phase 1 the *result* is still correct (both
honor `CI passed` + up-to-date + merge-commit), but for a clean single merge path:
**Recommended switchover (after this PR is reviewed + merged):**
1. Merge `.mergify.yml` (this PR).
2. Confirm the queue works on **one** low-priority PR end-to-end (check goes *queued* → PR merges
with a merge commit on `CI passed` green).
3. **Disarm GitHub native auto-merge** on the remaining open PRs so Mergify is the sole merge path:
`gh pr merge <n> --disable-auto` for #406/#401/#400/#397/#395.
4. Going forward, stop arming GitHub native auto-merge; let Mergify queue PRs (they auto-queue once
green via the `queue` action — no per-PR arming needed).
### Coordinator Q2 — make the Mergify checks *required*?
**No — not in Phase 1.** Keep the **single required context `CI passed`** unchanged; leave
`Mergify Merge Queue` / `Mergify Merge Protections` **informational**. Making the queue check
*required* risks a chicken-and-egg (the check only resolves when Mergify merges) and the "PRs that
can't merge" trap. Mergify functions **without** its checks being required — it merges when
`merge_conditions` (incl. `CI passed`) are met. **Sequencing that never deadlocks:** merge config →
verify one PR end-to-end → disarm GitHub auto-merge → (only later / Phase 2, if you want to *force*
all merges through Mergify) require the Mergify check + restrict direct merge access, done carefully
per Mergify's "merge only via Mergify" guidance. Not now.
## Design decisions to confirm (reviewer)
- **`max_parallel_checks: 1`** (the spec illustrated `2`). I chose **1** for a first activation: it
is the strictest, unambiguously require-up-to-date-compatible setting (Mergify updates + tests the
*real* PR branch — no speculative temp-branch/real-branch check mismatch), it embodies Phase 1's
"serial, no throughput multiplier yet," and it caps the expensive ~15-min E2E matrix at one
concurrent run. Trivially raised to 2+ later. Say the word if you'd prefer `2` from the start.
- **Auto-queue scope:** with 0 required approvals, the queue merges **any** green, non-draft,
non-conflicting, non-`broken` PR targeting `main` (matches "auto-merge by default" + the 0-approval
ruleset). If you'd rather opt PRs in explicitly (closer to today's manual auto-merge arming), gate
the `queue` action on a label (e.g. require a P-label, or a dedicated `queue` label) — one-line
change, happy to make it before merge.
Relates #407 / #408. Blocks/precedes Phase 2 (#410).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Implements issue #409 — Mergify Phase 1: serial merge queue, per the spec in #408
(
docs/ci/mergify-integration-spec.md+mergify.yml.proposed).What this adds
A single file,
.mergify.yml(repo root), defining a serial merge queue that replaces thehand-rolled poor-man's queue and the manual serial-bump grind.
queue_rules[default].batch_sizequeue_rules[default].merge_methodqueue_rules[default].merge_conditionscheck-success = CI passed,-draft,-conflict,label != brokenmerge_queue.max_parallel_checkspriority_rulesmedium(2000).pull_request_rules→queueactionbase = main,-draft,-conflict,label != broken,check-success = CI passedQueue behavior (end-to-end)
A PR that is green on
CI passed, targetsmain, and is not draft/conflicting/brokenisadded to the queue by the
pull_request_rulesqueueaction. Mergify then updates it ontothe latest
main, re-runs CI, and merges it with a merge commit whenmerge_conditionsare met —one PR at a time, in P0–P9 priority order.
Hard constraints — honored
batch_size: 1+merge_method: merge) is the only trilemma combo that keeps GitHub's Require branches to be upto date literally enabled and preserves merge commits. Mergify satisfies it by updating
each PR onto the latest
mainand re-running CI before merging. Confirmed against the live docsthat the "must disable up-to-date" rule is batches-only and does not apply to a serial
(
batch_size: 1) queue.CI passed— the exactname:of theci-passedjob (verified: the requiredcontext is
CI passed, notci-passed).Schema verification (live docs, 2026-07-06)
Verified every key against the current Mergify docs (the format evolves — not from memory):
queue_rules/queue_conditions/merge_conditions,batch_size(int 1–128 or{min,max}),merge_method(enum incl.merge),merge_queue.max_parallel_checks,priority_rules(name/conditions/priority; 1–10000; higher = first), and the
pull_request_rulesqueueaction.Pages: merge-queue/rules, workflow/actions/queue, merge-queue/priority, merge-queue/parallel-checks,
merge-queue/setup, merge-queue/batches, merge-queue/lifecycle.
Also validated mechanically:
.mergify.ymlparses and conforms to Mergify's publishedJSON schema (
mergify-configuration-schema.json) — with a negative control (an invalidmerge_methodis correctly rejected against['merge','rebase','squash','fast-forward','merge-batch']),so the pass is not vacuous.
Coordinator Q3 — does this move a PR pending → queued → merged?
Yes. The critical finding from
merge-queue/lifecycle:queue_conditionsalone do NOTauto-queue — a
queueaction (or@mergifyio queue/auto_merge) is required, else theMergify Merge Queuecheck sits permanently pending. That is exactly why this config uses apull_request_rulesqueueaction as the trigger. With it, a matching PR is embarked (check →queued), Mergify validates on the up-to-date base, and "if the validation is successful … the
pull request leaves the queue and is merged" — resolving the pending Mergify checks.
Disabled-workflow decision
autoupdate.yml,ci-trigger.yml, andtraffic-control.ymlare alreadydisabled_manuallyat the GitHub level (verified via the Actions API), and Mergify supersedes all three. Decision:
leave them as-is (disabled) — no YAML change in this PR. Rationale: they're already inert (zero
conflict with Mergify), keeping this PR minimal makes it easy to review, and
gh workflow enablegives a one-command rollback path if Mergify needs to be backed out during the first activation.
Recommend deleting the three files in a follow-up once Mergify is proven.
traffic_control.py+test_traffic_control.pyand thetraffic-control-testsjob stay (stillneeds:-ed byci-passedinci.yml) — untouched.Go-live / transition
Mergify is already installed and active; open PRs already show pending
Mergify Merge Queue/
Mergify Merge Protectionschecks (currently non-required, so nothing is blocked). At mergeof this PR, the queue becomes functional.
Currently open cascade PRs (all mergeable, non-draft, GitHub native auto-merge armed):
#406 (P2), #401 / #400 / #397 / #395 (P3). They all match the queue conditions, so once this lands
they get picked up and merged serially by priority (P2 first, then the P3s) — replacing the
manual bumping.
Coordinator Q1 — GitHub native auto-merge vs. Mergify
They double-handle: GitHub auto-merge would merge a PR the instant
CI passed+ up-to-date,racing/bypassing Mergify's queue ordering. In serial Phase 1 the result is still correct (both
honor
CI passed+ up-to-date + merge-commit), but for a clean single merge path:Recommended switchover (after this PR is reviewed + merged):
.mergify.yml(this PR).with a merge commit on
CI passedgreen).gh pr merge <n> --disable-autofor #406/#401/#400/#397/#395.green via the
queueaction — no per-PR arming needed).Coordinator Q2 — make the Mergify checks required?
No — not in Phase 1. Keep the single required context
CI passedunchanged; leaveMergify Merge Queue/Mergify Merge Protectionsinformational. Making the queue checkrequired risks a chicken-and-egg (the check only resolves when Mergify merges) and the "PRs that
can't merge" trap. Mergify functions without its checks being required — it merges when
merge_conditions(incl.CI passed) are met. Sequencing that never deadlocks: merge config →verify one PR end-to-end → disarm GitHub auto-merge → (only later / Phase 2, if you want to force
all merges through Mergify) require the Mergify check + restrict direct merge access, done carefully
per Mergify's "merge only via Mergify" guidance. Not now.
Design decisions to confirm (reviewer)
max_parallel_checks: 1(the spec illustrated2). I chose 1 for a first activation: itis the strictest, unambiguously require-up-to-date-compatible setting (Mergify updates + tests the
real PR branch — no speculative temp-branch/real-branch check mismatch), it embodies Phase 1's
"serial, no throughput multiplier yet," and it caps the expensive ~15-min E2E matrix at one
concurrent run. Trivially raised to 2+ later. Say the word if you'd prefer
2from the start.non-conflicting, non-
brokenPR targetingmain(matches "auto-merge by default" + the 0-approvalruleset). If you'd rather opt PRs in explicitly (closer to today's manual auto-merge arming), gate
the
queueaction on a label (e.g. require a P-label, or a dedicatedqueuelabel) — one-linechange, happy to make it before merge.
Relates #407 / #408. Blocks/precedes Phase 2 (#410).
🤖 Generated with Claude Code
Merge Protections
🟢 Merge protection satisfied — ready to merge.
Show 1 satisfied protection
🟢 📃 Configuration Change Requirements
Mergify configuration change
check-success = Configuration changed