Redact thread on merge success even if deploy fails; add clear command
A merge whose post-merge deploy failed left the review thread orphaned (redaction was gated on deploy success). Now the thread always redacts on merge success and the deploy-failure warning is posted at top level so it survives. New clear/resolve command redacts an orphaned thread without touching the PR.
This commit is contained in:
@@ -43,14 +43,17 @@ flat JSON in a writable `state/` mount.
|
|||||||
- **Onboard a repo:** create a Matrix room named like the repo, invite this bot → it auto-joins,
|
- **Onboard a repo:** create a Matrix room named like the repo, invite this bot → it auto-joins,
|
||||||
maps the room to `<owner>/<roomname>` (+ `~/Projects/<roomname>` on the Mac), and posts an
|
maps the room to `<owner>/<roomname>` (+ `~/Projects/<roomname>` on the Mac), and posts an
|
||||||
onboarding message. Pick review agents in-chat: `agents +reviewer +security -adjudicator`.
|
onboarding message. Pick review agents in-chat: `agents +reviewer +security -adjudicator`.
|
||||||
- **In a review room:** `merge` / `reject` (inside a PR's thread, or `merge <n>` by number), `yes`/`no`
|
- **In a review room:** `merge` / `reject` / `clear` (inside a PR's thread, or `<cmd> <n>` by number),
|
||||||
to confirm a merge, `agents …` to toggle the subagent panel.
|
`yes`/`no` to confirm a merge, `agents …` to toggle the subagent panel. `clear` (alias `resolve`)
|
||||||
|
redacts an orphaned thread without touching the PR.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
- `src/bot.py` — the bot: matrix-nio sync; auto-join + in-room auto-map (room→repo by name); a Gitea
|
- `src/bot.py` — the bot: matrix-nio sync; auto-join + in-room auto-map (room→repo by name); a Gitea
|
||||||
poll loop per mapped repo; threaded `claude -p` review (subagent panel); merge/reject/deploy
|
poll loop per mapped repo; threaded `claude -p` review (subagent panel); merge/reject/deploy
|
||||||
in-thread; whole-thread redaction on resolve (server-enumerated, restart-proof).
|
in-thread; whole-thread redaction on resolve (server-enumerated, restart-proof) — a merge always
|
||||||
|
redacts (a failed post-merge deploy leaves a top-level warning, not an orphaned thread); `clear`
|
||||||
|
redacts without touching the PR.
|
||||||
- `config.example.toml` — homeserver, `[mac]` (ssh alias + the reused matrix-bridge wrapper paths),
|
- `config.example.toml` — homeserver, `[mac]` (ssh alias + the reused matrix-bridge wrapper paths),
|
||||||
`[gitea]` (api_base/owner/verify_tls), `[defaults]`, optional `[repo.<name>]` deploy overrides.
|
`[gitea]` (api_base/owner/verify_tls), `[defaults]`, optional `[repo.<name>]` deploy overrides.
|
||||||
- `.env.example` — `MATRIX_*` + `GITEA_TOKEN` (real `.env` gitignored).
|
- `.env.example` — `MATRIX_*` + `GITEA_TOKEN` (real `.env` gitignored).
|
||||||
@@ -82,14 +85,19 @@ revisit this — local inference via Spark Control would be the path.
|
|||||||
|
|
||||||
## Current state
|
## Current state
|
||||||
|
|
||||||
**Built + DEPLOYED 2026-06-28; awaiting first real PR.** Bot is running on the Spark (`docker compose
|
**LIVE and proven.** Bot runs on the Spark (`docker compose up -d`), `@reviewer` maps each review room
|
||||||
up -d`), `@reviewer` is in a review room, the room is mapped to a repo, and the subagent panel is
|
to its repo, and the subagent panel is configured in-chat. Proven end-to-end on **ten31-site PR #4**
|
||||||
configured in-chat. Core flow ported from matrix-bridge (D15–D19), generalized to multi-repo + the
|
(2026-06-28): threaded review → `merge` + `yes` → force_merge → auto-publish → thread redacted — and
|
||||||
panel. **Not yet exercised on a live PR** — next session: open a test PR and confirm the chain
|
the previously-unproven bit, **headless `claude -p` spawning the reviewer/adjudicator/security
|
||||||
(threaded review → `merge`+`yes` → force_merge → auto-publish → thread redacted). **The one unproven
|
subagents, works**. The ten31-site colleague is onboarded; the pipeline is in steady use.
|
||||||
bit:** headless `claude -p` spawning the reviewer/adjudicator/security subagents (Option B) — if it's
|
|
||||||
flaky, fall back to bot-orchestrated separate `claude -p` runs (the rejected alternative; see ROADMAP
|
**This session's hardening (NOT yet on the Spark — needs a redeploy: `git fetch && git reset --hard
|
||||||
Phase 2). **Deploy gotchas hit this session:** the Spark needs a *dedicated* per-repo Gitea deploy
|
origin/master && docker compose up -d --build`):** a merge now **always redacts** the thread on merge
|
||||||
key + a `Host` alias (the default `immense-voyage.local` block uses matrix-bridge's key) — clone via
|
success; a failed post-merge deploy posts its "deploy manually" warning at **top level** so it
|
||||||
the alias; Gitea won't reuse one SSH key across repos' deploy keys. A Spark Control tile is captured
|
survives the redaction instead of orphaning the thread (the bug PR #4 hit when the Mac was left off
|
||||||
in the inbox.
|
`master`). New **`clear`/`resolve`** command redacts an orphaned thread without touching the PR.
|
||||||
|
|
||||||
|
**Deploy gotcha (still true):** the Spark needs a *dedicated* per-repo Gitea deploy key + a `Host`
|
||||||
|
alias (the default `immense-voyage.local` block uses matrix-bridge's key); Gitea won't reuse one SSH
|
||||||
|
key across repos' deploy keys. A Spark Control tile is captured in the inbox. **Next:** redeploy on
|
||||||
|
the Spark, then clear ten31-site's orphaned PR #4 thread (reply `clear` in it, or `reject`).
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ is PR review.
|
|||||||
`<owner>/<repo>` (and `~/Projects/<repo>` on the Mac), then posts an onboarding message.
|
`<owner>/<repo>` (and `~/Projects/<repo>` on the Mac), then posts an onboarding message.
|
||||||
2. It polls that repo's open PRs and posts each as a **thread**: a headless `claude -p` review (with
|
2. It polls that repo's open PRs and posts each as a **thread**: a headless `claude -p` review (with
|
||||||
an optional subagent panel) run on the Mac over SSH.
|
an optional subagent panel) run on the Mac over SSH.
|
||||||
3. In the thread: `merge` (then `yes`) or `reject`. On a clean merge it can auto-deploy, then
|
3. In the thread: `merge` (then `yes`) or `reject`. A merge auto-deploys (if enabled) and then
|
||||||
redacts the thread so the room shows only open PRs.
|
redacts the thread, so the room shows only open PRs; if the deploy fails, the thread still
|
||||||
|
clears and a standalone "deploy manually" warning stays in the room. `clear` redacts an
|
||||||
|
orphaned thread without touching the PR.
|
||||||
|
|
||||||
Pick which review agents run per room in chat: `agents +reviewer +security -adjudicator`.
|
Pick which review agents run per room in chat: `agents +reviewer +security -adjudicator`.
|
||||||
|
|
||||||
|
|||||||
+22
-9
@@ -446,12 +446,14 @@ async def main():
|
|||||||
await say(rid, "🚀 Publishing the merged change to the live site…", root)
|
await say(rid, "🚀 Publishing the merged change to the live site…", root)
|
||||||
rc, out = await run_deploy(room["repo_dir"])
|
rc, out = await run_deploy(room["repo_dir"])
|
||||||
if rc == 0:
|
if rc == 0:
|
||||||
await say(rid, "✅ Live site updated.", root)
|
print(f"{room['label']} deployed PR #{num}", flush=True)
|
||||||
return True
|
return True
|
||||||
tail = (out or "no output").splitlines()[-1] if out else "no output"
|
tail = (out or "no output").splitlines()[-1] if out else "no output"
|
||||||
print(f"{room['label']} deploy FAILED rc={rc}: {(out or '')[:400]}", flush=True)
|
print(f"{room['label']} deploy FAILED rc={rc}: {(out or '')[:400]}", flush=True)
|
||||||
await say(rid, f"⚠️ Merged, but auto-deploy didn't run (rc={rc}): {tail[:300]} "
|
# Top level (no thread_root) so it survives the thread redaction that
|
||||||
f"— deploy it manually.", root)
|
# follows a merge — the operator still needs this actionable warning.
|
||||||
|
await say(rid, f"⚠️ PR #{num} merged, but auto-deploy didn't run (rc={rc}): {tail[:300]} "
|
||||||
|
f"— deploy it manually.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def do_merge(rid, num):
|
async def do_merge(rid, num):
|
||||||
@@ -460,13 +462,16 @@ async def main():
|
|||||||
status, body = await asyncio.to_thread(
|
status, body = await asyncio.to_thread(
|
||||||
lambda: gitea_w("POST", f"/repos/{room['repo']}/pulls/{num}/merge",
|
lambda: gitea_w("POST", f"/repos/{room['repo']}/pulls/{num}/merge",
|
||||||
{"Do": "merge", "force_merge": True}))
|
{"Do": "merge", "force_merge": True}))
|
||||||
if status is not None and 200 <= status < 300:
|
if not (status is not None and 200 <= status < 300):
|
||||||
print(f"merged {room['label']} PR #{num}", flush=True)
|
|
||||||
if await deploy_after_merge(rid, num):
|
|
||||||
await redact_thread(rid, num)
|
|
||||||
else:
|
|
||||||
detail = gitea_error_message(body) if status else body
|
detail = gitea_error_message(body) if status else body
|
||||||
await say(rid, f"⚠️ merge PR #{num} failed (HTTP {status}): {detail}", root)
|
await say(rid, f"⚠️ merge PR #{num} failed (HTTP {status}): {detail}", root)
|
||||||
|
return
|
||||||
|
print(f"merged {room['label']} PR #{num}", flush=True)
|
||||||
|
# The PR is resolved, so always clear the review thread — a post-merge
|
||||||
|
# deploy hiccup must not orphan it. deploy_after_merge reports a failure
|
||||||
|
# at top level, which survives the redaction.
|
||||||
|
await deploy_after_merge(rid, num)
|
||||||
|
await redact_thread(rid, num)
|
||||||
|
|
||||||
async def do_reject(rid, num):
|
async def do_reject(rid, num):
|
||||||
room = rooms[rid]
|
room = rooms[rid]
|
||||||
@@ -510,7 +515,7 @@ async def main():
|
|||||||
rooms[rid]["threads"].get(thread_pr))
|
rooms[rid]["threads"].get(thread_pr))
|
||||||
return
|
return
|
||||||
|
|
||||||
if cmd not in ("merge", "close", "reject"):
|
if cmd not in ("merge", "close", "reject", "clear", "resolve"):
|
||||||
return
|
return
|
||||||
if thread_pr is not None:
|
if thread_pr is not None:
|
||||||
num = int(thread_pr)
|
num = int(thread_pr)
|
||||||
@@ -527,6 +532,14 @@ async def main():
|
|||||||
if cmd == "merge":
|
if cmd == "merge":
|
||||||
pending_merge[(rid, str(num))] = True
|
pending_merge[(rid, str(num))] = True
|
||||||
await say(rid, f"⚠️ Reply `yes` in this thread to merge PR #{num} (or `no` to cancel).", root)
|
await say(rid, f"⚠️ Reply `yes` in this thread to merge PR #{num} (or `no` to cancel).", root)
|
||||||
|
elif cmd in ("clear", "resolve"):
|
||||||
|
# Redact an orphaned thread without touching the PR (e.g. it was
|
||||||
|
# merged outside the bot, or a deploy failure left the thread up).
|
||||||
|
if not root:
|
||||||
|
await say(rid, f"No tracked thread for PR #{num} to clear.")
|
||||||
|
return
|
||||||
|
pending_merge.pop((rid, str(num)), None)
|
||||||
|
await redact_thread(rid, num)
|
||||||
else:
|
else:
|
||||||
await do_reject(rid, num)
|
await do_reject(rid, num)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user