Merge pull request 'feat: guarded merger lease adoption flow (#536)' (#538) from feat/issue-536-merger-lease-adoption into master
This commit was merged in pull request #538.
This commit is contained in:
+188
-2
@@ -751,6 +751,7 @@ import root_checkout_guard # noqa: E402
|
||||
import issue_claim_heartbeat # noqa: E402
|
||||
import issue_work_duplicate_gate # noqa: E402
|
||||
import reviewer_pr_lease # noqa: E402
|
||||
import merger_lease_adoption # noqa: E402
|
||||
import merged_cleanup_reconcile # noqa: E402
|
||||
import reconciler_profile # noqa: E402
|
||||
import reconciliation_workflow # noqa: E402
|
||||
@@ -5459,7 +5460,10 @@ def gitea_acquire_reviewer_pr_lease(
|
||||
"target_branch_sha": target_branch_sha,
|
||||
"repo": repo_label,
|
||||
"comment_id": posted.get("id"),
|
||||
})
|
||||
}, lease_provenance=merger_lease_adoption.build_lease_provenance(
|
||||
source=merger_lease_adoption.SOURCE_ACQUIRE,
|
||||
comment_id=posted.get("id"),
|
||||
))
|
||||
return {
|
||||
"success": True,
|
||||
"acquired": True,
|
||||
@@ -5471,6 +5475,176 @@ def gitea_acquire_reviewer_pr_lease(
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_adopt_merger_pr_lease(
|
||||
pr_number: int,
|
||||
worktree: str,
|
||||
expected_head_sha: str,
|
||||
issue_number: int | None = None,
|
||||
session_id: str | None = None,
|
||||
remote: str = "dadeschools",
|
||||
host: str | None = None,
|
||||
org: str | None = None,
|
||||
repo: str | None = None,
|
||||
) -> dict:
|
||||
"""Adopt an active reviewer PR lease for a merger session (#536).
|
||||
|
||||
Merger sessions must not manually seed ``reviewer_pr_lease._SESSION_LEASE``.
|
||||
This tool posts durable adoption proof on the PR thread and records
|
||||
sanctioned in-session lease provenance so ``gitea_merge_pr`` can proceed
|
||||
after a separate reviewer session held the lease.
|
||||
|
||||
Args:
|
||||
pr_number: Open PR to adopt.
|
||||
worktree: Merger workspace under ``branches/``.
|
||||
expected_head_sha: Approved head the merger will pin during merge.
|
||||
issue_number: Optional linked issue for the adoption record.
|
||||
session_id: Optional merger session id (generated when omitted).
|
||||
remote: Known instance — 'dadeschools' or 'prgs'.
|
||||
host: Override the Gitea host.
|
||||
org: Override the owner/organization.
|
||||
repo: Override the repository name.
|
||||
|
||||
Returns:
|
||||
dict with adoption proof, session lease, and block reasons when the
|
||||
guarded preconditions are not met.
|
||||
"""
|
||||
read_block = _profile_operation_gate("gitea.read")
|
||||
if read_block:
|
||||
return {
|
||||
"success": False,
|
||||
"adopted": False,
|
||||
"reasons": read_block,
|
||||
"permission_report": _permission_block_report("gitea.read"),
|
||||
}
|
||||
comment_block = _profile_operation_gate("gitea.pr.comment")
|
||||
if comment_block:
|
||||
return {
|
||||
"success": False,
|
||||
"adopted": False,
|
||||
"reasons": comment_block,
|
||||
"permission_report": _permission_block_report("gitea.pr.comment"),
|
||||
}
|
||||
merge_block = _profile_operation_gate("gitea.pr.merge")
|
||||
if merge_block:
|
||||
return {
|
||||
"success": False,
|
||||
"adopted": False,
|
||||
"reasons": merge_block,
|
||||
"permission_report": _permission_block_report("gitea.pr.merge"),
|
||||
}
|
||||
|
||||
_verify_role_mutation_workspace(
|
||||
remote, worktree=worktree, task="adopt_merger_pr_lease"
|
||||
)
|
||||
verify_preflight_purity(remote, task="adopt_merger_pr_lease")
|
||||
h, o, r = _resolve(remote, host, org, repo)
|
||||
auth = _auth(h)
|
||||
profile = get_profile()
|
||||
profile_name = profile.get("profile_name") or "unknown"
|
||||
identity = _authenticated_username(remote) or profile.get("username") or ""
|
||||
sid = (session_id or "").strip() or reviewer_pr_lease.new_session_id()
|
||||
repo_label = f"{o}/{r}"
|
||||
|
||||
pr_live = api_request(
|
||||
"GET", f"{repo_api_url(h, o, r)}/pulls/{pr_number}", auth) or {}
|
||||
pr_state = (pr_live.get("state") or "").strip().lower()
|
||||
pr_open = pr_state == "open" and not pr_live.get("merged")
|
||||
live_head = (pr_live.get("head", {}) or {}).get("sha") or pr_live.get(
|
||||
"head_sha"
|
||||
)
|
||||
|
||||
feedback = gitea_get_pr_review_feedback(
|
||||
pr_number=pr_number,
|
||||
remote=remote,
|
||||
host=host,
|
||||
org=org,
|
||||
repo=repo,
|
||||
)
|
||||
approval_at_head = bool(feedback.get("approval_at_current_head"))
|
||||
|
||||
comments = _fetch_pr_comments(
|
||||
pr_number, remote=remote, host=host, org=org, repo=repo)
|
||||
assessment = merger_lease_adoption.assess_adopt_merger_lease(
|
||||
comments,
|
||||
pr_number=pr_number,
|
||||
adopter_identity=identity,
|
||||
adopter_profile=profile_name,
|
||||
adopter_session_id=sid,
|
||||
repo=repo_label,
|
||||
issue_number=issue_number,
|
||||
worktree=worktree,
|
||||
expected_head_sha=expected_head_sha,
|
||||
live_head_sha=live_head,
|
||||
approval_at_head=approval_at_head,
|
||||
pr_open=pr_open,
|
||||
)
|
||||
if not assessment.get("adopt_allowed"):
|
||||
return {
|
||||
"success": False,
|
||||
"adopted": False,
|
||||
"pr_number": pr_number,
|
||||
"reasons": assessment.get("reasons") or [],
|
||||
"active_lease": assessment.get("active_lease"),
|
||||
"expected_head_sha": expected_head_sha,
|
||||
"live_head_sha": live_head,
|
||||
}
|
||||
|
||||
active = assessment.get("active_lease") or {}
|
||||
body = assessment["adoption_body"]
|
||||
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": "adopt_merger_pr_lease"},
|
||||
):
|
||||
posted = api_request("POST", comment_url, auth, {"body": body})
|
||||
|
||||
provenance = merger_lease_adoption.build_lease_provenance(
|
||||
source=merger_lease_adoption.SOURCE_ADOPT,
|
||||
comment_id=posted.get("id"),
|
||||
adopted_from_session_id=active.get("session_id"),
|
||||
adopted_from_profile=active.get("profile"),
|
||||
adopted_from_reviewer_identity=active.get("reviewer_identity"),
|
||||
adoption_reason=merger_lease_adoption.DEFAULT_ADOPTION_REASON,
|
||||
)
|
||||
session_lease = reviewer_pr_lease.record_session_lease({
|
||||
"pr_number": pr_number,
|
||||
"issue_number": issue_number or active.get("issue_number"),
|
||||
"session_id": sid,
|
||||
"reviewer_identity": identity,
|
||||
"profile": profile_name,
|
||||
"worktree": worktree,
|
||||
"phase": "adopted",
|
||||
"candidate_head": live_head,
|
||||
"target_branch": active.get("target_branch") or "master",
|
||||
"target_branch_sha": active.get("target_branch_sha"),
|
||||
"repo": repo_label,
|
||||
"comment_id": posted.get("id"),
|
||||
}, lease_provenance=provenance)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"adopted": True,
|
||||
"pr_number": pr_number,
|
||||
"session_id": sid,
|
||||
"adoption_comment_id": posted.get("id"),
|
||||
"adopted_from_session_id": active.get("session_id"),
|
||||
"adopted_from_profile": active.get("profile"),
|
||||
"adopted_from_reviewer_identity": active.get("reviewer_identity"),
|
||||
"adoption_reason": merger_lease_adoption.DEFAULT_ADOPTION_REASON,
|
||||
"expected_head_sha": expected_head_sha,
|
||||
"live_head_sha": live_head,
|
||||
"session_lease": session_lease,
|
||||
"lease_provenance": provenance,
|
||||
"reasons": [],
|
||||
}
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def gitea_heartbeat_reviewer_pr_lease(
|
||||
pr_number: int,
|
||||
@@ -5531,6 +5705,17 @@ def gitea_heartbeat_reviewer_pr_lease(
|
||||
):
|
||||
posted = api_request("POST", comment_url, auth, {"body": body})
|
||||
|
||||
prior_provenance = (session.get("lease_provenance") or {}).copy()
|
||||
heartbeat_provenance = merger_lease_adoption.build_lease_provenance(
|
||||
source=merger_lease_adoption.SOURCE_HEARTBEAT,
|
||||
comment_id=posted.get("id"),
|
||||
adopted_from_session_id=prior_provenance.get("adopted_from_session_id"),
|
||||
adopted_from_profile=prior_provenance.get("adopted_from_profile"),
|
||||
adopted_from_reviewer_identity=prior_provenance.get(
|
||||
"adopted_from_reviewer_identity"
|
||||
),
|
||||
adoption_reason=prior_provenance.get("adoption_reason"),
|
||||
)
|
||||
updated = reviewer_pr_lease.record_session_lease({
|
||||
**session,
|
||||
"phase": phase,
|
||||
@@ -5538,7 +5723,8 @@ def gitea_heartbeat_reviewer_pr_lease(
|
||||
"candidate_head": candidate_head or session.get("candidate_head"),
|
||||
"target_branch_sha": target_branch_sha or session.get("target_branch_sha"),
|
||||
"last_comment_id": posted.get("id"),
|
||||
})
|
||||
"comment_id": posted.get("id"),
|
||||
}, lease_provenance=heartbeat_provenance)
|
||||
return {
|
||||
"success": True,
|
||||
"posted": True,
|
||||
|
||||
Reference in New Issue
Block a user