Slice 8 WIP: §12 hygiene + §10.7 + routing + rollback cleanup
- Add §12 30/90 hygiene scheduler in hygiene.py, mirroring the
DigestScheduler shape; wires next to digest in main.py with the
same start/stop/run_tick test seam.
- Extend bot.delete_branch to accept actor=None for system gestures,
per §15.9 (actor_user_id=NULL, on_behalf_of=bot_login).
- Convert every branches/{branch} route in api_branches.py and
api_prs.py to {branch:path}; move the bare GET to the bottom of
the router so deeper GETs match before greedy-path swallow.
- Extend api_prs.py's _require_pr to accept pr_kind='meta_metadata'
so the §9.5 metadata-pane PRs land an in-app merge.
- Graduation rollback now deletes the graduate-<slug>-<6hex> branch
after closing the PR — §19.2 candidate that lands here.
- Email-bounce webhook gains a WEBHOOK_EMAIL_BOUNCE_SECRET seam.
- FakeGitea grows a DELETE /branches/{branch:path} handler and a
slashed-branch read; integration tests for the hygiene vertical
cover the 30d close, 90d delete, post-merge delete, pinned
exemption, per-user cursor preservation, no-notification rule,
and the graduation-rollback cleanup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
+76
-4
@@ -874,10 +874,12 @@ class Bot:
|
||||
slug: str,
|
||||
reason: str,
|
||||
) -> None:
|
||||
"""Undo of `open_graduation_pr`. Closes the PR without merging; the
|
||||
branch is left in place to dodge the case where another graduation
|
||||
attempt runs immediately — it'll get its own `graduate-<slug>-<hex>`
|
||||
suffix."""
|
||||
"""Undo of `open_graduation_pr`. Closes the PR without merging.
|
||||
The companion `delete_branch` call lives next to the rollback
|
||||
caller in `api_graduation.py` per the §19.2 'graduation rollback's
|
||||
branch cleanup' candidate Slice 8 settles — the §12 hygiene
|
||||
sweep would catch the branch eventually, but closing the loop
|
||||
on rollback avoids accumulation."""
|
||||
await self._gitea.close_pull(org, meta_repo, pr_number)
|
||||
_log(
|
||||
actor,
|
||||
@@ -888,6 +890,76 @@ class Bot:
|
||||
details={"reason": reason},
|
||||
)
|
||||
|
||||
# ----- §12 hygiene: branch deletion -----
|
||||
|
||||
async def delete_branch(
|
||||
self,
|
||||
actor: Actor | None,
|
||||
*,
|
||||
owner: str,
|
||||
repo: str,
|
||||
branch: str,
|
||||
slug: str | None,
|
||||
action_kind: str,
|
||||
reason: str,
|
||||
bot_login: str | None = None,
|
||||
) -> None:
|
||||
"""Per §12: the bot deletes a stale branch from Gitea.
|
||||
|
||||
Three callers: the §12 hygiene sweep (90-day boundary on
|
||||
meta-repo edit branches), the §10.7 90-day post-merge timer
|
||||
for per-RFC PR branches, and the graduation-rollback cleanup
|
||||
for `graduate-<slug>-<6hex>` per §19.2 "graduation rollback's
|
||||
branch cleanup."
|
||||
|
||||
For the timer paths the caller passes `actor=None`; the audit
|
||||
row lands with `actor_user_id=NULL` and `on_behalf_of=bot_login`
|
||||
per §15.9's "system-generated events" rule — "the app" in the
|
||||
noun slot. For the rollback case the human actor flows through
|
||||
the standard `_log` shape.
|
||||
|
||||
Idempotent against the Gitea API — 404 from a prior delete is
|
||||
swallowed so a retried sweep doesn't crash.
|
||||
"""
|
||||
try:
|
||||
await self._gitea.delete_branch(owner, repo, branch)
|
||||
except Exception as exc:
|
||||
from .gitea import GiteaError as _GE
|
||||
if isinstance(exc, _GE) and exc.status == 404:
|
||||
pass
|
||||
else:
|
||||
raise
|
||||
details = {"repo": f"{owner}/{repo}", "reason": reason}
|
||||
if actor is None:
|
||||
# System actor: write the audit row directly. Fan-out is
|
||||
# skipped — `delete_stale_branch` and `delete_post_merge_branch`
|
||||
# are intentionally absent from `notify._AUTO_WATCH_ACTIONS`
|
||||
# and `_ROUTING`, so no notification fires. The branches
|
||||
# being deleted are stale; the population that watched them
|
||||
# would be churn-grade noise per §15.4.
|
||||
db.conn().execute(
|
||||
"""
|
||||
INSERT INTO actions
|
||||
(actor_user_id, on_behalf_of, action_kind, rfc_slug, branch_name, details)
|
||||
VALUES (NULL, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
bot_login or "",
|
||||
action_kind,
|
||||
slug,
|
||||
branch,
|
||||
json.dumps(details),
|
||||
),
|
||||
)
|
||||
return
|
||||
_log(
|
||||
actor,
|
||||
action_kind,
|
||||
rfc_slug=slug,
|
||||
branch_name=branch,
|
||||
details=details,
|
||||
)
|
||||
|
||||
# ----- §13.1 claim PRs -----
|
||||
|
||||
async def open_claim_pr(
|
||||
|
||||
Reference in New Issue
Block a user