Merge pull request 'Merge robot: deterministic merge-on-green' (#203) from merge-robot into master

This commit is contained in:
conductor committed 2026-09-07 14:12:35 +01:00
commit 2975d7651d
1 file changed
+77
+77
View File
@@ -0,0 +1,77 @@
name: Merge robot
# Deterministic merge-on-green for this repo's PRs. No LLM, no polling by the
# agent: the forge event model does the watching, this job does the one action.
# Runs hourly + on CI completion events so green PRs merge without any agent
# turn being spent on "check CI, merge, clean up".
on:
schedule:
- cron: '23 * * * *'
workflow_run:
workflows: ["CI Pipeline", "Auto-version and Release"]
types: [completed]
permissions:
pull_requests: write
issues: write
contents: read
env:
OWNER: conductor
REPO: rune
jobs:
merge-green:
runs-on: rune-ci-desktop
steps:
- name: Merge PRs whose protected checks are green
env:
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
SERVER: ${{ gitea.server_url }}
run: |
python3 - <<'PY'
import json, os, time, urllib.request
TOKEN = os.environ["GITEA_TOKEN"]
BASE = os.environ["SERVER"].rstrip("/") + "/api/v1/repos/" + os.environ["OWNER"] + "/" + os.environ["REPO"]
def api(path, method="GET", data=None):
req = urllib.request.Request(
BASE + path,
data=json.dumps(data).encode() if data is not None else None,
headers={"Authorization": "token " + TOKEN, "Content-Type": "application/json"},
method=method)
try:
with urllib.request.urlopen(req, timeout=30) as r:
body = r.read()
return r.status, (json.loads(body) if body else {})
except urllib.error.HTTPError as e:
return e.code, {"_body": e.read().decode("utf-8", "replace")[:300]}
status, prs = api("/pulls?state=open&limit=50")
if status != 200:
print(f"list PRs failed: {status}"); raise SystemExit(0)
merged = []
for pr in prs:
n = pr["number"]
sha = pr["head"]["sha"]
st, cs = api(f"/commits/{sha}/status")
state = cs.get("state")
if state != "success":
print(f"#{n}: status={state} — skip")
continue
# mergeable guard: skip conflicts instead of hammering
st, detail = api(f"/pulls/{n}")
if not detail.get("mergeable", False):
print(f"#{n}: not mergeable (conflict?) — skip")
continue
st, resp = api(f"/pulls/{n}/merge", "POST", {"Do": "merge"})
if st in (200, 405):
print(f"#{n}: merged ({st})")
merged.append(n)
else:
print(f"#{n}: merge failed {st}: {str(resp)[:200]}")
time.sleep(2)
print(f"done — merged {len(merged)} PRs: {merged}")
PY