"""Guarded merger adoption of an existing reviewer PR lease (#536). Replaces manual in-process ``_SESSION_LEASE`` seeding with an auditable, comment-backed adoption path for merger sessions that inherit a reviewer lease after formal approval at the current PR head. """ from __future__ import annotations from datetime import datetime, timezone from typing import Any import reviewer_pr_lease as leases ADOPTION_MARKER = "" DEFAULT_ADOPTION_REASON = "merger-handoff-approved-head" SOURCE_ADOPT = "gitea_adopt_merger_pr_lease" SOURCE_ACQUIRE = "gitea_acquire_reviewer_pr_lease" SOURCE_HEARTBEAT = "gitea_heartbeat_reviewer_pr_lease" SANCTIONED_PROVENANCE_SOURCES = frozenset({ SOURCE_ADOPT, SOURCE_ACQUIRE, SOURCE_HEARTBEAT, }) _MERGER_ADOPTABLE_FRESHNESS = frozenset({"active", "stale_warning"}) def format_adoption_body( *, repo: str, pr_number: int, issue_number: int | None, adopter_identity: str, adopter_profile: str, adopter_session_id: str, worktree: str, candidate_head: str | None, target_branch: str, target_branch_sha: str | None, adopted_from_session_id: str, adopted_from_profile: str, adopted_from_reviewer_identity: str, adopted_from_comment_id: int | None, adoption_reason: str = DEFAULT_ADOPTION_REASON, adopted_at: datetime | None = None, ) -> str: """Render a durable merger adoption proof comment.""" adopted_at = adopted_at or datetime.now(timezone.utc) adopted_text = adopted_at.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace( "+00:00", "Z" ) lease_body = leases.format_lease_body( repo=repo, pr_number=pr_number, issue_number=issue_number, reviewer_identity=adopter_identity, profile=adopter_profile, session_id=adopter_session_id, worktree=worktree, phase="adopted", candidate_head=candidate_head, target_branch=target_branch, target_branch_sha=target_branch_sha, last_activity=adopted_at, blocker="none", ) lines = [ ADOPTION_MARKER, f"adopted_at: {adopted_text}", f"adopted_by_identity: {adopter_identity}", f"adopted_by_profile: {adopter_profile}", f"adopted_from_session_id: {adopted_from_session_id}", f"adopted_from_profile: {adopted_from_profile}", f"adopted_from_reviewer_identity: {adopted_from_reviewer_identity}", f"adopted_from_comment_id: {adopted_from_comment_id or 'none'}", f"adoption_reason: {adoption_reason}", lease_body, ] return "\n".join(lines) def is_adoption_comment(body: str) -> bool: return ADOPTION_MARKER in (body or "") def build_lease_provenance( *, source: str, comment_id: int | None = None, adopted_from_session_id: str | None = None, adopted_from_profile: str | None = None, adopted_from_reviewer_identity: str | None = None, adoption_reason: str | None = None, recorded_at: datetime | None = None, ) -> dict[str, Any]: recorded_at = recorded_at or datetime.now(timezone.utc) recorded_text = recorded_at.astimezone(timezone.utc).replace(microsecond=0).isoformat().replace( "+00:00", "Z" ) proof = { "source": source, "recorded_at": recorded_text, } if comment_id is not None: proof["comment_id"] = comment_id if adopted_from_session_id: proof["adopted_from_session_id"] = adopted_from_session_id if adopted_from_profile: proof["adopted_from_profile"] = adopted_from_profile if adopted_from_reviewer_identity: proof["adopted_from_reviewer_identity"] = adopted_from_reviewer_identity if adoption_reason: proof["adoption_reason"] = adoption_reason return proof def is_sanctioned_session_lease(session: dict[str, Any] | None) -> bool: if not session: return False provenance = session.get("lease_provenance") or {} source = (provenance.get("source") or "").strip() if source not in SANCTIONED_PROVENANCE_SOURCES: return False if source == SOURCE_ADOPT: return bool(provenance.get("comment_id")) and bool( provenance.get("adopted_from_session_id") ) if source in {SOURCE_ACQUIRE, SOURCE_HEARTBEAT}: return bool(session.get("comment_id") or provenance.get("comment_id")) return False def assess_adopt_merger_lease( comments: list[dict], *, pr_number: int, adopter_identity: str, adopter_profile: str, adopter_session_id: str, repo: str, issue_number: int | None, worktree: str, expected_head_sha: str | None, live_head_sha: str | None, approval_at_head: bool, pr_open: bool = True, now: datetime | None = None, ) -> dict[str, Any]: """Decide whether a merger may adopt the active reviewer lease (#536).""" now = now or datetime.now(timezone.utc) reasons: list[str] = [] pinned = leases._normalize_sha(expected_head_sha) live = leases._normalize_sha(live_head_sha) if not pr_open: reasons.append( f"PR #{pr_number} is not open; merger lease adoption is only for open PRs" ) if not approval_at_head: reasons.append( "formal APPROVED review at the current PR head is required before " "merger lease adoption (fail closed)" ) if not pinned: reasons.append("expected_head_sha is required for merger lease adoption") if not live: reasons.append("live PR head SHA unavailable (fail closed)") elif pinned and live and pinned != live: reasons.append( "expected_head_sha does not match live PR head; refresh approval before adoption" ) active = leases.find_active_reviewer_lease( comments, pr_number=pr_number, now=now ) if not active: reasons.append( f"no active reviewer lease on PR #{pr_number} to adopt; reviewer " "must acquire first" ) else: owner_session = (active.get("session_id") or "").strip() freshness = active.get("freshness") or leases.classify_lease_freshness( active, now=now ) if freshness not in _MERGER_ADOPTABLE_FRESHNESS: reasons.append( f"active reviewer lease freshness is '{freshness}'; explicit " "reclaim is not implemented (fail closed)" ) lease_head = active.get("candidate_head") if lease_head and live and lease_head != live: reasons.append( "active reviewer lease candidate_head differs from live PR head" ) if owner_session and owner_session == adopter_session_id: # Same session already holds the thread lease — allow idempotent adopt # only when the newest entry is not yet an adoption by this session. entries = leases._lease_entries(comments, pr_number=pr_number) newest = entries[-1] if entries else None if newest and (newest.get("phase") or "") == "adopted": reasons.append( "merger session already recorded an adoption lease on this PR" ) elif owner_session and owner_session != adopter_session_id: # Cross-session merger handoff: adopt the foreign reviewer lease. pass if not (adopter_identity or "").strip(): reasons.append("adopter identity required") if not (adopter_session_id or "").strip(): reasons.append("adopter session_id required") if not (worktree or "").strip(): reasons.append("merger worktree path required") if "merger" not in (adopter_profile or "").lower(): reasons.append( f"profile '{adopter_profile}' is not a merger profile; adoption is " "merger-only (fail closed)" ) adopt_allowed = not reasons adoption_body = None if adopt_allowed and active: adoption_body = format_adoption_body( repo=repo, pr_number=pr_number, issue_number=issue_number or active.get("issue_number"), adopter_identity=adopter_identity, adopter_profile=adopter_profile, adopter_session_id=adopter_session_id, worktree=worktree, candidate_head=live, target_branch=active.get("target_branch") or "master", target_branch_sha=active.get("target_branch_sha"), adopted_from_session_id=active.get("session_id") or "", adopted_from_profile=active.get("profile") or "unknown", adopted_from_reviewer_identity=active.get("reviewer_identity") or "", adopted_from_comment_id=active.get("comment_id"), adopted_at=now, ) return { "adopt_allowed": adopt_allowed, "reasons": reasons, "active_lease": active, "adoption_body": adoption_body, "adopter_session_id": adopter_session_id, "expected_head_sha": pinned, "live_head_sha": live, }