diff --git a/scripts/pr_queue_health.py b/scripts/pr_queue_health.py index e96ed86d..4eafcc5a 100644 --- a/scripts/pr_queue_health.py +++ b/scripts/pr_queue_health.py @@ -8,7 +8,11 @@ from collections import defaultdict from typing import Any -BOUNTY_REF_RE = re.compile(r"\b(?:bounty|refs?|fixes|closes|claims?)\s+#(\d+)", re.IGNORECASE) +BOUNTY_REF_RE = re.compile( + r"\b(?:(?Plive|mrwk|native|internal)\s+)?" + r"(?Pbounty|issues?|refs?|fixes|closes|claims?)\s+#(?P\d+)", + re.IGNORECASE, +) NOISY_TITLE_PREFIX_RE = re.compile(r"^\s*(?:\[[^\]]+\]\s*)+") UNSTABLE_MERGE_STATES = {"blocked", "conflicting", "dirty", "unknown", "unstable"} GH_TIMEOUT_SECONDS = 30 @@ -47,15 +51,22 @@ def _scope_key(raw: dict[str, Any]) -> str: def _bounty_refs(raw: dict[str, Any]) -> list[int]: explicit = raw.get("bounty_refs") if isinstance(explicit, list): - refs = [item for item in explicit if isinstance(item, int)] - if refs: - return sorted(set(refs)) + explicit_refs = [ + item for item in explicit if isinstance(item, int) and not isinstance(item, bool) + ] + if explicit_refs: + return sorted(set(explicit_refs)) text = "\n".join( str(raw.get(key) or "") for key in ("title", "body", "description") if raw.get(key) is not None ) - return sorted({int(match) for match in BOUNTY_REF_RE.findall(text)}) + refs: set[int] = set() + for match in BOUNTY_REF_RE.finditer(text): + if match.group("native_prefix") and match.group("keyword").lower() == "bounty": + continue + refs.add(int(match.group("number"))) + return sorted(refs) def _is_open_bounty(raw: dict[str, Any]) -> bool: @@ -116,7 +127,7 @@ def analyze_queue(data: dict[str, Any]) -> dict[str, Any]: _issue( pr, "missing_bounty_reference", - "No Bounty #, Refs #, or /claim # found", + "No Bounty #, Issue #, Refs #, or /claim # found", ) ) for ref in pr["refs"]: diff --git a/tests/test_pr_queue_health.py b/tests/test_pr_queue_health.py index 58ff8fbe..67be0ece 100644 --- a/tests/test_pr_queue_health.py +++ b/tests/test_pr_queue_health.py @@ -127,6 +127,120 @@ def test_pr_queue_health_accepts_claim_command_reference() -> None: assert report["missing_bounty_references"] == [] +def test_pr_queue_health_ignores_boolean_explicit_bounty_refs() -> None: + report = analyze_queue( + { + "bounties": [{"number": 406, "state": "OPEN", "awards_remaining": 1}], + "pull_requests": [ + { + "number": 531, + "title": "Boolean refs are not issue refs", + "body": "", + "bounty_refs": [True, False], + "merge_state": "clean", + "labels": [], + }, + { + "number": 532, + "title": "Valid explicit issue ref still counts", + "body": "", + "bounty_refs": [True, 406], + "merge_state": "clean", + "labels": [], + }, + ], + } + ) + + assert report["summary"]["missing_bounty_references"] == 1 + assert report["missing_bounty_references"][0]["pull_request"] == 531 + assert report["closed_bounty_references"] == [] + + +def test_pr_queue_health_ignores_native_bounty_ids_when_issue_ref_is_present() -> None: + report = analyze_queue( + { + "bounties": [{"number": 406, "state": "OPEN", "awards_remaining": 16}], + "pull_requests": [ + { + "number": 524, + "title": "Refs #406: Reject malformed IPv4 public URLs", + "body": ( + "Focused #406 small fix. Evidence: live bounty #66 / issue #406 " + "preflight returned status=open." + ), + "merge_state": "clean", + "labels": [], + }, + { + "number": 525, + "title": "Guard another #406 edge case", + "body": "MRWK bounty #66 maps to issue #406.", + "merge_state": "clean", + "labels": [], + }, + { + "number": 528, + "title": "Ignore spaced live bounty evidence ids", + "body": ( + "Evidence: live bounty #66 and live\tbounty #67 both map to issue #406." + ), + "merge_state": "clean", + "labels": [], + }, + { + "number": 526, + "title": "Ignore native #406 evidence ids", + "body": ( + "Evidence: native bounty #66 / issue #406 preflight returned status=open." + ), + "merge_state": "clean", + "labels": [], + }, + { + "number": 527, + "title": "Ignore internal #406 evidence ids", + "body": "Evidence: internal bounty #66, issue #406 is the GitHub bounty.", + "merge_state": "clean", + "labels": [], + }, + ], + } + ) + + assert report["summary"]["closed_bounty_references"] == 0 + assert report["summary"]["missing_bounty_references"] == 0 + assert report["closed_bounty_references"] == [] + assert report["missing_bounty_references"] == [] + + +def test_pr_queue_health_requires_issue_ref_when_only_native_bounty_id_is_present() -> None: + report = analyze_queue( + { + "bounties": [{"number": 406, "state": "OPEN", "awards_remaining": 16}], + "pull_requests": [ + { + "number": 529, + "title": "Needs GitHub issue reference", + "body": "Evidence: live bounty #66 returned status=open.", + "merge_state": "clean", + "labels": [], + }, + { + "number": 530, + "title": "Longer words still count as bounty refs", + "body": "Evidence: relive bounty #406 returned status=open.", + "merge_state": "clean", + "labels": [], + }, + ], + } + ) + + assert report["summary"]["missing_bounty_references"] == 1 + assert report["missing_bounty_references"][0]["pull_request"] == 529 + + def test_pr_queue_health_markdown_report_includes_required_sections() -> None: report = analyze_queue( { @@ -183,7 +297,8 @@ def test_pr_queue_health_markdown_report_includes_required_sections() -> None: assert "### Missing bounty references" in markdown assert ( "- [PR #2](https://github.com/ramimbo/mergework/pull/2): " - "Improve bounty filters (No Bounty #, Refs #, or /claim # found)" + "Improve bounty filters " + "(No Bounty #, Issue #, Refs #, or /claim # found)" ) in markdown assert "### Dirty or unstable merge state" in markdown assert "Merge state is dirty" in markdown