Skip to content

Commit ef21d98

Browse files
chore: address second round of PR feedback (#2909)
Co-authored-by: openhands <openhands@all-hands.dev>
1 parent c604d38 commit ef21d98

5 files changed

Lines changed: 161 additions & 17 deletions

File tree

.github/workflows/issue-duplicate-checker.yml

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -291,14 +291,27 @@ jobs:
291291
);
292292
const body = sections.join('\n').trim();
293293
294-
const { data: comments } = await github.rest.issues.listComments({
295-
owner: context.repo.owner,
296-
repo: context.repo.repo,
297-
issue_number: issueNumber,
298-
per_page: 100,
299-
});
294+
let allComments = [];
295+
let page = 1;
296+
while (true) {
297+
const { data: comments } = await github.rest.issues.listComments({
298+
owner: context.repo.owner,
299+
repo: context.repo.repo,
300+
issue_number: issueNumber,
301+
per_page: 100,
302+
page,
303+
});
304+
if (!comments || comments.length === 0) {
305+
break;
306+
}
307+
allComments = allComments.concat(comments);
308+
if (comments.length < 100) {
309+
break;
310+
}
311+
page += 1;
312+
}
300313
301-
const existing = comments.find((comment) => comment.body && comment.body.includes('<!-- openhands-duplicate-check '));
314+
const existing = allComments.find((comment) => comment.body && comment.body.includes('<!-- openhands-duplicate-check '));
302315
if (existing) {
303316
if (autoClose) {
304317
await ensureCandidateLabelOnIssue();

.github/workflows/remove-duplicate-candidate-label.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ jobs:
1515
github.event.issue.pull_request == null &&
1616
contains(github.event.issue.labels.*.name, 'duplicate-candidate') &&
1717
github.event.comment.user.type != 'Bot' &&
18+
!endsWith(github.event.comment.user.login, '[bot]') &&
19+
github.event.comment.user.login != 'all-hands-bot' &&
1820
!contains(github.event.comment.body, '<!-- openhands-duplicate-check') &&
1921
!contains(github.event.comment.body, '<!-- openhands-duplicate-veto')
2022
runs-on: ubuntu-latest

scripts/auto_close_duplicate_issues.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,15 @@ def request_json(
7171
raise RuntimeError(
7272
f"{method} {path} failed with HTTP {exc.code}: {error_body}"
7373
) from exc
74+
except urllib.error.URLError as exc:
75+
raise RuntimeError(f"{method} {path} failed: {exc}") from exc
7476

7577
if not payload:
7678
return None
77-
return json.loads(payload)
79+
try:
80+
return json.loads(payload)
81+
except json.JSONDecodeError as exc:
82+
raise RuntimeError(f"Failed to parse JSON from {path}: {exc}") from exc
7883

7984

8085
def parse_timestamp(value: str) -> datetime:
@@ -291,8 +296,12 @@ def main() -> int:
291296

292297
summary: list[dict[str, Any]] = []
293298
for issue in list_open_issues(args.repository):
294-
issue_number = int(issue["number"])
295-
issue_created_at = parse_timestamp(issue["created_at"])
299+
issue_number = issue.get("number")
300+
issue_created_at_str = issue.get("created_at")
301+
if issue_number is None or not issue_created_at_str:
302+
continue
303+
issue_number = int(issue_number)
304+
issue_created_at = parse_timestamp(issue_created_at_str)
296305
if issue_created_at > cutoff:
297306
continue
298307

@@ -303,12 +312,16 @@ def main() -> int:
303312
if latest_comment is None or canonical_issue_number is None:
304313
continue
305314

306-
comment_created_at = parse_timestamp(latest_comment["created_at"])
315+
comment_created_at_str = latest_comment.get("created_at")
316+
comment_id = latest_comment.get("id")
317+
if not comment_created_at_str or comment_id is None:
318+
continue
319+
comment_created_at = parse_timestamp(comment_created_at_str)
307320
if comment_created_at > cutoff:
308321
continue
309322

310323
author_id = user_id_from_item(issue)
311-
reactions = list_comment_reactions(args.repository, int(latest_comment["id"]))
324+
reactions = list_comment_reactions(args.repository, int(comment_id))
312325
author_thumbs_down = has_reaction_from_user(reactions, author_id, "-1")
313326
author_thumbs_up = has_reaction_from_user(reactions, author_id, "+1")
314327
if author_thumbs_down:
@@ -341,7 +354,8 @@ def main() -> int:
341354
newer_comments = [
342355
comment
343356
for comment in comments
344-
if parse_timestamp(comment["created_at"]) > comment_created_at
357+
if comment.get("created_at")
358+
and parse_timestamp(comment["created_at"]) > comment_created_at
345359
]
346360
if newer_comments:
347361
summary.append(

scripts/issue_duplicate_check_openhands.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,17 @@ def request_json(
106106
try:
107107
with urllib.request.urlopen(request, timeout=60) as response:
108108
return json.load(response)
109-
except json.JSONDecodeError as exc:
110-
raise RuntimeError(
111-
f"Failed to parse JSON from {method} {base_url}{path}: {exc}"
112-
) from exc
113109
except urllib.error.HTTPError as exc:
114110
error_body = exc.read().decode("utf-8", errors="replace")
115111
raise RuntimeError(
116112
f"{method} {base_url}{path} failed with HTTP {exc.code}: {error_body}"
117113
) from exc
114+
except json.JSONDecodeError as exc:
115+
raise RuntimeError(
116+
f"Failed to parse JSON from {method} {base_url}{path}: {exc}"
117+
) from exc
118+
except urllib.error.URLError as exc:
119+
raise RuntimeError(f"{method} {base_url}{path} failed: {exc}") from exc
118120

119121

120122
def fetch_issue(repository: str, issue_number: int) -> dict[str, Any]:

tests/cross/test_issue_duplicate_scripts.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,44 @@ def test_parse_timestamp_reports_invalid_values():
124124
module.parse_timestamp("invalid")
125125

126126

127+
def test_auto_close_request_json_reports_urlerror(monkeypatch):
128+
module = load_module("auto_close_duplicate_issues.py")
129+
130+
monkeypatch.setattr(module, "github_headers", lambda: {})
131+
monkeypatch.setattr(
132+
module.urllib.request,
133+
"urlopen",
134+
lambda *args, **kwargs: (_ for _ in ()).throw(
135+
module.urllib.error.URLError("boom")
136+
),
137+
)
138+
139+
with pytest.raises(RuntimeError, match="GET /test failed"):
140+
module.request_json("/test")
141+
142+
143+
def test_auto_close_request_json_reports_invalid_json(monkeypatch):
144+
module = load_module("auto_close_duplicate_issues.py")
145+
monkeypatch.setattr(module, "github_headers", lambda: {})
146+
147+
class DummyResponse:
148+
def __enter__(self):
149+
return self
150+
151+
def __exit__(self, exc_type, exc, tb):
152+
return False
153+
154+
def read(self):
155+
return b"not-json"
156+
157+
monkeypatch.setattr(
158+
module.urllib.request, "urlopen", lambda *args, **kwargs: DummyResponse()
159+
)
160+
161+
with pytest.raises(RuntimeError, match="Failed to parse JSON from /test"):
162+
module.request_json("/test")
163+
164+
127165
def test_has_reaction_from_user_ignores_missing_user_ids():
128166
module = load_module("auto_close_duplicate_issues.py")
129167
reactions = [
@@ -382,6 +420,66 @@ def test_auto_close_main_closes_old_duplicate(monkeypatch, capsys):
382420
assert closed == [("OpenHands/agent-sdk", 123, 45, False)]
383421

384422

423+
def test_auto_close_main_skips_malformed_issue_data(monkeypatch, capsys):
424+
module = load_module("auto_close_duplicate_issues.py")
425+
426+
monkeypatch.setattr(
427+
module,
428+
"parse_args",
429+
lambda: argparse.Namespace(
430+
repository="OpenHands/agent-sdk", close_after_days=3, dry_run=False
431+
),
432+
)
433+
monkeypatch.setattr(
434+
module, "list_open_issues", lambda repository: [{"number": 123}]
435+
)
436+
437+
assert module.main() == 0
438+
439+
summary = json.loads(capsys.readouterr().out)
440+
assert summary == {"repository": "OpenHands/agent-sdk", "results": []}
441+
442+
443+
def test_auto_close_main_skips_malformed_duplicate_comment(monkeypatch, capsys):
444+
module = load_module("auto_close_duplicate_issues.py")
445+
now = datetime.now(UTC)
446+
old_timestamp = iso_timestamp(now - timedelta(days=5))
447+
issue = {
448+
"number": 123,
449+
"created_at": old_timestamp,
450+
"labels": [{"name": module.DUPLICATE_CANDIDATE_LABEL}],
451+
"user": {"id": 7},
452+
}
453+
comments = [
454+
{
455+
"body": "<!-- openhands-duplicate-check canonical=45 auto-close=true -->",
456+
"created_at": old_timestamp,
457+
}
458+
]
459+
460+
monkeypatch.setattr(
461+
module,
462+
"parse_args",
463+
lambda: argparse.Namespace(
464+
repository="OpenHands/agent-sdk", close_after_days=3, dry_run=False
465+
),
466+
)
467+
monkeypatch.setattr(module, "list_open_issues", lambda repository: [issue])
468+
monkeypatch.setattr(
469+
module, "list_issue_comments", lambda repository, number: comments
470+
)
471+
monkeypatch.setattr(
472+
module,
473+
"close_issue_as_duplicate",
474+
lambda *args, **kwargs: pytest.fail("close_issue_as_duplicate should not run"),
475+
)
476+
477+
assert module.main() == 0
478+
479+
summary = json.loads(capsys.readouterr().out)
480+
assert summary == {"repository": "OpenHands/agent-sdk", "results": []}
481+
482+
385483
def test_auto_close_main_removes_label_when_newer_comment_exists(monkeypatch, capsys):
386484
module = load_module("auto_close_duplicate_issues.py")
387485
now = datetime.now(UTC)
@@ -500,6 +598,21 @@ def test_normalize_result_promotes_actionable_duplicates():
500598
assert normalized["summary"] == "duplicate summary"
501599

502600

601+
def test_issue_duplicate_request_json_reports_urlerror(monkeypatch):
602+
module = load_module("issue_duplicate_check_openhands.py")
603+
604+
monkeypatch.setattr(
605+
module.urllib.request,
606+
"urlopen",
607+
lambda *args, **kwargs: (_ for _ in ()).throw(
608+
module.urllib.error.URLError("boom")
609+
),
610+
)
611+
612+
with pytest.raises(RuntimeError, match="GET https://example.test/path failed"):
613+
module.request_json("https://example.test", "/path")
614+
615+
503616
def test_normalize_result_lowercases_classification():
504617
module = load_module("issue_duplicate_check_openhands.py")
505618
normalized = module.normalize_result(

0 commit comments

Comments
 (0)