aircall-merge-train (stage 4 of the react-doctor pipeline, but reusable for any MR batch)
Merges a batch of MRs that are already approved and Ready, one at a time, on a repo using fast-forward merge (merge_method=ff). This is a supervised main-loop, NOT a Workflow script: it spans hours (CI-paced) and stops for human decisions. Pace it with ScheduleWakeup (~120s) per the /loop pattern; report progress to the user (and Slack if asked).
Why this is a loop, not a workflow
ff + reset_approvals_on_push + Bugbot gates + day-long approval latency mean a deterministic background script can't sit and wait or make the judgment calls. Keep it in the main loop where it can DM/ask the user.
Inputs
- A list of MR iids to merge (e.g.
aircall-react-doctor-pipeline'sreadyMRs), or "all open approved MRs matching a branch prefix". (Nothing here is react-doctor-specific — any approved MR batch works.) - Project (path-encoded for
glab api, e.g.aircall%2Fdashboard-extensions%2Fconversation-center-ext). - Optional Slack channel/thread for progress, and a DM channel for decisions.
Setup (once)
glab auth status— confirm authenticated.- Read repo merge settings:
glab api projects/$ENC | jq '{merge_method, only_allow_merge_if_pipeline_succeeds, only_allow_merge_if_all_discussions_are_resolved, reset_approvals_on_push}'. Confirm ff. - Persist a state file (e.g.
.omc/state/mr-merge-train.json):{queue, merged, skipped, blocked, needs_approval, current}so the loop survives compaction/restart. - Parsing note: MR descriptions often contain raw control chars that break
jq. Parseglab apiMR objects withpython3 -c "import sys,json; d=json.loads(sys.stdin.read(),strict=False); ...".
Per-MR procedure (strictly one at a time)
For current MR:
- Rebase:
glab api -X PUT projects/$ENC/merge_requests/$mr/rebase; wait ~13s. - Re-check
detailed_merge_status(dms):need_rebase(raced — another MR merged) → rebase again.not_approved→ the rebase changed the diff andreset_approvals_on_pushcleared approval. Do NOT arm. Move toneeds_approval, re-queue at END, advance. (Ping the approver if the user wants.)discussions_not_resolved→ a Cursor Bugbot thread (authorproject_*_bot_*) is blocking. Read it; do not silently resolve High/Medium findings. Cancel auto-merge (POST .../cancel_merge_when_pipeline_succeeds), move toblocked, DM the user with the finding, advance.mergeable/ci_still_running→ proceed.
- Conflict on rebase (
merge_error: "Rebase failed"):- If it's
doctor.config.json(or any file the user pre-flagged) → do NOT skip; DM the user and wait for instructions. - Otherwise →
skipped+ note "needs local rebase", advance. (Optionally spawn a worktree agent to resolve.)
- If it's
- Arm auto-merge:
glab api -X PUT projects/$ENC/merge_requests/$mr/merge -f merge_when_pipeline_succeeds=true. If the pipeline is already green it merges immediately. - Poll every ~120s (
ScheduleWakeup): whenstate==merged→ record, advance to next. Ifmwpsdropped to false (fell behind main) → re-rebase + re-arm. - Flaky E2E: if the pipeline fails on a known-flaky job (e.g.
e2e-509-admin-merge-branchPlaywright timeout) unrelated to the diff → retry that job once (POST projects/$ENC/jobs/$jid/retry); leave mwps armed. If it fails again →skipped+ note "needs attention". - Real CI failure (e.g.
check-fallow,check-ts-compile) → inspect the job trace;skipped+ note the cause. Do NOT push code changes to an approved MR autonomously unless the user said so.
Re-approval pile
Items in needs_approval need a human approval that may arrive much later. Each cycle, re-check their approval; when approved, move into the queue (they'll be rebased+armed when they become current). Note: re-arming requires a rebase, which may reset approval again if the diff changed — surface that.
Finish
When the queue is empty and only undecided items remain (blocked on Bugbot / unapproved / needing local fixes):
- Stop rescheduling (do not call
ScheduleWakeup). - Post a final summary (merged / skipped / blocked / needs-approval, with what each needs).
- If a backlog/status doc exists, tick merged tickets and refresh the tally.
- Clean up stale worktrees for merged/closed branches: verify each via
glabMR state, thengit worktree remove --force <wt>+git worktree prune. KEEP worktrees for open MRs and active WIP (no MR). Branch refs are preserved.
Hard-won gotchas (do not relearn)
- ff means every merge invalidates the others' rebase → strictly one at a time.
reset_approvals_on_pushfires on a rebase only when the rebase changes the MR's diff (overlap with siblings merged ahead). Clean replays keep approval.- Never auto-resolve Bugbot High/Medium — that's a human call (DM).
- The shell here is zsh (the env may report fish): use
for x in a b c; do …; done, not fish syntax; it does not word-split unquoted vars. - Slack has no edit-message API exposed — post an updated checklist reply rather than editing.