[object Object]

← back to Cli Printing Press

chore(ci): enable parallel queue checks in Mergify (#1668)

019dcf3b98e3e4ee12611b7898e298787ff5ca93 · 2026-05-19 12:05:20 -0700 · Trevin Chow

* chore(ci): enable parallel queue checks in Mergify

Raise `merge_queue.max_parallel_checks` from 1 to 3 and split
`queue_conditions` / `merge_conditions` so Mergify can speculate on
multiple queued PRs in parallel via `mergify/merge-queue/*` draft PRs.

The prior config pinned both `batch_size` and `max_parallel_checks` to
1 under the belief that both were paywalled features on the OSS plan.
Mergify support clarified (2026-05-19) that only `batch_size > 1`
triggers the paid "Merge Queue Batch" feature; `max_parallel_checks`
is a separate, free knob. `batch_size` stays at 1.

Greptile compatibility, which was the secondary reason for the old
pinning, is handled by the new split:

- `queue_conditions` (gates entry on the source PR) keeps the Greptile
  + `#review-threads-unresolved = 0` requirements.
- `merge_conditions` (gates the merge after the speculative draft PR's
  CI passes) requires only the CI checks that actually run on bot-owned
  branches. Greptile silent-skips bot branches by design, so requiring
  it here would block every queued PR.
- `merge_protections` continues to re-enforce Greptile + review-threads
  on the source PR before the GitHub merge, as belt-and-suspenders.

* docs(ci): capture mergify parallel-checks learning under docs/solutions/

Add a knowledge-track learning doc that explains the corrected mental
model for Mergify on the OSS plan: `batch_size > 1` is paywalled (the
"Merge Queue Batch" feature), `max_parallel_checks > 1` is not, and
the three-block (queue_conditions / merge_conditions / merge_protections)
pattern is what makes draft_pr mode safe alongside Greptile, which
silent-skips bot branches by design.

Cross-references the wrong-turn history (#1306 raise, #1321 revert,
#1336 single-block narrowing) and the present fix (#1668) so the doc
is useful both as forward guidance and as context for why the prior
config was conservative.

* fix(ci): drop pr-title from mergify merge_conditions

Greptile flagged this as a P1 on PR #1668: `.github/workflows/pr-title.yml`
skips semantic validation only for titles starting with `merge queue:`
(the batch-mode prefix from #1306). In `draft_pr` mode with `batch_size: 1`,
Mergify's draft PR title format is not guaranteed to match that prefix,
so requiring `check-success = pr-title` on the draft could stall every
queued PR.

The source PR's title is already validated at `queue_conditions` entry
and cannot change while queued, so re-checking it on the speculative
draft is redundant. Drop it from `merge_conditions` and document the
trap in the comment block so the next person editing this config sees
why the omission is intentional.

Also update the learning doc with this as a third non-obvious failure
mode under "Why This Matters".

Files touched

Diff

commit 019dcf3b98e3e4ee12611b7898e298787ff5ca93
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Tue May 19 12:05:20 2026 -0700

    chore(ci): enable parallel queue checks in Mergify (#1668)
    
    * chore(ci): enable parallel queue checks in Mergify
    
    Raise `merge_queue.max_parallel_checks` from 1 to 3 and split
    `queue_conditions` / `merge_conditions` so Mergify can speculate on
    multiple queued PRs in parallel via `mergify/merge-queue/*` draft PRs.
    
    The prior config pinned both `batch_size` and `max_parallel_checks` to
    1 under the belief that both were paywalled features on the OSS plan.
    Mergify support clarified (2026-05-19) that only `batch_size > 1`
    triggers the paid "Merge Queue Batch" feature; `max_parallel_checks`
    is a separate, free knob. `batch_size` stays at 1.
    
    Greptile compatibility, which was the secondary reason for the old
    pinning, is handled by the new split:
    
    - `queue_conditions` (gates entry on the source PR) keeps the Greptile
      + `#review-threads-unresolved = 0` requirements.
    - `merge_conditions` (gates the merge after the speculative draft PR's
      CI passes) requires only the CI checks that actually run on bot-owned
      branches. Greptile silent-skips bot branches by design, so requiring
      it here would block every queued PR.
    - `merge_protections` continues to re-enforce Greptile + review-threads
      on the source PR before the GitHub merge, as belt-and-suspenders.
    
    * docs(ci): capture mergify parallel-checks learning under docs/solutions/
    
    Add a knowledge-track learning doc that explains the corrected mental
    model for Mergify on the OSS plan: `batch_size > 1` is paywalled (the
    "Merge Queue Batch" feature), `max_parallel_checks > 1` is not, and
    the three-block (queue_conditions / merge_conditions / merge_protections)
    pattern is what makes draft_pr mode safe alongside Greptile, which
    silent-skips bot branches by design.
    
    Cross-references the wrong-turn history (#1306 raise, #1321 revert,
    #1336 single-block narrowing) and the present fix (#1668) so the doc
    is useful both as forward guidance and as context for why the prior
    config was conservative.
    
    * fix(ci): drop pr-title from mergify merge_conditions
    
    Greptile flagged this as a P1 on PR #1668: `.github/workflows/pr-title.yml`
    skips semantic validation only for titles starting with `merge queue:`
    (the batch-mode prefix from #1306). In `draft_pr` mode with `batch_size: 1`,
    Mergify's draft PR title format is not guaranteed to match that prefix,
    so requiring `check-success = pr-title` on the draft could stall every
    queued PR.
    
    The source PR's title is already validated at `queue_conditions` entry
    and cannot change while queued, so re-checking it on the speculative
    draft is redundant. Drop it from `merge_conditions` and document the
    trap in the comment block so the next person editing this config sees
    why the omission is intentional.
    
    Also update the learning doc with this as a third non-obvious failure
    mode under "Why This Matters".
---
 .mergify.yml                                       |  50 +++++--
 ...max-parallel-checks-vs-batch-size-2026-05-19.md | 164 +++++++++++++++++++++
 2 files changed, 205 insertions(+), 9 deletions(-)

diff --git a/.mergify.yml b/.mergify.yml
index cd7bd3f8..3f827704 100644
--- a/.mergify.yml
+++ b/.mergify.yml
@@ -1,12 +1,22 @@
 queue_rules:
   - name: default
-    # Single condition block (no separate merge_conditions) so Mergify uses
-    # in-place mode instead of draft_pr mode. In-place mode rebases the
-    # source PR's branch directly; CI re-runs on the source PR where
-    # Greptile (and any other PR-only review apps) can actually post.
-    # The draft `mergify/merge-queue/*` mechanism creates bot-owned branches
-    # that Greptile skips, which was the root cause of every queued PR
-    # timing out waiting for Greptile.
+    # Two-block setup (queue_conditions + merge_conditions) intentionally
+    # flips Mergify into draft_pr mode so it can speculate on multiple
+    # PRs in parallel (see merge_queue.max_parallel_checks below).
+    #
+    # The split is what makes draft_pr mode safe for our setup:
+    #   - queue_conditions gate entry on the SOURCE PR -> Greptile and
+    #     review-thread resolution are enforced here, against the human-
+    #     reviewable PR where Greptile actually posts checks.
+    #   - merge_conditions gate the actual merge after the speculative
+    #     draft PR's CI passes -> only the CI checks that run on the
+    #     bot-owned `mergify/merge-queue/*` branch are required. Greptile
+    #     silent-skips bot branches by design (Mergify confirmed this is
+    #     expected), so requiring it here would block every queued PR.
+    #
+    # merge_protections (further down) re-enforces Greptile + review-
+    # threads against the source PR before the GitHub merge actually
+    # happens, so the human-review gate isn't bypassed by speculation.
     queue_conditions:
       - base = main
       # All Greptile/human inline review threads must be resolved before
@@ -30,6 +40,22 @@ queue_rules:
         - and:
           - head = release-please--branches--main
           - "title ~= ^chore\\(main\\): release"
+    # Evaluated against the speculative draft PR. Intentionally omitted:
+    #   - Greptile / review-threads — Greptile silent-skips bot branches,
+    #     and draft PRs have no review threads; both are re-enforced on
+    #     the source PR via queue_conditions + merge_protections.
+    #   - pr-title — `.github/workflows/pr-title.yml` skips only titles
+    #     starting with `merge queue:` (batch-mode prefix). In draft_pr
+    #     mode the draft title format is not guaranteed to match, which
+    #     would hang the check. Source PR title is already validated at
+    #     queue_conditions entry and cannot change while queued, so the
+    #     re-check on the draft is redundant.
+    merge_conditions:
+      - check-success = build-and-test
+      - check-success = generated-test
+      - check-success = go-lint
+      - check-success = golden
+      - check-success = test
     merge_method: squash
     # `update_method: merge` instead of `rebase` so the queue can update
     # fork PRs without `update_bot_account`. Mergify deprecated the
@@ -39,12 +65,18 @@ queue_rules:
     # in the queue, but `merge_method: squash` collapses it on the way
     # to main, so the on-main history is unchanged.
     update_method: merge
-    # OSS plan: batch_size and max_parallel_checks must both be 1.
+    # OSS plan: batch_size must stay at 1 (raising it triggers the paid
+    # "Merge Queue Batch" feature and dequeues PRs with a billing block).
+    # max_parallel_checks below is a separate knob and is NOT paywalled.
     batch_size: 1
     checks_timeout: 60m
 
 merge_queue:
-  max_parallel_checks: 1
+  # Parallel speculation across queued PRs. Mergify creates up to N
+  # `mergify/merge-queue/*` draft PRs concurrently and runs CI on each.
+  # Free on the OSS plan; the paid feature is `batch_size > 1`, which
+  # groups multiple PRs into one queue position.
+  max_parallel_checks: 3
   queued_label: queued
   dequeued_label: dequeued
   status_comments: outcomes
diff --git a/docs/solutions/tooling-decisions/mergify-max-parallel-checks-vs-batch-size-2026-05-19.md b/docs/solutions/tooling-decisions/mergify-max-parallel-checks-vs-batch-size-2026-05-19.md
new file mode 100644
index 00000000..2a8c7887
--- /dev/null
+++ b/docs/solutions/tooling-decisions/mergify-max-parallel-checks-vs-batch-size-2026-05-19.md
@@ -0,0 +1,164 @@
+---
+title: "Mergify max_parallel_checks is free; only batch_size is paywalled"
+date: 2026-05-19
+category: tooling-decisions
+module: ci/mergify
+problem_type: tooling_decision
+component: tooling
+severity: medium
+applies_when:
+  - "Tuning Mergify merge queue throughput on the OSS / free plan"
+  - "Greptile or other human-review tools silent-skip mergify/merge-queue/* bot branches and gate the queue"
+  - "Deciding whether to raise batch_size or max_parallel_checks to speed up a slow queue"
+related_components:
+  - development_workflow
+tags:
+  - mergify
+  - merge-queue
+  - max-parallel-checks
+  - batch-size
+  - greptile
+  - oss-plan
+  - queue-conditions
+  - merge-conditions
+---
+
+# Mergify max_parallel_checks is free; only batch_size is paywalled
+
+## Context
+
+`mvanhorn/cli-printing-press` runs on Mergify's free OSS plan. The merge queue had been pinned to the most conservative possible config — `batch_size: 1`, `max_parallel_checks: 1`, a single `queue_conditions` block — under the belief that both throughput knobs were paywalled features. They aren't. Only `batch_size > 1` is paid. `max_parallel_checks > 1` is free, and not using it leaves a real speedup on the floor.
+
+The wrong-turn history:
+
+- **PR #1306** raised `batch_size: 1 → 3` plus `speculative_checks: 2` (the older Mergify name for `max_parallel_checks`). The queue immediately dequeued PRs with `Cannot use Merge Queue batch. The 'Merge Queue Batch' feature requires a higher subscription tier`.
+- **PR #1321** reverted both knobs to 1 and moved Greptile into Mergify conditions to restore stable merging.
+- **PR #1336** narrowed the config to a single-block `queue_conditions` setup, locking the queue into in-place mode.
+- The institutional takeaway became "both knobs are paywalled on OSS." Filed in private memory, propagated into other repos.
+
+A second concern reinforced the same conservative config: Greptile silent-skips bot-owned branches (`mergify/merge-queue/*` draft PRs). With only `queue_conditions` (no `merge_conditions`), the team was implicitly running in-place mode and never observed the draft-PR + Greptile interaction. The fear was that adding draft PRs would silently bypass Greptile.
+
+Mergify support clarified on 2026-05-19:
+
+- `batch_size > 1` is the paid "Merge Queue Batch" feature, which groups multiple PRs into one queue position.
+- `max_parallel_checks > 1` is free. It lets Mergify check multiple PRs in parallel via individual `mergify/merge-queue/*` draft PRs — one PR per draft, no batching.
+- Greptile not posting on bot branches is expected. The human-review gate belongs on the **source** PR before queue entry, not on the speculative draft.
+
+PR #1668 codified the corrected configuration.
+
+## Guidance
+
+Treat `batch_size` and `max_parallel_checks` as two independent decisions:
+
+1. **`batch_size`**: keep at `1` on the OSS plan. Any value > 1 trips the paid-tier dequeue with `Cannot use Merge Queue batch`.
+2. **`max_parallel_checks`**: free to raise above 1. Pick a value that matches realistic queue depth; 3 is a reasonable starting point for a small-team repo. Mergify's default has been 5 since June 2025, so a missing setting silently inherits that default.
+
+Adding a `merge_conditions` block (distinct from `queue_conditions`) is what flips Mergify into **draft_pr** mode, which is what makes parallel speculation actually parallelize across PRs. With only `queue_conditions`, the queue runs in-place and `max_parallel_checks > 1` has nothing to parallelize.
+
+When you adopt draft_pr mode, structure gating across **three blocks**, each enforcing the right conditions against the right PR:
+
+- **`queue_conditions`** — gates entry on the *source* PR. Put human-review signals here: review-thread resolution, code-review tools (Greptile), and all CI checks.
+- **`merge_conditions`** — gates merge after the *draft* PR's CI passes. Put *only* the CI checks that run on bot branches here. Do **not** put Greptile or review-thread counts here — Greptile silent-skips bot branches, and draft PRs have no review threads.
+- **`merge_protections`** — re-enforces source-PR conditions before the final GitHub merge. Belt-and-suspenders: ensures speculation can't bypass the human-review gate.
+
+## Why This Matters
+
+The corrected config gives free throughput we were leaving on the floor. With `max_parallel_checks: 1`, queued PRs serialize behind each other's full CI run. With `max_parallel_checks: 3`, three PRs speculate in parallel via independent draft branches; the first one to land merges immediately, and the others either land in sequence (if still applicable to the new `main`) or re-queue with one less position ahead.
+
+Three non-obvious failure modes to avoid:
+
+- **Raising `max_parallel_checks` without adding `merge_conditions`** — nothing breaks, but nothing speeds up either. Mergify stays in in-place mode and parallel checks have no draft PRs to run against. The change appears to take effect in config review and quietly does nothing.
+- **Putting Greptile or `#review-threads-unresolved = 0` in `merge_conditions`** — the queue stalls forever waiting for signals that will never appear on bot branches. The dequeue message is unhelpful; the symptom looks like Mergify is broken.
+- **Putting `pr-title` (or any title-pattern-skipping workflow) in `merge_conditions`** — if the workflow's skip condition was written for a *batch-mode* draft title prefix (e.g. `startsWith(title, 'merge queue:')`), it won't match `draft_pr`-mode draft titles, and the semantic check may fail on every draft. The source PR's title is already validated at queue entry and cannot change while queued, so the re-check is redundant — omit it from `merge_conditions` rather than try to extend the skip pattern.
+
+## When to Apply
+
+Apply this guidance when **all** of the following hold:
+
+- The repo is on Mergify's free OSS plan (or any plan where batch is paywalled but parallel-check isn't — verify against current Mergify pricing).
+- Queue depth regularly exceeds 1 (i.e., PRs wait behind each other's CI).
+- CI runs are long enough that serialization is a real cost.
+- A code-review tool (Greptile, Coderabbit, similar) silent-skips bot-owned branches.
+
+Skip this if queue depth is reliably 0–1 PRs (no parallelism to extract) or if your code-review tool *does* run on bot branches (the `merge_conditions` split is less critical, though still cleaner).
+
+## Examples
+
+**Before** — in-place mode, max parallelism of 1:
+
+```yaml
+queue_rules:
+  - name: default
+    queue_conditions:
+      - base = main
+      - "#review-threads-unresolved = 0"
+      - check-success = build-and-test
+      - check-success = generated-test
+      - or:
+        - check-success = Greptile Review
+        - check-neutral = Greptile Review
+        - check-skipped = Greptile Review
+    batch_size: 1
+    merge_method: squash
+
+merge_queue:
+  max_parallel_checks: 1
+```
+
+**After** — draft_pr mode, parallel speculation, three-tier gating:
+
+```yaml
+queue_rules:
+  - name: default
+    # Source PR gating: human-review signals + all CI checks.
+    queue_conditions:
+      - base = main
+      - "#review-threads-unresolved = 0"
+      - check-success = build-and-test
+      - check-success = generated-test
+      - or:
+        - check-success = Greptile Review
+        - check-neutral = Greptile Review
+        - check-skipped = Greptile Review
+
+    # Draft PR gating: only CI checks that run on bot branches.
+    # No Greptile (silent-skips bots), no review-thread count (drafts have none).
+    merge_conditions:
+      - check-success = build-and-test
+      - check-success = generated-test
+
+    batch_size: 1            # paid above this; keep at 1
+    merge_method: squash
+    update_method: merge
+
+merge_queue:
+  max_parallel_checks: 3     # free; raised from 1
+
+# Re-enforce source-PR gates before the actual GitHub merge.
+merge_protections:
+  - name: require-ready-label-and-ci
+    success_conditions:
+      - or:
+        - label = ready-to-merge
+        # release-please carve-out elided
+      - "#review-threads-unresolved = 0"
+      - check-success = build-and-test
+      - check-success = generated-test
+      - or:
+        - check-success = Greptile Review
+        - check-neutral = Greptile Review
+        - check-skipped = Greptile Review
+```
+
+## Three rules to remember
+
+1. **`batch_size > 1` requires paid Mergify; `max_parallel_checks > 1` does not.** Different features, only the former is paywalled on OSS.
+2. **`merge_conditions` is the switch that turns on draft_pr mode.** Without it, `max_parallel_checks > 1` has nothing to parallelize.
+3. **Greptile silent-skips bot branches by design.** Put Greptile gates in `queue_conditions` and `merge_protections` (both evaluate the source PR), never in `merge_conditions` (evaluates the draft branch).
+
+## Related
+
+- PR #1306 — the regression: raised `batch_size` and `speculative_checks` together, hit the paid-tier dequeue.
+- PR #1321 — the over-correction: reverted both knobs, calcified the "both paywalled" belief.
+- PR #1336 — narrowed to single-block in-place mode.
+- PR #1668 — the fix: split gating into three blocks, raised `max_parallel_checks` to 3, kept `batch_size` at 1.

← 81f07e53 feat(skills): check public library for existing CLI before g  ·  back to Cli Printing Press  ·  fix(ci): allow queue drafts to skip Greptile 96bc822e →