ci(mergify): implement Phase 1 — serial merge queue (require-up-to-date KEPT ON) #412

Merged
JMR-dev merged 1 commits from ci-409-mergify-phase1 into main 2026-07-07 03:50:20 +00:00
JMR-dev commented 2026-07-07 03:30:05 +00:00 (Migrated from github.com)

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

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)
mergify[bot] commented 2026-07-07 03:30:42 +00:00 (Migrated from github.com)

Merge Protections

🟢 Merge protection satisfied — ready to merge.

Show 1 satisfied protection

🟢 📃 Configuration Change Requirements

Mergify configuration change

  • check-success = Configuration changed
# Merge Protections 🟢 **Merge protection satisfied** — ready to merge. <details><summary>Show 1 satisfied protection</summary> ## 🟢 📃 Configuration Change Requirements Mergify configuration change - [X] `check-success = Configuration changed` </details>
Sign in to join this conversation.