Audit: Merge queue reasonability¶
What we check¶
Repositories that enable a GitHub merge queue on their default branch must configure it so entries are processed serially and merged individually:
max_entries_to_build: 1— no speculative stacking.min_entries_to_merge: 1— merge each green entry immediately.
Repositories without a merge queue are out of scope here; whether to adopt two-stage CI at all is a per-project decision, except for a repository that releases, which release-merge-queue requires to have one.
The check reads the effective rules for the default branch via
GET /repos/shakenfist/<repo>/rules/branches/<branch> and validates
the parameters of any merge_queue rule found.
Why these values¶
Both were learned the hard way on shakenfist/shakenfist in August 2026.
Speculative stacking multiplies failures. With
max_entries_to_build above 1, entry N+1 builds on a temporary branch
containing entry N's changes, so when any entry ahead fails the entries
behind it are ejected and rebuilt on new SHAs. On a CI cluster whose
dominant failure mode is load this is doubly wrong: the stacked builds
waste runs (we observed single PRs rebuilt five times in one day) and
the extra concurrent merge groups add the very load that causes the
failures. A serialized queue never starts CI for an entry until the one
ahead has merged or ejected.
Batched merging is pure latency. The queue always builds one merge
group and runs CI once per entry no matter how merges are batched —
min_entries_to_merge only controls how many green entries land in a
single default-branch update. Raising it makes the queue idle for up to
min_entries_to_merge_wait_minutes hoping more PRs arrive, which on a
mostly-single-developer project delays every merge and saves nothing.
At 1 that wait timer never engages.
Other parameters (grouping_strategy, merge_method,
max_entries_to_merge, check_response_timeout_minutes) are
conventions rather than correctness issues and are not enforced; the
fleet reference is shakenfist/shakenfist's "Develop branch" ruleset:
ALLGREEN, MERGE, 5, and 360 respectively.
Template¶
No template — this is a one-time ruleset configuration change.
To inspect the current configuration:
gh api repos/shakenfist/<repo>/rules/branches/develop \
--jq '.[] | select(.type == "merge_queue") | .parameters'
To fix a non-compliant ruleset, fetch it, rewrite the two parameters,
and PUT it back, preserving everything else including bypass_actors:
gh api repos/shakenfist/<repo>/rulesets --jq \
'.[] | select(.target == "branch") | {id, name}'
# The bypass_actors guard is inside the transform on purpose. GitHub
# omits that field for a credential it does not show it to, and the
# earlier form of this recipe defaulted it with // [], which turned
# "cannot see the list" into "the list is empty" and removed every
# bypass actor on the PUT. A separate check before the read would be
# easy to paste past; error() cannot be, it names the problem on
# stderr, and jq exits non-zero so the PUT has nothing valid to send.
gh api repos/shakenfist/<repo>/rulesets/<id> | jq \
'{name, target, enforcement, conditions,
bypass_actors: (if has("bypass_actors") then .bypass_actors
else error("bypass_actors not visible to this credential: "
+ "use one that can edit the ruleset") end),
rules: (.rules | map(
if .type == "merge_queue"
then .parameters.max_entries_to_build = 1
| .parameters.min_entries_to_merge = 1
else . end))}' > /tmp/ruleset.json
gh api -X PUT repos/shakenfist/<repo>/rulesets/<id> \
--input /tmp/ruleset.json
Afterwards, trigger the repository's export-repo-config workflow so
the change is captured in .github/exported-config/.
Projects¶
Per-project compliance for this criterion is regenerated on every run of the consistency audit: see the compliance page.