mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(ci): use gh api --slurp to parse paginated issues in close_duplicate_issues.py
The Check Duplicate Issues workflow crashed on every issues event with json.decoder.JSONDecodeError. fetch_open_issues split gh api --paginate output with splitlines() and parsed each line as JSON, but gh concatenates pages without guaranteeing each physical line is a self-contained JSON document, and splitlines() also breaks on unescaped Unicode line separators (U+2028/U+2029) that GitHub returns raw inside issue bodies. Fetch with --slurp instead, which emits one well-formed JSON array of pages, and flatten it. Fixes #32217
This commit is contained in:
parent
5b93ba0ada
commit
069cc9a01f
2 changed files with 79 additions and 14 deletions
18
.github/scripts/close_duplicate_issues.py
vendored
18
.github/scripts/close_duplicate_issues.py
vendored
|
|
@ -40,27 +40,17 @@ def gh(*args: str) -> str:
|
|||
|
||||
|
||||
def fetch_open_issues(repo: str | None) -> list[dict]:
|
||||
"""Fetch all open issues (excluding PRs) via gh api --paginate."""
|
||||
"""Fetch all open issues (excluding PRs) via gh api --paginate --slurp."""
|
||||
if repo:
|
||||
endpoint = (
|
||||
f"repos/{repo}/issues?state=open&per_page=100&sort=created&direction=asc"
|
||||
)
|
||||
else:
|
||||
endpoint = "repos/{owner}/{repo}/issues?state=open&per_page=100&sort=created&direction=asc"
|
||||
cmd = ["api", "--paginate", endpoint]
|
||||
|
||||
raw = gh(*cmd)
|
||||
# gh --paginate concatenates JSON arrays, so we may get multiple arrays
|
||||
issues = []
|
||||
for line in raw.strip().splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parsed = json.loads(line)
|
||||
if isinstance(parsed, list):
|
||||
issues.extend(parsed)
|
||||
else:
|
||||
issues.append(parsed)
|
||||
raw = gh("api", "--paginate", "--slurp", endpoint)
|
||||
pages = json.loads(raw)
|
||||
issues = [issue for page in pages for issue in page]
|
||||
|
||||
# Filter out pull requests (they also appear in the issues endpoint)
|
||||
return [i for i in issues if "pull_request" not in i]
|
||||
|
|
|
|||
75
tests/test_litellm/test_github_close_duplicate_issues.py
Normal file
75
tests/test_litellm/test_github_close_duplicate_issues.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
SCRIPT_PATH = Path(__file__).resolve().parents[2] / ".github" / "scripts" / "close_duplicate_issues.py"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def closer_module():
|
||||
spec = importlib.util.spec_from_file_location("close_duplicate_issues", SCRIPT_PATH)
|
||||
assert spec and spec.loader, f"Could not load spec for {SCRIPT_PATH}"
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules["close_duplicate_issues"] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def _issue(number: int, title: str, body: str = "") -> dict:
|
||||
return {"number": number, "title": title, "body": body}
|
||||
|
||||
|
||||
def _fake_gh_cli(pages: list[list[dict]]):
|
||||
def fake_gh(*args: str) -> str:
|
||||
assert args[0] == "api"
|
||||
assert "--paginate" in args
|
||||
if "--slurp" in args:
|
||||
return json.dumps(pages, ensure_ascii=False)
|
||||
return "".join(json.dumps(page, ensure_ascii=False) for page in pages)
|
||||
|
||||
return fake_gh
|
||||
|
||||
|
||||
class TestFetchOpenIssues:
|
||||
def test_should_parse_multi_page_payload_with_unescaped_line_separators(self, closer_module, monkeypatch):
|
||||
pages = [
|
||||
[_issue(1, "first bug", body="traceback\u2028line two\u2028line three")],
|
||||
[_issue(2, "second bug"), _issue(3, "third bug")],
|
||||
]
|
||||
monkeypatch.setattr(closer_module, "gh", _fake_gh_cli(pages))
|
||||
|
||||
issues = closer_module.fetch_open_issues("BerriAI/litellm")
|
||||
|
||||
assert [i["number"] for i in issues] == [1, 2, 3]
|
||||
|
||||
def test_should_request_slurp_mode(self, closer_module, monkeypatch):
|
||||
calls: list[tuple[str, ...]] = []
|
||||
|
||||
def recording_gh(*args: str) -> str:
|
||||
calls.append(args)
|
||||
return json.dumps([[]])
|
||||
|
||||
monkeypatch.setattr(closer_module, "gh", recording_gh)
|
||||
|
||||
closer_module.fetch_open_issues("BerriAI/litellm")
|
||||
|
||||
assert len(calls) == 1
|
||||
assert "--slurp" in calls[0]
|
||||
|
||||
def test_should_filter_out_pull_requests(self, closer_module, monkeypatch):
|
||||
pages = [
|
||||
[
|
||||
_issue(1, "real issue"),
|
||||
{**_issue(2, "a pr"), "pull_request": {"url": "https://x"}},
|
||||
]
|
||||
]
|
||||
monkeypatch.setattr(closer_module, "gh", _fake_gh_cli(pages))
|
||||
|
||||
issues = closer_module.fetch_open_issues("BerriAI/litellm")
|
||||
|
||||
assert [i["number"] for i in issues] == [1]
|
||||
Loading…
Add table
Reference in a new issue