fix(skills): exclude pull requests from GitHub issue research (#7516)

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
sxl-lucky7 2026-10-26 15:38:59 +08:00 committed by GitHub
parent 30b3a012cb
commit 5ca17829be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 149 additions and 5 deletions

View File

@ -1729,6 +1729,8 @@ File reads accept raw repository-relative paths and preserve spaces, Unicode,
`#`, `?`, and literal percent signs with either HTTP client.
Directory listings ask for the repository's own default branch, so a repository that
uses neither `main` nor `master` reports its current tree instead of a stale branch.
The `issues` command excludes pull requests and follows additional pages to fill
its requested issue limit; the `prs` command remains available for pull requests.
Users can explicitly activate an enabled skill for a single turn by starting the request with `/skill-name`, for example `/data-analysis analyze uploads/foo.csv`. DeerFlow loads that skill's `SKILL.md` as hidden current-turn context while leaving the base prompt limited to skill metadata. Slash activation respects disabled skills, custom-agent skill whitelists, and existing channel commands such as `/new` and `/help`.

View File

@ -56,6 +56,8 @@ python /path/to/skill/scripts/github_api.py <owner> <repo> tree
- prs
- releases
The `issues` command returns repository issues only, excluding pull requests. It follows additional pages when needed to fill the requested issue limit; use `prs` to research pull requests separately.
**Round 2 - Discovery (3-5 web_search)**
- Get overview and identify key terms
- Find official website/repo

View File

@ -172,16 +172,28 @@ class GitHubAPI:
labels: Optional[str] = None,
) -> List[Dict]:
"""
Get repository issues.
Get repository issues, excluding pull requests.
Args:
state: 'open', 'closed', or 'all'
labels: Comma-separated label names
"""
params = {"state": state, "per_page": min(limit, 100)}
limit = min(limit, 100)
if limit <= 0:
return []
params = {"state": state, "per_page": limit}
if labels:
params["labels"] = labels
return self._get(f"/repos/{owner}/{repo}/issues", params)
issues = []
page = 1
while len(issues) < limit:
items = self._get(f"/repos/{owner}/{repo}/issues", params)
issues.extend(item for item in items if "pull_request" not in item)
if len(items) < limit:
break
page += 1
params["page"] = page
return issues[:limit]
def get_pull_requests(
self, owner: str, repo: str, state: str = "all", limit: int = 30

View File

@ -29,7 +29,7 @@ def fallback_client(monkeypatch):
finally:
connection.close()
captured.update(url=request.full_url, timeout=timeout, headers=request.headers)
response = io.BytesIO(b'{"ok": true}')
response = io.BytesIO(captured.get("body", b'{"ok": true}'))
response.status = 200
return response
@ -102,10 +102,11 @@ def test_fallback_search_preserves_query(fallback_client, query):
def test_fallback_issues_preserves_filters(fallback_client):
client, captured = fallback_client
captured["body"] = b'[{"number": 1}]'
assert client.get_issues(
"owner", "repo", state="open", limit=10, labels="bug,help wanted"
) == {"ok": True}
) == [{"number": 1}]
assert parse_qs(urlsplit(captured["url"]).query) == {
"state": ["open"],

View File

@ -0,0 +1,127 @@
"""Keep pull requests out of repository issue research, across both transports."""
import io
import json
import runpy
import sys
import urllib.error
import urllib.request
from pathlib import Path
from urllib.parse import parse_qs, urlsplit
import pytest
@pytest.fixture(params=["requests", "urllib"])
def issue_client(request, monkeypatch):
if request.param == "urllib":
monkeypatch.setitem(sys.modules, "requests", None)
script = (
Path(__file__).resolve().parents[2]
/ "skills/public/github-deep-research/scripts/github_api.py"
)
module = runpy.run_path(str(script))
pages = []
calls = []
if request.param == "urllib":
def fake_urlopen(prepared, timeout):
calls.append(prepared.full_url)
payload, status = pages.pop(0)
data = json.dumps(payload).encode()
if status >= 400:
raise urllib.error.HTTPError(prepared.full_url, status, "error", {}, io.BytesIO(data))
response = io.BytesIO(data)
response.status = status
return response
monkeypatch.setattr(urllib.request, "urlopen", fake_urlopen)
else:
requests = pytest.importorskip("requests")
def fake_send(session, prepared, **kwargs):
calls.append(prepared.url)
payload, status = pages.pop(0)
response = requests.Response()
response.status_code = status
response._content = json.dumps(payload).encode()
return response
monkeypatch.setattr(requests.Session, "send", fake_send)
return module["GitHubAPI"](), pages, calls
def test_issue_limit_counts_issues_instead_of_pull_requests(issue_client):
client, pages, calls = issue_client
pages.extend([
([{"number": 10, "pull_request": {}}, {"number": 9}], 200),
([{"number": 8, "pull_request": {}}, {"number": 7}], 200),
])
assert client.get_issues("owner", "repo", state="open", limit=2, labels="bug,help wanted") == [{"number": 9}, {"number": 7}]
assert len(calls) == 2
for index, url in enumerate(calls):
assert urlsplit(url).path == "/repos/owner/repo/issues"
parameters = parse_qs(urlsplit(url).query)
assert parameters["state"] == ["open"]
assert parameters["labels"] == ["bug,help wanted"]
assert parameters["per_page"] == ["2"]
assert parameters.get("page", ["1"]) == [str(index + 1)]
def test_issue_search_continues_after_a_page_of_only_pull_requests(issue_client):
client, pages, calls = issue_client
pages.extend([
([{"number": 5, "pull_request": {}}, {"number": 4, "pull_request": {}}], 200),
([{"number": 3}], 200),
])
assert client.get_issues("owner", "repo", limit=2) == [{"number": 3}]
assert len(calls) == 2
def test_issue_search_stops_at_an_empty_page(issue_client):
client, pages, calls = issue_client
pages.extend([([{"number": 2, "pull_request": {}}], 200), ([], 200)])
assert client.get_issues("owner", "repo", limit=1) == []
assert len(calls) == 2
def test_issue_search_does_not_fetch_an_unneeded_page(issue_client):
client, pages, calls = issue_client
pages.append(([{"number": 2}, {"number": 1}], 200))
assert client.get_issues("owner", "repo", limit=2) == [{"number": 2}, {"number": 1}]
assert len(calls) == 1
def test_last_page_does_not_exceed_the_requested_issue_limit(issue_client):
client, pages, calls = issue_client
pages.extend([
([{"number": 4}, {"number": 3, "pull_request": {}}], 200),
([{"number": 2}, {"number": 1}], 200),
])
assert client.get_issues("owner", "repo", limit=2) == [{"number": 4}, {"number": 2}]
assert len(calls) == 2
@pytest.mark.parametrize("limit", [0, -1])
def test_nonpositive_issue_limit_does_not_make_a_request(issue_client, limit):
client, pages, calls = issue_client
pages.append(([], 200))
assert client.get_issues("owner", "repo", limit=limit) == []
assert calls == []
def test_later_page_failure_is_not_returned_as_partial_research(issue_client):
client, pages, calls = issue_client
pages.extend([([{"number": 3}, {"number": 2, "pull_request": {}}], 200), ({"message": "rate limited"}, 403)])
with pytest.raises(Exception, match="403"):
client.get_issues("owner", "repo", limit=2)
assert len(calls) == 2