fix: resolve reviewer preflight capability/lease deadlock and add gitea_release_reviewer_pr_lease tool (closes #546)
This commit is contained in:
+122
-1
@@ -2409,10 +2409,18 @@ def _review_decision_session_reasons(lock: dict | None) -> list[str]:
|
||||
return reasons
|
||||
|
||||
|
||||
def init_review_decision_lock(remote: str | None, task: str | None):
|
||||
def init_review_decision_lock(remote: str | None, task: str | None, force: bool = True):
|
||||
"""Seed read-only-until-ready state for reviewer PR review tasks."""
|
||||
if task != "review_pr":
|
||||
return
|
||||
if not force:
|
||||
lock = _load_review_decision_lock()
|
||||
if lock is not None:
|
||||
if lock.get("remote") == remote and lock.get("session_pid") == os.getpid():
|
||||
env_lock = (os.environ.get(SESSION_PROFILE_LOCK_ENV) or "").strip()
|
||||
stored_lock = (lock.get("session_profile_lock") or "").strip()
|
||||
if not env_lock or not stored_lock or env_lock == stored_lock:
|
||||
return
|
||||
review_workflow_load.clear_review_workflow_load()
|
||||
profile = get_profile()
|
||||
profile_name = (profile.get("profile_name") or "").strip()
|
||||
@@ -5939,6 +5947,118 @@ def gitea_cleanup_post_merge_moot_lease(
|
||||
return report
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_release_reviewer_pr_lease(
|
||||
pr_number: int,
|
||||
worktree: str | None = None,
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
) -> dict:
|
||||
"""Safely release an active reviewer lease owned by the current session on an open PR.
|
||||
|
||||
This provides a canonical way to clean up reviewer leases when a review fails
|
||||
before submission.
|
||||
|
||||
Args:
|
||||
pr_number: The PR number whose reviewer lease to release.
|
||||
worktree: Optional worktree path (resolves automatically if not supplied).
|
||||
remote: Known instance — 'dadeschools' or 'prgs'.
|
||||
host: Override the Gitea host.
|
||||
org: Override the owner/organization.
|
||||
repo: Override the repository name.
|
||||
|
||||
Returns:
|
||||
dict reporting release status.
|
||||
"""
|
||||
_verify_role_mutation_workspace(
|
||||
remote, worktree=worktree, task="review_pr"
|
||||
)
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
auth = _auth(h)
|
||||
|
||||
comments = _fetch_pr_comments(
|
||||
pr_number, remote=remote, host=host, org=org, repo=repo
|
||||
)
|
||||
active = reviewer_pr_lease.find_active_reviewer_lease(
|
||||
comments, pr_number=pr_number
|
||||
)
|
||||
|
||||
if not active:
|
||||
return {
|
||||
"success": True,
|
||||
"released": False,
|
||||
"reasons": ["no active reviewer lease found on PR"],
|
||||
}
|
||||
|
||||
identity = _authenticated_username(remote) or ""
|
||||
session = reviewer_pr_lease.get_session_lease() or {}
|
||||
session_id = session.get("session_id")
|
||||
|
||||
owner_identity = active.get("reviewer_identity")
|
||||
owner_session_id = active.get("session_id")
|
||||
|
||||
# Authorize release: identity matches, session_id matches, or lease is expired/reclaimable
|
||||
authorized = False
|
||||
reasons = []
|
||||
if owner_identity and identity and owner_identity == identity:
|
||||
authorized = True
|
||||
elif owner_session_id and session_id and owner_session_id == session_id:
|
||||
authorized = True
|
||||
else:
|
||||
freshness = reviewer_pr_lease.classify_lease_freshness(active)
|
||||
if freshness in {"expired", "reclaimable"}:
|
||||
authorized = True
|
||||
|
||||
if not authorized:
|
||||
return {
|
||||
"success": False,
|
||||
"released": False,
|
||||
"reasons": [
|
||||
f"unauthorized to release active lease: owned by {owner_identity} "
|
||||
f"(session {owner_session_id})"
|
||||
],
|
||||
}
|
||||
|
||||
# Format and post release comment
|
||||
body = reviewer_pr_lease.format_lease_body(
|
||||
repo=active.get("repo") or f"{o}/{r}",
|
||||
pr_number=pr_number,
|
||||
issue_number=active.get("issue_number"),
|
||||
reviewer_identity=owner_identity or identity,
|
||||
profile=active.get("profile") or "reviewer",
|
||||
session_id=owner_session_id or session_id or "unknown",
|
||||
worktree=active.get("worktree") or "",
|
||||
phase="released",
|
||||
candidate_head=active.get("candidate_head"),
|
||||
target_branch=active.get("target_branch") or "master",
|
||||
target_branch_sha=active.get("target_branch_sha"),
|
||||
blocker="manual-release",
|
||||
)
|
||||
|
||||
comment_url = f"{repo_api_url(h, o, r)}/issues/{pr_number}/comments"
|
||||
with _audited(
|
||||
"comment_pr",
|
||||
host=h,
|
||||
remote=remote,
|
||||
org=o,
|
||||
repo=r,
|
||||
pr_number=pr_number,
|
||||
request_metadata={"source": "release_reviewer_pr_lease"},
|
||||
):
|
||||
posted = api_request("POST", comment_url, auth, {"body": body})
|
||||
|
||||
reviewer_pr_lease.clear_session_lease()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"released": True,
|
||||
"comment_id": posted.get("id"),
|
||||
"reasons": [],
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_list_issue_comments(
|
||||
issue_number: int,
|
||||
@@ -8491,6 +8611,7 @@ def gitea_resolve_task_capability(
|
||||
init_review_decision_lock(
|
||||
remote if remote in REMOTES else None,
|
||||
task,
|
||||
force=False,
|
||||
)
|
||||
|
||||
record_mutation_authority(profile["profile_name"], username, remote if remote in REMOTES else None, task)
|
||||
|
||||
Reference in New Issue
Block a user