Merge pull request 'Merge robot: deterministic merge-on-green' (#203) from merge-robot into master
This commit is contained in:
commit
2975d7651d
1 file changed
+77
@@ -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
|
||||
Reference in new issue
Block a user