Skip to main content

ops-merge-loop

Sweeps the PRs a maintainer has labelled for landing and hands each to `ops-integrate · land`, then reports what came back. It owns scheduling only — the sweep, the CI poll cadence and its 15-minute cap, the cap of 10 PRs per run, and the comment on each PR. Every merge gate, the release-base skip and the merge itself belong to `ops-integrate`; this loop never resolves a base branch or a merge strategy and never calls `ops-branching`. Repo-agnostic; runs locally or as a scheduled cloud routine. Trigger on "land the ready PRs", "run the merge loop".

معلومات المصدر

المستودع
umbraco/umbraco-ai-ops
آخر نشاط في المصدر
٢٤ سبتمبر ٢٠٢٦ في ١٤:٥٠
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
٢
التفرعات
٠

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
ops-merge-loop
description
Sweeps the PRs a maintainer has labelled for landing and hands each to `ops-integrate · land`, then reports what came back. It owns scheduling only — the sweep, the CI poll cadence and its 15-minute cap, the cap of 10 PRs per run, and the comment on each PR. Every merge gate, the release-base skip and the merge itself belong to `ops-integrate`; this loop never resolves a base branch or a merge strategy and never calls `ops-branching`. Repo-agnostic; runs locally or as a scheduled cloud routine. Trigger on "land the ready PRs", "run the merge loop".
# ops-merge-loop The loop that removes the manual PR merge — the step where mistakes happen (merging red CI, merging before approval, merging into the wrong base, forgetting the branch). **It decides nothing about merging.** A maintainer labels a PR; this loop finds it, hands it to **`ops-integrate · land`**, and says what happened. Every gate, the release-base skip and the strategy live in that service. **Policy in the service, scheduling in the loop.** That line is the whole design: | This loop owns | `ops-integrate` owns | |---|---| | finding the labelled PRs | the landing-label gate | | the CI poll cadence + its 15-minute cap | whether CI is green | | the cap of 10 PRs per run | whether the PR is mergeable | | commenting the outcome on each PR | whether the base is legitimate | | when to stop | the merge itself, and the strategy | ## What it calls | Capability · action | Why | Visibility | |---|---|---| | **`ops-integrate · land`** | the command this loop exists to issue | service | | **`ops-change · close-issue`** | after a merge. The capability decides whether the issue closes now or waits, usually for a release | service | | **`ops-port-loop`** | after a merge, when the PR carries `labels.port` — the normal way a port starts | framework loop | | `ops-ci · status` | poll before handing over, so a pending PR costs one cheap read instead of a full gate run | cross-cutting (read) | | `ops-repo-meta · identity` | the landing label, **by purpose** (`labels.land`) — never a hard-coded name | cross-cutting (read) | | `ops-repo-meta · topology` | which repo holds the code, so the sweep looks in the right place | cross-cutting (read) | | `ops-notify · send` | only when a human is blocking something | cross-cutting (infra) | **It never calls `ops-branching`.** If this loop knows a branch name, the design has failed. All GitHub reads and the comments go through **`github-ops`** by operation name. ## The `/goal` ``` /goal Every PR labelled <labels.land> on <code repo> is either merged with its branch deleted, or carries a comment naming the specific blocker. No PR left silently unhandled. ``` `/goal` is what makes "done" unambiguous — every candidate reaches one of those two states, and there are no half-done merges. ## Step 1 — sweep Resolve `labels.land` (`ops-repo-meta · identity`) and the `code` repo (`ops-repo-meta · topology`). **List open PRs with that label** (`github-ops` → *List PRs by label / state*). No candidates → report "nothing to land" and stop. That is a normal outcome, not a problem. **Cap: 10 PRs per run.** More than that, take the oldest ten and **log how many were deferred** — never silently drop the tail. The next run picks them up. ## Step 2 — poll CI, then hand over For each candidate, in order: 1. **`ops-ci · status`.** If `pending`, wait and re-read on a sane cadence, up to a **15-minute cap for the whole run**. Still pending at the cap → leave it for the next run. **The next run comes by itself:** when that build finishes green, the edge routes the `check_suite.completed` event back here for any PR carrying the landing label. So stopping at the cap is safe, and waiting longer to avoid it only burns a session. This poll is an **optimisation, not a gate**: it stops the loop paying for a full gate run on a PR whose CI has not finished. 2. **`ops-integrate · land`** with `{ pr: { repo, number } }`. That is the entire decision. Do not pre-check the label, the base, the mergeability or the reviews — the service checks all four, and a second opinion here is how the two drift apart. ## Step 3 — report the outcome Read `outcome` and act: | `outcome` | Do | |---|---| | `merged` | Comment confirming the merge, then **`ops-change · close-issue`** with the landed PR, then hand off to **`ops-port-loop`** if the PR carries `labels.port` (both below). | | `skipped:release-base` | **Say nothing.** The release path owns it; a comment here is noise on someone else's PR. | | `blocked:ci` | Comment `detail` — the specific failing check. Leave the label on. | | `blocked:conflict` | Comment `detail`. **Remove the label** — a conflict needs a human, and re-poking it every run is noise. Say in the comment that you removed it, and why. | | `blocked:changes-requested` | Comment `detail`. **Remove the label** — a human said no. | | `blocked:wrong-base` | Comment `detail`. **Remove the label** — retargeting is a human decision. | | `blocked:no-label` | Say nothing; the label went away between the sweep and the gate. Not an error. | **Leave the label on for anything that will clear by itself** (CI, a pending run) so the next run re-checks. **Remove it for anything needing a human**, and always say which you did. ### Closing the issue behind a merge After a `merged` outcome, call **`ops-change · close-issue`** with `{ landed: { repo, pr_number, line } }`, where `line` is **the one `land` just returned**. **Pass the PR, not an issue.** This loop does not know which issue the PR was for, and must not guess: how a PR references its issue is a repo convention (a `Closes #N`, a full cross-repo URL), and which lines are targets for that change is a repo fact. `ops-change` created the PR, so it knows both. **And do not work the line out yourself.** `land` returns it, having got it from the only thing that holds the branch-to-line mapping. Reading `v18` out of a base ref here would be this loop learning a product fact, and it would be wrong on the first repo whose lines are not named `vN`. It returns `closed: false` with `waiting_on` when something is still outstanding. That can be other lines, or `release` on a repo that closes issues only once the fix ships. **This is the normal case, not a failure.** One logical change lands N times at N moments, and the issue closes when the repo's condition is met. That may happen in `ops-release-loop`, not here. Report what it says and move on; do not retry, and never close an issue yourself. **Write the confirming comment on the PR, not the issue.** This loop's comments go to the code repo. On a split topology, anything it wrote to the issues repo would follow the `github-ops` no-code-links rule, and it has nothing to tell the reporter until the fix ships. This is here for one reason: this is the moment a landing becomes known, and nothing else is watching for it. ### Handing a landed change to the port loop Still after a `merged` outcome, check whether the PR carries **`labels.port`** (from `ops-repo-meta · identity` — never the literal name). If it does, hand off to **`ops-port-loop`** with the merged PR, its merge commit, **and the `line` `land` returned**. Passing the line is not a convenience: the port loop derives every target from it, and it has no way to name the line itself. Hand it over and the port loop needs no guess; leave it out and it has to ask a human. - **This is the normal way a port starts.** The merge is the moment: a port is cut from the merge commit, so it cannot begin before one exists. The route row on the port label exists only to catch the other case — a human labelling a PR that has *already* merged — and the edge enforces it: an `ops/port` label on an open PR that carries the landing label routes to none, so this handoff is the only port that starts. Do not skip it on the theory that the label already fired a port; for a PR labelled for both, it did not. - **Hand off, do not port here.** This loop schedules landings. It does not resolve lines, and it must never call `ops-change · implement`. Everything about which lines and in which direction lives in `ops-port-loop` and `ops-repo-meta · lines`. - **Read `ops-port-loop` for the handoff only.** Its rules bind it, not you and not the PR. In particular its "never applies the landing label" is about what that loop *writes*; it is not a reason to treat a landing label on a port PR as wrong, and it is never a reason to remove one. A PR being a port is not a gate — the gates live in `ops-integrate` and ports are not among them. - **No label, no ports.** A port is never opened without a maintainer confirming it, and the absence of the label is that confirmation withheld. Do not infer one from the issue text. If the hand-off cannot happen, say so in the outcome comment. A change that should have been ported and silently was not is invisible until the line drifts. **Notify only when a human is blocking something** — `blocked:conflict`, `blocked:changes-requested`, `blocked:wrong-base`. Use `ops-notify · send` with a stable `key` such as `land-blocked-<repo>-<number>`, so a PR that stays blocked for a week does not notify daily. A successful merge is not notification-worthy; the comment is the record. ## Guardrails - **Never merge anything yourself.** `ops-integrate` merges. This loop has no merge path, no `github-ops` merge call, and no strategy. - **Never resolve a base branch, a release base or a merge strategy.** You cannot get them, and must not want them. - **Never re-implement a gate** to "save a call". The duplication that used to exist between this loop and its config is exactly the bug the capability model removes. - **≤ 10 PRs per run; log the deferred.** - **15-minute CI cap for the run**, not per PR — one stuck PR must not eat the whole window. - **One comment per PR per run**, and don't repeat last run's comment verbatim when nothing has changed. A PR blocked for a week should not carry seven identical comments. - **The landing label must be applied deliberately by a maintainer after review** — that act is the human gate, so control who can apply it. - **Never use `fable`.** ## Running as a routine **Primary: event-triggered, twice over.** `loop-dispatch` routes `pull_request.labeled` + `ops/auto-merge` here, so labelling a PR fires it immediately. It also routes `check_suite.completed` here when a build finishes green on a PR carrying the landing label, so a PR labelled while a long build ran lands when the build finishes. The loop sweeps *all* labelled PRs, so either event just runs one pass of the same loop. **Optional backstop:** a low-frequency poll (once or twice a weekday). With the CI-finished route it should find nothing. It is only worth having where the caller workflow predates that route (no `check_suite` trigger), or where a CI provider does not report through a GitHub check suite at all.
عرض على GitHub