Page to the end when searching for an existing PR
find_pull_request read only the newest 50 per state, so once a repo passed that many pull requests a "not found" would mean "not in the newest page" — and the A3 reconciler would read that as "the PR was never opened" and open a duplicate. Exactly the side effect the intent log exists to prevent. It now pages until a short page proves the end, and raises rather than answering "absent" from an incomplete search; the reconciler turns that into unverifiable and gates to the human. Found while wiring CI: every CI run opens a PR in the sandbox, so the threshold would have been crossed in weeks rather than never. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2071e6ab6d
commit
8dd5e3eb15
2 files changed
+68
-23
No files matched your search
@@ -20,6 +20,9 @@ import httpx
|
||||
from nisse_dev.reconcile import Reconciler
|
||||
from nisse_dev.state import Intent
|
||||
|
||||
PR_PAGE_SIZE = 50
|
||||
MAX_PR_PAGES = 40 # 2000 PRs; beyond that, say so rather than guess
|
||||
|
||||
|
||||
class GiteaError(RuntimeError):
|
||||
pass
|
||||
@@ -92,16 +95,31 @@ class GiteaClient:
|
||||
|
||||
def find_pull_request(self, repo: str, head: str) -> PullRequest | None:
|
||||
"""External truth for A3: did the PR for this branch already get
|
||||
opened? Checks closed PRs too — a crash after opening must not be
|
||||
answered with "no PR is open, so open another"."""
|
||||
for state in ("open", "closed"):
|
||||
opened? Includes closed PRs — a crash after opening must not be
|
||||
answered with "no PR is open, so open another".
|
||||
|
||||
Pages to the end rather than sampling the first page: a "not found"
|
||||
that only means "not in the newest 50" would licence a duplicate PR.
|
||||
If the history is longer than this walk, raise instead of guessing —
|
||||
the reconciler turns that into "unverifiable" and gates to the human.
|
||||
"""
|
||||
for page in range(1, MAX_PR_PAGES + 1):
|
||||
data = self._request(
|
||||
"GET", f"/repos/{repo}/pulls", params={"state": state, "limit": 50}
|
||||
"GET",
|
||||
f"/repos/{repo}/pulls",
|
||||
params={"state": "all", "limit": PR_PAGE_SIZE, "page": page},
|
||||
)
|
||||
for item in data or []:
|
||||
items = data or []
|
||||
for item in items:
|
||||
if (item.get("head") or {}).get("ref") == head:
|
||||
return _pull_request(item)
|
||||
return None
|
||||
if len(items) < PR_PAGE_SIZE:
|
||||
return None # reached the end: definitively absent
|
||||
raise GiteaError(
|
||||
f"searched {MAX_PR_PAGES * PR_PAGE_SIZE} pull requests in {repo} "
|
||||
f"without reaching the end; cannot confirm whether {head!r} was "
|
||||
"already opened"
|
||||
)
|
||||
|
||||
def list_comments(
|
||||
self, repo: str, index: int, since: str | None = None
|
||||
|
||||
Reference in new issue
Block a user