Audience: people who build or review codeboost's runner and merge code. Purpose: say exactly what happens between a click on Merge PR and GitHub merging the task's pull request, and what stops it.
With a runner, Merge PR no longer merges at once. It starts one merge job and returns. The job:
- prepares the merge (refresh, rebase, review check, push,
cmd:checks), asprepare-mergedoes; - waits for GitHub's required checks on the pushed head, for up to 30 minutes;
- merges through the existing guarded merge, which reruns the already-fixed check and pins the exact head.
Any change that needs a person (a stale approval, a moved pull request, a failing check) stops the job and says why. A task cancelled while the job runs ends it cancelled, however the job learns of it, including during one of the job's own review loads. A failing or slow required check, or a refusal from GitHub, moves the task to approved, merge blocked; Retry merge starts the whole job again. The browser follows the job by polling.
Without a runner, Merge PR works as before.
| Term | Meaning |
|---|---|
| Merge job | The background work one click starts. One per task at a time. |
| Preparation | PreMergeCoordinator.start: refresh, rebase, review check, push of the rewritten head, cmd: checks. |
| Readiness | The record preparation leaves when it succeeds. It binds the task state version, review version, snapshot, base, head and command policy. The guarded merge refuses without it. |
| Required checks | The union of the checks GitHub's rulesets and classic branch protection require. |
| Guarded merge | MergeCoordinator.merge: two or more fresh inspections, then gh pr merge --match-head-commit. |
| Already-fixed check | The check that the issue is not fixed elsewhere: closed, another PR closes it, or a base-branch commit mentions it. |
| Step | What happens | It continues when | It stops when |
|---|---|---|---|
| Admission | The click's review token must be current, the task mergeable, the issue trusted (read from GitHub, as for prepare-merge), and no merge job, runner attempt, publish or preparation active. The review load that checks the token awaits a history worker (#170), so it comes first: a saved click replays before it, and after it a resend of the same click replays its job, shutdown refuses (503, nothing saved), and every other check and the job's record run in one turn. A load stopped by shutdown answers 503; a load that fails for another reason saves nothing and the click may be sent again. If a resend of the same click was admitted while this load or the click's trust read was pending, a failure of either answers that resend's job. Once the job is saved, a failure of the click's own review reload answers the job with mergeRefreshRequired: the page keeps its review, follows the job, and a refresh shows the reloaded review. A reload stopped by shutdown answers 503, and the resent click returns the job. The job's first step reads the task and review it binds the preparation to in one turn, after its own review load, and ends at once if the task was cancelled meanwhile. If the review changed between admission and the end of that load (another tab approved an item, accepted a segment, or imported a plan), its token no longer matches the click's: the job ends failed with "The review changed after Merge PR was clicked. Refresh to see the change.", saves no prepare-merge action and prepares nothing, as the direct merge refuses a review that changed during its validation. The job record is saved under the click's action ID and the click returns the review with the job's latest record (usually running, step preparing; it may already have moved on or ended). |
always | the token is stale, the issue is not trusted, or something is busy. The refusal is saved under the click's action ID, so a resend replays it; a refusal because the server is shutting down (HTTP 503, also when shutdown stops the click's trust read), or because trust could not be read, saves nothing and may be sent again. A trust read that timed out, or that gh answered, keeps that answer even when shutdown started while gh stopped |
| Prepare | Only for the review the click was admitted for (see Admission). The job records its own prepare-merge action and runs preparation, which binds its readiness to the review version it read. |
preparation is ready |
preparation needs review (job: review-required) or fails (job: failed) |
| Wait for checks | The job inspects the pull request (fresh) at once, then again after 10 s, with the wait doubling up to 60 s. | every required check succeeds, or the required set was read and is empty, and nothing else blocks | a required check fails, or 30 minutes pass (job: blocked, if the preparation still applies: the block re-checks readiness in its transaction, and a review edit or other task change meanwhile ends the job failed instead); the pull request moved, another blocker appears, or GitHub or the local review cannot be read (job: failed; one failed read fails closed rather than retrying; each inspection also loads the review, which awaits a history worker since #170, within the same 30-minute limit) |
| Merge | The job calls the guarded merge with the current review token, not the click's: a rebase during preparation makes a new snapshot and so a new token. A review change after preparation is refused through readiness, which binds the review version. Its inspections rerun the already-fixed check. | GitHub merged or queued the head, as the job's own merge attempt records it when the job reads it after the guarded merge (job: merged or queued; if a merge-queue poll ended the attempt removed or failed while GitHub was asked, the guarded merge cannot save it queued and rejects, and the job ends as for a refusal; submitted when GitHub accepted the merge but saving its outcome failed, so the attempt is still submitting and reconciles from GitHub) |
GitHub refused (job: blocked; failed when GitHub refused because the head changed, which needs a fresh review, or when the review, snapshot or plan revision changed while GitHub was asked: the block is for the review the merge attempt was admitted for. A review edit (a review note) may land while the attempt is in flight. The snapshot term is defensive for a merge job: jobs need a runner, and a runner server's review load reads the recorded head, so no HEAD observation records a new snapshot. A plan revision lands only after a merge-queue poll ended the attempt while GitHub was asked, as plan writes are refused while a merge attempt is in flight), validation refused (job: failed), or the outcome is unknown (job: submitted; the merge attempt reconciles as before) |
A required check run passes when it completed with success, neutral or skipped, as GitHub counts required checks; a commit status passes only with success. When a check has several runs or several statuses with its name (a re-run), the one that started last counts. The same rule applies to commit statuses. A run or status not yet completed (queued, in progress or pending) with no start time counts as the latest. A completed run or status with no start time started before its completion time, so it is known to be older than, and is dropped for, any run or status of its kind that started after that time (or that is not yet completed and has no start time). Otherwise, including when it has no completion time either, its order relative to the latest is unknown, so it is never ordered below another: it ties with the latest. Among runs that tie, the one that passes least counts, so a run or status with no time can make the check fail or wait, but never makes it pass (fail closed). A check run and a commit status with the same name are not re-runs of each other: GitHub requires both to pass, so the latest of each counts, and the check is failing if either fails, otherwise pending if either is pending. A check that names an app matches only that app's check runs. gh's rollup does not say which app reported a run (#176), so for such a check the inspection asks GitHub for that app's runs of that name on the head (guarded-merge.md). No commit status matches it; GitHub may count a status the app set, so such a check waits or reads missing (fails closed). If that read fails, the inspection fails, and the job ends failed with the reason: gh's error, or codeboost's own when GitHub's answer is malformed, too large to read whole, or changed between pages. An app with so many suites on the head that their pages cannot be read in time ends the inspection with its timeout instead. While waiting, an unknown mergeability (GitHub is still computing it) and a pending or missing required check keep the job waiting. No inspection starts after the 30 minutes, and one still running when they end is stopped, so only checks seen passing within the limit count. An inspection that finds every check passing but finishes after the limit, before its stop arrives, also ends the job blocked, with "Required checks passed only after the 30-minute limit. Retry merge." and no waiting checks listed; one that finds a failing check or another blocker decides as it does within the limit. Only that stop counts as the wait timing out: an inspection that GitHub failed before the limit, but that settles after it (its stopped reads finishing), ends the job failed with GitHub's error. The guarded merge that follows inspects again and may ask GitHub to merge shortly after the limit; it refuses anything no longer clear. Unreadable branch rules never pass. Another blocker outranks a failing check: checks that fail on a head a collaborator pushed say nothing about this review, so the job ends failed and the task's status is unchanged.
A push to the base branch during the wait is such a blocker: the next inspection sees the moved base, and the job ends failed with "The base branch moved; merging rebases onto it and prepares again, so the result may need review." The task stays in review. The person clicks Merge PR again: the new job rebases onto the new base and prepares again. If the rebase makes an approval stale, that job ends review-required.
With merge jobs, the merge status words the blockers the job resolves as what merging does, because the screen offers no Rebase or Prepare control: a moved base reads as above, and preparation reads "The merge has not been prepared for the current review; merging prepares it." A push of the rewritten head whose outcome is not known yet is a preparation blocker too, and keeps its own detail: "A push of the rewritten head has not settled; merging reads its outcome and prepares again." The reason a job ends failed with uses the same wording when the job's own inspection or the guarded merge's first pass finds the blocker (for example, the base moved after the job's last inspection): the guarded merge's refusal carries the blocker, and the job words it. At the guarded merge's later passes this wording is defensive: during a job nothing changes the task's state version, and a moved base fails the pull request identity check first. Without a runner, and for a direct merge, the guarded merge's own wording is unchanged.
| Job state | Task status afterwards | What the person does |
|---|---|---|
merged |
merged | nothing |
queued |
unchanged (in review, or approved, merge blocked on a retry) while GitHub's queue holds it | nothing |
submitted |
unchanged | refresh; the merge attempt settles from GitHub, and while GitHub's queue holds it the page follows it, as for queued |
blocked |
approved, merge blocked | what the reason's last sentence says. After the 30-minute wait ran out, nothing failed: "Wait for the checks to pass, then Retry merge." (or wait for GitHub to determine mergeability). After a failing check or GitHub's refusal: "Fix it, then Retry merge." ("Fix them" for several failing checks). After GitHub refused a merge, a retry whose preparation rebases makes a new snapshot, and that needs every item approved again (the replacement-snapshot rule of merge-queue.md): the items show stale, the retry ends review-required, and the merge status asks for every item to be approved again. To change the task's code, use the scope-amendment flow, as from review: it accepts a blocked task. |
review-required |
unchanged | review the refreshed pull request, then merge again |
failed |
unchanged | do what the reason asks first, if anything (for example, review the new head), then merge again. The reason never says "merge again": the page adds "Merge again when ready." once |
cancelled |
cancelled | nothing |
Moving the task to approved, merge blocked advances its state version, so the next job prepares again from the start.
A rebase moves the pull request's base. The guarded merge would then scan only the base commits after the new base and miss the ones the rebase brought in. The scan must cover every base commit after the task's original base (the base of its first snapshot), since a rebase can bring any of them under the pull request. Each opening records the pre-PR check it followed (schema v19). That check of the task's first opening into the current base branch scanned the branch from its snapshot's base to the branch's tip then, and found nothing. When it scanned from the task's original base, each merge inspection scans from that tip to the branch's tip now. A later rebase onto the branch can bring in any commit after the original base: those up to that tip were scanned by that check, and later ones are in the merge's scan, so the scan covers them whichever of the task's openings into that branch the merge follows (after a re-publish recorded a newer check from a rebased base, and after a person closed the pull request and a new one was opened from a rebased base). A newer check is not a safe start: it scanned only from a rebased base, and a compare range excludes its own start commit. An opening into another base branch, left by a change of the github.baseBranch setting, says nothing about this branch and is not used. A first opening recorded before schema v19 has no check, and no record shows which base branch an older check scanned (a publish can record its check and stop before opening), so no other check stands in. Then, and when the first opening's check scanned from a rebased base (the base branch setting changed after a rebase), the scan starts at the task's original base. If that start is not on the base branch (for example, it was force-pushed), the answer is unknown and merging stays blocked. The check reads at most 250 base commits: if more landed since the scan's start, its answer is unknown and merging stays blocked, as for a pull request never rebased. There is no person override yet (the Continue action of pull-request-opening.md is a later slice). The task's own commits, the ledger's owned entries, are excluded, as in the pre-PR check. The status shown while a merge attempt is submitting, queued or merged scans from the pull request's base, as before: those inspections follow the attempt's pinned pull request, and no merge is asked for while an attempt is active.
While a job runs, a step list under the review strip shows the design's seven steps (D16), in order: Fetch → Rebase → Push → Re-run checks → GitHub checks → Already-fixed check → Merge. Each step shows an icon, a word and its elapsed time (from the step's saved times to the browser's clock, so a clock difference between browser and server can skew a running step's time; a step ended by startup settlement after a restart counts the time the server was down): ○ Waiting, ● Running, ✓ Done, – Skipped, ✕ Failed, ! Stopped or ! Unknown.
| Step | Running while | Skipped when |
|---|---|---|
| Fetch | preparation reads and fetches the pull request, until the head is known not to have moved | never |
| Rebase | preparation rebases onto the moved base and reloads the review | the base has not moved |
| Push | preparation pushes the rewritten head and waits for GitHub to show it | the head did not change |
| Re-run checks | preparation runs the plan items' cmd: checks on the exact head |
no plan item has a cmd: check |
| GitHub checks | the job waits for the required checks | never |
| Already-fixed check | the guarded merge validates, rerunning the already-fixed check in each pass | never |
| Merge | GitHub has been asked to merge, until it answers | never |
The job saves each change to the list in its merge_jobs record, so the list survives a reload and a second tab sees it. Starting a step finishes any step still running. Steps are display only: the job never waits for a step write, and one that fails is logged ("Could not record merge step …") without stopping the job. When the job ends, the step that was running takes the outcome's word: Done after merged or queued, Unknown after submitted, Stopped after cancelled, and Failed otherwise. Steps the job never reached stay Waiting. The list says where the job stopped, not why: the banner gives the reason. So:
- The step that was running is Failed even when its own work succeeded and a check during it stopped the job. For example, Fetch fails when the fetch shows the pull request head moved, Push fails when the pull request moves after the push, and Already-fixed check fails for any refusal in the guarded merge's validation, such as a pull request that changed.
- When no step was running, the list stays as it was. For example: preparation stops for review after the rebase (a stale approval); its final inspection or authorization, after Re-run checks, refuses; it refuses before Fetch starts (an earlier push was settled, or cleanup is unresolved); the review changed between the click and the job's first review load; or the merge was cancelled during that load. Then every step may still read Waiting.
The list stays on screen after the job ends until the review reloads (Refresh, or any review action, plan import or Apply), also while the page follows the merge queue a job ended queued in. Cancel merge shows only while the job runs, not "always" as the design says: an ended job has nothing to cancel. When Cancel, Merge PR or the blockers button (Review blockers or Merge status) hides (the job ends, the bar hides, a reload clears the blockers or makes merging unavailable, or a load fails), keyboard focus on it moves to Merge PR, or to Refresh when Merge PR is hidden or disabled. The page reads the focused control before it changes any control and moves focus once every control's final state is set. A job saved before the list existed has no steps; the page then shows its step as before, without the list or Cancel.
Cancel merge stops the job without cancelling the task. The page sends {action: "cancel-merge", jobId} to POST /api/action:
- The job is marked
cancelRequestedfirst, then told to stop, and so is its preparation. Preparation stops as for a task cancel: a rebase is aborted and cleaned up, and acmd:check is stopped as cancelled. - The job ends
cancelled, with "You cancelled the merge; the task and its pull request stay open." The task keeps its status (in review, or approved, merge blocked), its pull request stays open, and Merge PR may be clicked again. A task cancelled before the job's work stops ends the job as a cancelled task instead. - Once the job's merge attempt exists, GitHub has been asked: stopping then could only make the outcome unknown, so the server refuses ("GitHub has been asked to merge; wait for its outcome.") and the page marks the button unavailable (
aria-disabled, so it keeps keyboard focus) once the Merge step has started, and says why beside it. If that step's write failed (steps are display only), the button stays available, and a click shows the refusal in the banner. - If the cancel is answered with a job that already ended (it merged before the page saw it, or another tab cancelled it), the page polls the merge status at once (or, when a poll is already on its way, 2 s after it), and the poll shows the outcome, as when it sees a job end. An answer with a newer running job another tab started is followed at once, and a poll that read an older job is discarded. An answer with a newer job that ended asks the poll at once too, also when the page already shows its own job ended (the poll then presents the newer job's outcome, with its queue attempt), and polls continue until the page shows that job, also when a poll already on its way showed the page's own job end. A newer job the page follows, from a cancel's answer, a poll or a full review response read before it was followed, does not keep the older job's queue attempt: only its own attempt (by its merge action ID) is followed. The merge click's answer is reconciled the same way: a later record a cancel's answer or a poll showed while the click was on its way (of the click's job, or a newer job) is kept over it, so an accepted cancel is not undone. Once the page shows a job ended, a poll that read that job, or an older one, before it ended changes nothing, and neither does an answer about anything but that job's own merge attempt: a job that ended
queuedorsubmittedfollows its own attempt (by its merge action ID). A refused merge click while the page follows a running job (a click needs Merge PR ready, so a cancel's answer or a poll showed that job while the click was on its way) keeps it followed, with the refusal's message before the job's step; a later ended record the refusal carries (of that job, or of a newer job) is presented, and an older one is not, except that one that endedqueuedorsubmittedis left to the poll, which follows its attempt. Once an answer carried an ended job, Cancel reads Cancel requested and stays unavailable for every record older than the latest ended record any answer carried (an earlier record of that job, or an older job), so a poll or reload read before it ended, an older job's cancel answered late, or a poll that read the job replaced does not offer Cancel again. A newer job another tab starts afterwards offers Cancel. An answer the page has already seen (the poll showed the job end first) changes nothing. A refusal's message is shown only while the cancelled job is still the one shown and still running, so it never replaces an outcome the poll showed. It is the only explanation of a Cancel that stayed enabled, so each poll shows it again until the job's record changes, also after a reload or a failed poll rewrote the banner. - A cancel while the task itself is being cancelled is refused ("The task is being cancelled."): the task cancel already stops the job and closes the pull requests.
- A cancel of a job that ended, a refused click, an unknown job, or a job still running on disk that this server does not run (its ending write failed) is refused (HTTP 409, with the task's latest job). During shutdown it answers 503. A repeat of the same cancel answers the job while it runs (200), unless the task is being cancelled, and once it has ended is refused as above with the ended job, which the page shows; so the request needs no key of its own.
- A restart after the cancel was saved, but before GitHub was asked, settles the job
cancelled, notfailed.
| Event | Effect |
|---|---|
| Cancel during preparation | as for prepare-merge: the preparation stops, and the task closes when its work has settled |
| Cancel while waiting | the wait stops at once; the task closes; the job ends cancelled |
| Cancel during the guarded merge | before GitHub is asked, the merge stops and the job ends cancelled; once a merge attempt is submitting, a cancel is refused, as before |
| Cancel merge (the merge alone) | as for a task cancel at each step, except that the task keeps its status and its pull request: see Step list and Cancel |
| Shutdown | in shutdown's first, synchronous step, before requests drain, the job is told to stop: a wait ends at once, and nothing after it asks GitHub to merge. A preparation in progress stops only when preparation does, after the drain, as for a prepare-merge click; the job then stops without merging. The job is awaited before the store closes. While preparing or waiting, it stays running on disk, unless the task or the merge was cancelled before the job stopped: then it ends cancelled, also when its stopped work (such as one of its own review loads) settles only after shutdown began. A guarded merge in progress is stopped in that first step too (a direct click's merge gets the drain period instead), and its outcome is recorded as a click's is: if GitHub was not asked yet, the job stays running; if it was, the job ends as the merge attempt records it: blocked if GitHub refused (failed if the head changed, or the review changed while GitHub was asked), otherwise submitted, and the attempt reconciles as before |
| Ending the job fails | if the write that ends a job fails, or reading the task after the job failed does, the server logs, also during shutdown (after GitHub was asked to merge), "Could not end merge job …; it shows as running until the server restarts" with the store's message. The job stays running on disk: the page keeps following it, and a new click is refused while it runs. The next start settles it as below |
| Restart | a running job that had not asked GitHub to merge becomes failed ("The server restarted before the merge finished"), or cancelled if the person had cancelled the merge (with "Task cancelled." when the task was cancelled too; see Step list and Cancel). One that had (its merge action ID is saved before the guarded merge starts) ends as its merge attempt records it: merged, queued, submitted, or blocked after a refusal, which also moves the task to approved, merge blocked (failed, task unchanged, when the head changed or the review changed while GitHub was asked). A prepare-merge action and a push marker are settled as before |
Schema v19 adds task_pull_requests.check_id (the pre-PR check each opening followed) and merge_jobs: one row per job or refused click, with three columns: id (the click's action ID, primary key), plan_key (the task, indexed by merge_jobs_plan) and data. data is a JSON record holding the rest: the click's review token, the guarded merge's action ID once the job reaches that step, state, step, reason, required-check names and states, the merge result URL, the step list (steps, with each step's state and start and end times) and cancelRequested (see Step list and Cancel), sequence (the record's place among the task's jobs and refused clicks, from 1, so the browser orders jobs without comparing clock times), revision (the record's version, from 1; every saved change to the job adds one, so the browser orders records of one job without comparing clock times, which can tie or step back), start and update times, and refused for a refused click. Lookups by state (the running job, the latest job that was not refused) read these JSON fields. A refused click's row is replayed to that click and never shown as the task's job. Writing it never changes the task's state version, so the readiness preparation recorded stays valid while the job waits. A resend of the same click returns its row; another click while a job runs is refused. While a job runs, prepare-merge is refused (the job prepares the merge itself). publish is refused too, by publishing's own status rule: it publishes only a running task whose plan items have all run, or a task that needs a person, and a job runs only for a task in review or approved but merge blocked. The runner's start and resume actions are refused too ("A merge is running for this task; wait for it to end, or cancel the merge or the task."), saved and replayed like other refusals, and neither is offered: a run would move the task's head, and the publish at its end would push it. So no run starts, and no publish can push the pull request's head or begin a refresh of it while the job prepares, waits or merges. Plan import stays allowed during a job, as at any time in review. It changes only the plan, never the branch or the pull request; the readiness the job prepared is then for an older review, so the job's next inspection ends it failed ("The merge has not been prepared for the current review; merging prepares it."), or the guarded merge refuses, and nothing is merged.
POST /api/actionwithmergereturns at once when the server runs merge jobs (a runner and a pull request published by it, which requires preparation): the reloaded review with the job's latest record asjob, or, if that reload failed,{ job, mergeRefreshRequired: true }. The job may already have moved on or ended by then; the page follows a running job and, for an ended one, asks for a refresh. A resend of a click whose job has ended replays that job while a later one (another tab's retry) may run: the reloaded review then carries the later job, and the page follows the later of the two records (bysequence, orrevisionwithin one job), so it follows the running job rather than showing the replayed outcome.- With merge jobs, the merge status in the review (
GET /api/reviewand the click's answer) carriesjob, the task's latest job, andsettledBeforeJobMerge: the blocker codes settled once a job asks GitHub to merge. It is built from the server's job-resolved blockers (preparation, base, acceptance, check) plusmergeable, and the page uses it rather than a copy of its own. GET /api/mergereturns{ queue, job }:jobis the task's latest job. A saved refusal is not a job; it is replayed only to its own click.- A refused merge click (409, new or replayed) carries
job, the task's latest job, asGET /api/mergewould answer it. A page that holds an older review, such as a second tab, may click Merge PR while another tab's job runs; it is refused ("A merge is already running for this task."). When the carried job is running and later than the one the page shows, the page follows it as its poll would: the control shows the job's step, and the banner reads the refusal, then the step. The refusal is a definite answer, so the click's key is dropped, as for any other refusal; any other refusal still asks for a refresh. - Merge PR is offered only while the task is in a mergeable status (read after the inspection) and the only blockers left are ones the job resolves or reports: preparation, a moved base (with a merge conflict it causes),
cmd:checks, required checks (the job waits for pending ones and endsblockedon failing ones, so a person can retry after re-running CI), and mergeability GitHub is still computing. - While a job runs, the Merge button keeps keyboard focus: it is marked
aria-disabled, not disabled, and a repeat activation starts nothing. While the click's own request is in flight, the button reads Requesting merge… and stays marked, also when the page renders meanwhile (for example when a person selects an item). - The browser polls
GET /api/mergewith the existing 2 s to 30 s backoff while a job runs, shows its step in the merge banner, and lists waiting checks in the blocker dialog. The control and the dialog's heading name the step the same way: Preparing…, Waiting for checks… or Merging…. A full review response read before the poll's latest record keeps that newer record: a later record of the same job (by itsrevision, never itsupdatedAt), or a later job (by itssequence, which another tab's click may have started). A kept running job keeps Merge PR not ready and its poll running, so a slow Refresh, review action, Refresh plan, plan import or Apply reload does not restart following a job the poll saw end, or stop following a newer job and offer Retry while it runs. A kept job that endedqueuedalso keeps the merge attempt the poll followed for it (the attempt whoseactionIdis the job'smergeActionId), whatever queue the full response holds, and shows it as the poll did: Merged or a removal stays, and while GitHub's queue holds it the control reads Merge queued and the queue poll continues. A click's key is kept until its own job ends or a job newer than the one shown at the click is the latest, so a retry that failed with no answer is resent with the same key. When the job ends, the page updates only the merge control and banner (the outcome, and "Refresh to see the result"), so a person's selection and input survive. A job that endsqueuedleft its merge attempt in GitHub's queue, and the poll that sees it end answers that attempt asqueue(itsactionIdis the job'smergeActionId). The page then follows the attempt as it follows a queue merge, also when its review had no queue: the control reads Merge queued while GitHub's queue holds it, and the queue poll continues until GitHub merges or removes it, which the control and banner then read (Merged and "GitHub confirmed the reviewed head was merged.", or the removal) without a refresh. The click's answer is read the same way: when it carries a job that endedqueuedand that job's own attempt (another client's poll may have seen the attempt merged, removed or failed before the click's review reload read it), the control and banner read the attempt (Merged, or the removal, not Merge queued), and an attempt GitHub's queue still holds is polled until it ends. The job poll, the click's answer and a newer record kept over a full response present an ended job and its own attempt through one rule, in which the attempt's status takes precedence over the job's outcome in both the control and the banner. Whichever way the page learns the job ended (the poll, the click's answer, or a newer record kept over a full response), the control reads the outcome, as it does after a merge-queue merge: Merged, Merge queued, Merge submitted, Merge blocked, Review needed, Merge stopped or Merge cancelled. Its Merge status dialog lists the outcome with "Refresh to see the result". After Merged, Merge queued or Merge submitted the job asked GitHub to merge, which it does only once no blocker remains, so the dialog leaves out the blockers the merge status'ssettledBeforeJobMergenames (preparation, a moved base,cmd:checks, required checks and mergeability): the review the page holds was read before or during the job and still lists them. After any other outcome the job did not settle them (a blocked job's failing check still fails), so the dialog keeps every blocker the page last read, and Refresh shows which still apply. After Refresh, the page offers Retry merge when the job isblocked; its confirmation says the retry prepares the merge again and may rebase onto the current base and need a new review, not that it merges the exact reviewed head (that wording stays for a merge without a runner). The step list and its Cancel merge button are described in Step list and Cancel.
test/merge-job.test.ts runs the job against the real Store and guarded merge with a faked GitHub and preparation. It covers the three wait cases the design names (zero required checks, one check pending then passing, a timeout), a failing check, GitHub still computing mergeability, preparation ending for review or failing, another blocker while waiting, a failed GitHub read while waiting (also one that settles after the wait limit), GitHub's refusal, an unknown merge outcome, a merge queue (queued), cancel while waiting (told or not) and during the guarded merge's validation, shutdown while waiting and during the guarded merge, startup settlement, resends, a second click, a stale token, a busy runner, which blockers keep Merge PR available (including Retry merge after blocked), the job-mode wording of a moved base and of preparation, and each ended reason's next step (wait after a timeout, fix after a failing check or refusal, nothing repeated in a failed reason). test/pre-merge.test.ts runs the #22 acceptance cases through the job with real preparation: a base move that leaves approvals valid merges; a rebase that makes an approval stale stops for review; a breaking command after a rebase fails the job; a collaborator push stops for review; a cancel during a command check ends the job cancelled. Further regressions cover a cancel, then shutdown, before the job's own review load rejects (the job ends cancelled), a plan import while the job's first review load is pending (the job ends failed and prepares nothing), a queue poll that records removed or failed while GitHub is asked to merge (the guarded merge rejects and the job ends blocked; with a plan import after the poll, the job ends failed and the task stays in review), and an inspection that finds the checks passing, or one failing, but finishes after the wait limit before its timer runs (a timeout, or the failing check). Server tests cover the merge route (including a second tab's refused click, which carries the running job), its trust admission (an untrusted click refused and replayed, a failed read saving nothing), GET /api/merge, the prepare-merge and publish refusals, the start refusal after a plan import and approval during the preparation, the start and resume refusals after the same during the check wait (the import then ends the job failed, with nothing run, pushed, published or merged), cancel closing the pull requests, and shutdown; a production test covers startup settlement; a browser test follows a job to merged, and others show that once a job ends (polled, answered to the click, or kept over an older full response) the control reads its outcome; after a job that reached the merge it drops the blockers the server names in settledBeforeJobMerge, and after a failed one it keeps a conflict and a check blocker. Others show that a job the poll sees end queued is followed through GitHub's queue to merged without a refresh, from a review with no queue, that a resent click whose answer replays its ended job follows the later running job the reloaded review carries, that a click refused because another tab's job runs follows that job, that a blocked banner states its reason's next step once, and that the running job's dialog names the step as the control does.
The step list and Cancel merge (D16) are covered by: test/merge-job.test.ts (the seven steps and their times, skipped and failed steps, a failed step write, a merge cancel during preparation, the checks wait, the guarded merge's validation and the first review load, refusals once GitHub was asked, while the task is being cancelled and for an ended or unknown job, a task cancel winning, cancel then shutdown, an inspection answering after a cancel or after shutdown began, a resend that joins or replays a merge click, Fetch Failed when preparation stops for review, the Merge step staying Waiting when the merge attempt cannot be saved, a refused cancel for a job whose ending write failed, and restart settlement); test/pre-merge.test.ts (the steps preparation reports, a throwing progress callback, and a merge cancel during a rebase, a push and a command check, each leaving the task in review); test/runner-publishing.test.ts (the cancel-merge route); and the browser tests tagged D16 in test/browser/review.spec.ts (the list and its accessible text, Cancel and its refusals, answers in every order against the poll, the poll's speed when the list's step changes, and focus), plus the F6b test that follows a job ended in the merge queue (the queue poll keeping the list).
Before merging, ask someone who did not write it to answer, from this page alone: what one click does, when the task becomes approved, merge blocked, and why a job survives a review edit only by failing.