fix(ci-drift): paginate open issues to prevent duplicate [ci-drift] issues #2373
@@ -552,23 +552,34 @@ def find_open_issue(title: str) -> dict | None:
|
||||
hourly; failing one cycle loudly is strictly better than silently
|
||||
duplicating.
|
||||
|
||||
Gitea issue search returns at most page=50 per page; one page is
|
||||
enough as long as `[ci-drift]` issues are a tiny minority. (See
|
||||
follow-up issue for Link-header pagination.)
|
||||
Paginates through all open issues (limit=50 per page) until the
|
||||
title is found or the result set is exhausted. Previously only one
|
||||
page was fetched, causing duplicate [ci-drift] issues when the
|
||||
existing tracking issue fell beyond page 1.
|
||||
"""
|
||||
_, results = api(
|
||||
"GET",
|
||||
f"/repos/{OWNER}/{NAME}/issues",
|
||||
query={"state": "open", "type": "issues", "limit": "50"},
|
||||
)
|
||||
if not isinstance(results, list):
|
||||
raise ApiError(
|
||||
f"issue search returned non-list body (got {type(results).__name__})"
|
||||
page = 1
|
||||
while True:
|
||||
_, results = api(
|
||||
"GET",
|
||||
f"/repos/{OWNER}/{NAME}/issues",
|
||||
query={
|
||||
"state": "open",
|
||||
"type": "issues",
|
||||
"limit": "50",
|
||||
"page": str(page),
|
||||
},
|
||||
)
|
||||
for issue in results:
|
||||
if issue.get("title") == title:
|
||||
return issue
|
||||
return None
|
||||
if not isinstance(results, list):
|
||||
raise ApiError(
|
||||
f"issue search returned non-list body (got {type(results).__name__})"
|
||||
)
|
||||
for issue in results:
|
||||
if issue.get("title") == title:
|
||||
return issue
|
||||
# Fewer than limit results means last page reached.
|
||||
if len(results) < 50:
|
||||
return None
|
||||
page += 1
|
||||
|
||||
|
||||
def render_body(branch: str, findings: list[str], debug: dict) -> str:
|
||||
|
||||
@@ -584,6 +584,54 @@ def test_find_open_issue_raises_on_transient_error(drift_module, monkeypatch):
|
||||
drift_module.find_open_issue("[ci-drift] foo")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Pagination: search beyond page 1 so an existing issue on any page is found
|
||||
# --------------------------------------------------------------------------
|
||||
def test_find_open_issue_paginates_to_page_2(drift_module, monkeypatch):
|
||||
"""Issue exists on page 2 → paginate and find it."""
|
||||
target = {"number": 99, "title": "[ci-drift] foo"}
|
||||
filler = [{"number": i, "title": f"other-{i}"} for i in range(1, 51)]
|
||||
|
||||
class PaginatedStub:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def __call__(self, method, path, *, body=None, query=None, expect_json=True):
|
||||
self.calls.append((method, path, body, query))
|
||||
page = int((query or {}).get("page", "1"))
|
||||
if page == 1:
|
||||
return 200, filler
|
||||
if page == 2:
|
||||
return 200, [target]
|
||||
return 200, []
|
||||
|
||||
stub = PaginatedStub()
|
||||
monkeypatch.setattr(drift_module, "api", stub)
|
||||
assert drift_module.find_open_issue("[ci-drift] foo") == target
|
||||
assert len(stub.calls) == 2
|
||||
|
||||
|
||||
def test_find_open_issue_stops_at_last_page(drift_module, monkeypatch):
|
||||
"""No match across pages → stop when a page has <50 results."""
|
||||
filler = [{"number": i, "title": f"other-{i}"} for i in range(1, 51)]
|
||||
|
||||
class PaginatedStub:
|
||||
def __init__(self):
|
||||
self.calls = []
|
||||
|
||||
def __call__(self, method, path, *, body=None, query=None, expect_json=True):
|
||||
self.calls.append((method, path, body, query))
|
||||
page = int((query or {}).get("page", "1"))
|
||||
if page == 1:
|
||||
return 200, filler
|
||||
return 200, []
|
||||
|
||||
stub = PaginatedStub()
|
||||
monkeypatch.setattr(drift_module, "api", stub)
|
||||
assert drift_module.find_open_issue("[ci-drift] foo") is None
|
||||
assert len(stub.calls) == 2
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Idempotent path: existing issue is PATCHed, NOT duplicated
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user