<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Graphql on Prskavec.Net</title><link>https://www.prskavec.net/tags/graphql/</link><description>Personal site of Ladislav Prskavec — Software Engineer &amp; Site Reliability Engineer in Prague. Writing, conference talks, an OnCall design guide, and the You Build It You Run It podcast.</description><generator>Hugo -- gohugo.io</generator><language>en</language><managingEditor>ladislav@prskavec.net (Prskavec.Net)</managingEditor><webMaster>ladislav@prskavec.net (Prskavec.Net)</webMaster><copyright>&amp;copy; {year} Ladislav Prskavec</copyright><lastBuildDate>Wed, 26 Aug 2026 09:00:00 +0200</lastBuildDate><atom:link href="https://www.prskavec.net/tags/graphql/index.xml" rel="self" type="application/rss+xml"/><item><title>Merge queues, and the API GitHub only exposes over GraphQL</title><link>https://www.prskavec.net/post/merge-queues-and-the-graphql-only-api/</link><pubDate>Wed, 26 Aug 2026 09:00:00 +0200</pubDate><author>ladislav@prskavec.net (Prskavec.Net)</author><guid>https://www.prskavec.net/post/merge-queues-and-the-graphql-only-api/</guid><description>What a merge queue actually does, how GitHub's version is configured, where the richer tools go further, and why writing a terminal dashboard for it meant giving up on REST entirely.</description><content:encoded>&lt;p&gt;Every team that merges more than a handful of pull requests a day eventually hits the same failure: two pull requests are both green, both get merged, and the main branch goes red. Neither author did anything wrong. One renamed a function, the other added a caller for it, and nothing ever tested the two changes together.&lt;/p&gt;
&lt;p&gt;That is a semantic conflict, and git will not catch it. Git merges text. It has no opinion about whether the result compiles.&lt;/p&gt;
&lt;h2 id="what-a-merge-queue-does"&gt;What a merge queue does&lt;/h2&gt;
&lt;p&gt;A merge queue closes the gap by refusing to trust a check run against a stale base. Instead of merging your branch as it was tested, it builds a &lt;em&gt;merge group&lt;/em&gt;: your changes on top of the current main branch, plus everything already queued ahead of you. Then it runs CI against that. Only if the group passes does anything land.&lt;/p&gt;
&lt;p&gt;The queue is the serialisation point. It means the tested state and the merged state are the same state, which is the whole point. It also means merging stops being instant: you join a line, and how long you wait depends on how many people are ahead of you and how slow CI is.&lt;/p&gt;
&lt;h2 id="how-githubs-version-works"&gt;How GitHub&amp;rsquo;s version works&lt;/h2&gt;
&lt;p&gt;GitHub&amp;rsquo;s merge queue went generally available in 2023 and is configured through branch protection rules, or now rulesets, on the target branch. Once it is on, the merge button changes: you no longer merge, you enqueue.&lt;/p&gt;
&lt;p&gt;Your CI has to opt in, because merge groups are not pull requests. GitHub dispatches a &lt;code&gt;merge_group&lt;/code&gt; event, and a workflow that only triggers on &lt;code&gt;pull_request&lt;/code&gt; will never run against the group at all. The queue then sits there waiting for a check that is never coming, which is the first thing most people get wrong.&lt;/p&gt;
&lt;p&gt;The settings worth knowing about, all from the &lt;a href="https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue"&gt;merge queue docs&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Merge method&lt;/strong&gt;: merge, rebase or squash.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Minimum and maximum pull requests to merge&lt;/strong&gt;, between 1 and 100, plus a timeout for how long to wait for the minimum before giving up and merging a smaller group. This is batching: test five pull requests as one group and you run CI once instead of five times.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build concurrency&lt;/strong&gt;: the maximum number of &lt;code&gt;merge_group&lt;/code&gt; webhooks in flight, between 1 and 100. Turn this down when the queue starts saturating your runners.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Status check timeout&lt;/strong&gt;: how long to wait for CI before treating silence as failure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Only merge non-failing pull requests&lt;/strong&gt;: whether every pull request in the group has to pass on its own, or whether the group can go in if the final one passes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Entries leave the queue when checks fail, when the timeout expires, when someone removes them by hand, or when a branch protection conflict appears that GitHub cannot resolve.&lt;/p&gt;
&lt;h2 id="where-the-other-tools-go-further"&gt;Where the other tools go further&lt;/h2&gt;
&lt;p&gt;GitHub&amp;rsquo;s queue is fine, and it is free and already there. The dedicated tools are mostly better at one thing: what happens when a batch fails.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://docs.mergify.com/merge-queue/batches/"&gt;Mergify&lt;/a&gt; bisects. A failed batch is split into smaller batches and re-tested until the offending pull request is isolated on its own, and only that one is removed. Everything else stays in line. Batches are also grouped by similarity rather than strict arrival order, first by scope, then by changed directory, then by queue time, with priority overriding all of it and stacked pull requests kept together. There are partitioned queues too, so a docs change is not stuck behind a database migration.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://graphite.com/guides/merge-queue-tools-options"&gt;Graphite&lt;/a&gt; comes at it from the stacked-diff side: it understands that a stack of dependent pull requests is one unit, runs CI for the whole stack in parallel, and lands it as a single fast-forward.&lt;/p&gt;
&lt;p&gt;Both are paid services, and worth pricing out only once you can point at the hours the native queue is costing you. If your batches rarely fail, GitHub&amp;rsquo;s is enough and the rest is a subscription for a problem you do not have.&lt;/p&gt;
&lt;h2 id="the-problem-i-actually-had"&gt;The problem I actually had&lt;/h2&gt;
&lt;p&gt;None of the above is visible from a terminal. The specific thing that kept happening: a pull request goes into the queue, sits there for the better part of an hour, reaches the front, and only then gets thrown out as unmergeable. An hour of waiting to learn something the API knew the whole time.&lt;/p&gt;
&lt;p&gt;Worse, the obvious response is frequently the wrong one. &amp;ldquo;Unmergeable&amp;rdquo; has two entirely different causes and GitHub renders them identically:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the branch genuinely conflicts with the base branch, and a rebase fixes it;&lt;/li&gt;
&lt;li&gt;the branch is clean against the base, and the conflict is with something ahead of it &lt;em&gt;inside the merge group&lt;/em&gt;. Rebasing accomplishes nothing here. The only fix is waiting for the entries in front to land.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So I wrote &lt;a href="https://github.com/abtris/mqw"&gt;mqw&lt;/a&gt;, a small Go terminal dashboard: the queue for a base branch in one pane, my open pull requests and the reason each can or cannot go in the other, enter to enqueue, &lt;code&gt;d&lt;/code&gt; to dequeue.&lt;/p&gt;
&lt;h2 id="why-it-all-goes-through-graphql"&gt;Why it all goes through GraphQL&lt;/h2&gt;
&lt;p&gt;Then I went looking for the REST endpoint that lists a merge queue, and there isn&amp;rsquo;t one. REST will happily tell you about pull requests, checks, branches and rulesets. Merge queue entries are not there. The &lt;code&gt;MergeQueue&lt;/code&gt; and &lt;code&gt;MergeQueueEntry&lt;/code&gt; types exist only in the GraphQL schema, so every call in mqw goes through &lt;code&gt;gh api graphql&lt;/code&gt;. That is not a design preference, it is the only door.&lt;/p&gt;
&lt;p&gt;The query for the queue itself:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-graphql" data-lang="graphql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;query&lt;/span&gt;($owner:&lt;span style="color:#a6e22e"&gt;String&lt;/span&gt;!,$name:&lt;span style="color:#a6e22e"&gt;String&lt;/span&gt;!,$branch:&lt;span style="color:#a6e22e"&gt;String&lt;/span&gt;!){
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; repository(owner:$owner,&lt;span style="color:#a6e22e"&gt;name&lt;/span&gt;:$name){
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; mergeQueue(branch:$branch){
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; entries(first:&lt;span style="color:#a6e22e"&gt;20&lt;/span&gt;){
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; nodes{
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; position
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; state
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; enqueuedAt
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; estimatedTimeToMerge
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; enqueuer{ login }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; pullRequest{ &lt;span style="color:#66d9ef"&gt;...&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;position&lt;/code&gt; and &lt;code&gt;estimatedTimeToMerge&lt;/code&gt; are the two fields that make the tool worth running. They are how you find out you have forty minutes to wait before you go and make coffee. &lt;code&gt;enqueuer&lt;/code&gt; is separate from the pull request author on purpose: somebody else can put your pull request in the queue.&lt;/p&gt;
&lt;p&gt;Having been forced into GraphQL, the thing I did not expect is that it turns out better for this problem than REST would have been. A pull request has to be described identically whether it arrives through the queue or through a search, so the selection set is a Go constant that both queries interpolate:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;const&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;prFields&lt;/span&gt; = &lt;span style="color:#e6db74"&gt;`
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; id
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; number
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; title
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; state
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; isDraft
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; merged
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; mergeable
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; reviewDecision
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; headRefName
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; baseRefName
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; author{ login __typename }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; mergeQueueEntry{ state position estimatedTimeToMerge }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; files(first:100){ totalCount nodes{ path } }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; labels(first:20){ nodes{ name } }
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#e6db74"&gt; commits(last:1){ nodes{ commit{ statusCheckRollup{ state } } } }`&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Over REST, filling that struct means a request for the pull request, another for its files, another for the check rollup, another for labels, and the merge queue state is unavailable at any price. Here it is one round trip, and I picked the fields.&lt;/p&gt;
&lt;h2 id="the-one-query-that-pays-for-itself"&gt;The one query that pays for itself&lt;/h2&gt;
&lt;p&gt;The payoff is the ambiguity from earlier. Telling the two kinds of &amp;ldquo;unmergeable&amp;rdquo; apart needs two facts at once: the queue entry&amp;rsquo;s &lt;code&gt;state&lt;/code&gt;, and the pull request&amp;rsquo;s own &lt;code&gt;mergeable&lt;/code&gt; against the base branch. Both are in the response above, so the classification is a switch with no extra call behind it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;e&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;State&lt;/span&gt; &lt;span style="color:#f92672"&gt;==&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;UNMERGEABLE&amp;#34;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;switch&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;p&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Mergeable&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;case&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;CONFLICTING&amp;#34;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// genuinely conflicts with the base&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;{&lt;span style="color:#a6e22e"&gt;headline&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;unmergeable: conflicts with &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;p&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;BaseRefName&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;detail&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;dequeue, rebase &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;p&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;HeadRefName&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;, re-queue&amp;#34;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;case&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;MERGEABLE&amp;#34;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// clean against the base: the merge group is the problem&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;{&lt;span style="color:#a6e22e"&gt;headline&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;unmergeable inside the merge group&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;detail&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;clean against &amp;#34;&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;p&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;BaseRefName&lt;/span&gt; &lt;span style="color:#f92672"&gt;+&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;, so rebasing will not help&amp;#34;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;default&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// UNKNOWN: GitHub has not computed it yet. Guessing here would be worse&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#75715e"&gt;// than admitting it.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;status&lt;/span&gt;{&lt;span style="color:#a6e22e"&gt;headline&lt;/span&gt;: &lt;span style="color:#e6db74"&gt;&amp;#34;unmergeable, cause not yet known&amp;#34;&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That third branch matters more than it looks. &lt;code&gt;mergeable&lt;/code&gt; is &lt;code&gt;UNKNOWN&lt;/code&gt; until GitHub finishes computing the merge, which takes a moment after any push. Collapsing &lt;code&gt;UNKNOWN&lt;/code&gt; into either of the other two produces confident advice that is wrong about half the time, and confident wrong advice is worse than a tool that says it does not know yet.&lt;/p&gt;
&lt;h2 id="mutations-and-one-sharp-edge"&gt;Mutations, and one sharp edge&lt;/h2&gt;
&lt;p&gt;Enqueueing and dequeueing are mutations, and they are not symmetric:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-graphql" data-lang="graphql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;mutation&lt;/span&gt;($id:&lt;span style="color:#a6e22e"&gt;ID&lt;/span&gt;!){ enqueuePullRequest(&lt;span style="color:#66d9ef"&gt;input&lt;/span&gt;:{&lt;span style="color:#a6e22e"&gt;pullRequestId&lt;/span&gt;:$id}){ mergeQueueEntry{ position state } } }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;mutation&lt;/span&gt;($id:&lt;span style="color:#a6e22e"&gt;ID&lt;/span&gt;!){ dequeuePullRequest(&lt;span style="color:#66d9ef"&gt;input&lt;/span&gt;:{&lt;span style="color:#a6e22e"&gt;id&lt;/span&gt;:$id}){ mergeQueueEntry{ id } } }
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;enqueuePullRequest&lt;/code&gt; takes &lt;code&gt;pullRequestId&lt;/code&gt;. &lt;code&gt;dequeuePullRequest&lt;/code&gt; takes &lt;code&gt;id&lt;/code&gt;, and it is still the pull request&amp;rsquo;s ID, not the entry&amp;rsquo;s. Passing the wrong node ID gets you a type error from the API rather than anything explanatory, which cost me a while.&lt;/p&gt;
&lt;h2 id="delegating-auth-to-gh"&gt;Delegating auth to gh&lt;/h2&gt;
&lt;p&gt;Every request shells out to &lt;code&gt;gh api graphql&lt;/code&gt; rather than opening an HTTP client. No token storage, no refresh, no device flow, no &lt;code&gt;Authorization&lt;/code&gt; header to get wrong. &lt;code&gt;gh&lt;/code&gt; is already installed and already authenticated, and the whole transport is thirty lines.&lt;/p&gt;
&lt;p&gt;The catch is that &lt;code&gt;gh&lt;/code&gt; has global mutable state. A &lt;code&gt;gh auth switch&lt;/code&gt; in another terminal silently reaches into a running dashboard, and the symptom is unhelpful: the pull request pane goes empty, because a repository you cannot see returns an &lt;em&gt;empty search result&lt;/em&gt; rather than an error, while only the queue query reports &lt;code&gt;NOT_FOUND&lt;/code&gt;. Half the screen quietly lies to you.&lt;/p&gt;
&lt;p&gt;The fix is to resolve one account&amp;rsquo;s token once at startup and pass it to every child process, since &lt;code&gt;gh&lt;/code&gt; prefers &lt;code&gt;GH_TOKEN&lt;/code&gt; over whichever account happens to be active:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-go" data-lang="go"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#66d9ef"&gt;func&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ghCommand&lt;/span&gt;(&lt;span style="color:#a6e22e"&gt;args&lt;/span&gt; &lt;span style="color:#f92672"&gt;...&lt;/span&gt;&lt;span style="color:#66d9ef"&gt;string&lt;/span&gt;) &lt;span style="color:#f92672"&gt;*&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;exec&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Cmd&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;cmd&lt;/span&gt; &lt;span style="color:#f92672"&gt;:=&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;execCommand&lt;/span&gt;(&lt;span style="color:#e6db74"&gt;&amp;#34;gh&amp;#34;&lt;/span&gt;, &lt;span style="color:#a6e22e"&gt;args&lt;/span&gt;&lt;span style="color:#f92672"&gt;...&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;if&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;ghToken&lt;/span&gt; &lt;span style="color:#f92672"&gt;!=&lt;/span&gt; &lt;span style="color:#e6db74"&gt;&amp;#34;&amp;#34;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#a6e22e"&gt;cmd&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Env&lt;/span&gt; = append(&lt;span style="color:#a6e22e"&gt;cmd&lt;/span&gt;.&lt;span style="color:#a6e22e"&gt;Environ&lt;/span&gt;(), &lt;span style="color:#e6db74"&gt;&amp;#34;GH_TOKEN=&amp;#34;&lt;/span&gt;&lt;span style="color:#f92672"&gt;+&lt;/span&gt;&lt;span style="color:#a6e22e"&gt;ghToken&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#66d9ef"&gt;return&lt;/span&gt; &lt;span style="color:#a6e22e"&gt;cmd&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Pinning the account per checkout via a &lt;code&gt;.mqw.toml&lt;/code&gt; is what makes a work account and a personal account coexist without either one breaking the other.&lt;/p&gt;
&lt;h2 id="being-honest-about-the-edges"&gt;Being honest about the edges&lt;/h2&gt;
&lt;p&gt;Two limits are worth surfacing rather than hiding, because a dashboard that quietly rounds off is worse than no dashboard.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;files(first:100)&lt;/code&gt; caps the file list. mqw uses it to warn that a candidate pull request shares files with something already queued, which is a &lt;em&gt;hint&lt;/em&gt; and nothing more: touching the same file is not a conflict, and touching none does not rule one out, because merge groups also fail on checks. So the code tracks &lt;code&gt;totalCount&lt;/code&gt; against the number of nodes returned and knows when an empty overlap result is untrustworthy.&lt;/p&gt;
&lt;p&gt;The pull request search is paginated and capped at four pages. When the cap is hit, the UI says so instead of pretending that is the whole repository.&lt;/p&gt;
&lt;h2 id="try-it"&gt;Try it&lt;/h2&gt;
&lt;pre tabindex="0"&gt;&lt;code&gt;brew install abtris/tap/mqw
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Source and README at &lt;a href="https://github.com/abtris/mqw"&gt;github.com/abtris/mqw&lt;/a&gt;. It needs &lt;code&gt;gh&lt;/code&gt; installed and logged in, and a base branch with a merge queue configured.&lt;/p&gt;
&lt;p&gt;I went in annoyed that REST was not an option, and came out thinking GitHub put the merge queue in the graph API because that is the shape the thing has. An entry means nothing on its own. It means something next to the pull request it points at, the commit that points at, and the checks hanging off that, and a dashboard needs all four at the same instant or it will tell you to rebase when rebasing cannot help. Fetching that over REST would have been the bulk of the program. The entire query layer, both queries, both mutations and every type they decode into, is 400 lines.&lt;/p&gt;</content:encoded></item></channel></rss>