diff --git a/.github/scripts/close_low_quality_prs.py b/.github/scripts/close_low_quality_prs.py index d7363f88328..81607c57332 100644 --- a/.github/scripts/close_low_quality_prs.py +++ b/.github/scripts/close_low_quality_prs.py @@ -78,6 +78,12 @@ def gh(*args: str) -> str: return result.stdout +# `gh pr list --limit` caps at 1000 (the CLI's documented hard ceiling). +# Surface a warning if we ever hit that cap so the silent truncation is +# visible in workflow logs instead of just being a missed close. +GH_PR_LIST_LIMIT = 1000 + + def fetch_open_prs(repo: str | None) -> list[dict]: """Fetch all open PRs (number, createdAt, isDraft, labels, author). @@ -94,12 +100,22 @@ def fetch_open_prs(repo: str | None) -> list[dict]: "--state", "open", "--limit", - "1000", + str(GH_PR_LIST_LIMIT), "--json", fields, *repo_args, ) - return json.loads(raw) + prs = json.loads(raw) + if len(prs) >= GH_PR_LIST_LIMIT: + # `gh pr list --limit N` returns at most N rows even if more exist; + # log a GitHub Actions warning so the truncation isn't silent. + message = ( + f"fetch_open_prs hit the gh CLI cap ({GH_PR_LIST_LIMIT}); " + "the open-PR list is likely truncated. Switch to paginated " + "`gh api` calls if the repo regularly exceeds this cap." + ) + print(f"::warning::{message}", file=sys.stderr) + return prs def fetch_pr_author_association(pr_number: int, repo: str | None) -> str: diff --git a/tests/test_litellm/test_github_close_low_quality_prs.py b/tests/test_litellm/test_github_close_low_quality_prs.py index 141fb7f8c02..7050683b681 100644 --- a/tests/test_litellm/test_github_close_low_quality_prs.py +++ b/tests/test_litellm/test_github_close_low_quality_prs.py @@ -422,6 +422,41 @@ class TestMainOptoutLabelDefault: assert default not in captured["optout_labels"], default +class TestFetchOpenPrsLimitWarning: + """`fetch_open_prs` must surface a warning when the gh CLI cap is hit.""" + + def test_should_warn_when_at_cap(self, closer_module, monkeypatch, capsys): + # Pretend `gh pr list --limit 1000` returned exactly 1000 PRs — + # this is the silent-truncation case the warning is meant to catch. + cap = closer_module.GH_PR_LIST_LIMIT + synthetic = [{"number": i} for i in range(cap)] + import json as _json + + monkeypatch.setattr( + closer_module, "gh", lambda *a, **kw: _json.dumps(synthetic) + ) + result = closer_module.fetch_open_prs(None) + assert len(result) == cap + captured = capsys.readouterr() + # GitHub Actions `::warning::` annotations go to stderr by + # convention; just check the marker appears somewhere visible. + combined = captured.out + captured.err + assert "::warning::" in combined + assert str(cap) in combined + + def test_should_not_warn_when_under_cap(self, closer_module, monkeypatch, capsys): + synthetic = [{"number": i} for i in range(5)] + import json as _json + + monkeypatch.setattr( + closer_module, "gh", lambda *a, **kw: _json.dumps(synthetic) + ) + result = closer_module.fetch_open_prs(None) + assert len(result) == 5 + captured = capsys.readouterr() + assert "::warning::" not in (captured.out + captured.err) + + class TestHasOptoutLabel: def test_should_match_label_case_insensitively(self, closer_module): pr = {"labels": [{"name": "Do Not Close"}, {"name": "bug"}]}