Compare commits

...

1 Commits

Author SHA1 Message Date
Molecule AI Dev Engineer A (Kimi) 9c47108e02 fix(ci-drift): paginate open issues to prevent duplicate [ci-drift] issues
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
CI / Detect changes (pull_request) Successful in 5s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 6s
CI / Python Lint & Test (pull_request) Successful in 6s
E2E API Smoke Test / detect-changes (pull_request) Successful in 5s
E2E Chat / detect-changes (pull_request) Successful in 5s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 6s
sop-checklist / review-refire (pull_request_target) Has been skipped
Handlers Postgres Integration / detect-changes (pull_request) Successful in 7s
CI / Platform (Go) (pull_request) Successful in 2s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 8s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 8s
gate-check-v3 / gate-check (pull_request_target) Successful in 6s
CI / Canvas (Next.js) (pull_request) Successful in 2s
qa-review / approved (pull_request_target) Failing after 5s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 1s
CI / Canvas Deploy Status (pull_request) Has been skipped
sop-tier-check / tier-check (pull_request_target) Failing after 7s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 2s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 1s
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Successful in 14s
sop-checklist / all-items-acked (pull_request) [info tier:low] acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4 — body-unfilled: comprehensive-testing, l
sop-checklist / na-declarations (pull_request) N/A: (none)
security-review / approved (pull_request_target) Failing after 18s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 21s
sop-checklist / all-items-acked (pull_request_target) Successful in 19s
CI / all-required (pull_request) Successful in 10s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 17s
E2E Chat / E2E Chat (pull_request) Successful in 16s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m0s
Ops Scripts Tests / Ops scripts (unittest) (pull_request) Successful in 1m22s
qa-review / approved (pull_request_review) Has been skipped
security-review / approved (pull_request_review) Has been skipped
sop-tier-check / tier-check (pull_request_review) Successful in 11s
audit-force-merge / audit (pull_request_target) Successful in 7s
The find_open_issue() helper only fetched one page of open issues
(limit=50). When the existing [ci-drift] tracking issue fell beyond
page 1, the caller POSTed a duplicate instead of updating the existing
one.

Fix: loop over pages until the title is found or the result set is
exhausted (page returns <50 items). API errors propagate loud via
ApiError as before.

Adds two regression tests:
- test_find_open_issue_paginates_to_page_2
- test_find_open_issue_stops_at_last_page

Fixes issue-403.
2026-06-06 18:44:10 +00:00
2 changed files with 74 additions and 15 deletions
+26 -15
View File
@@ -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:
+48
View File
@@ -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
# --------------------------------------------------------------------------