Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions scripts/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Helper scripts for MergeWork maintenance checks."""
9 changes: 9 additions & 0 deletions scripts/bounty_refs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from __future__ import annotations

import re

LINKED_BOUNTY_VERBS = r"bounty|claims?|close[sd]?|fix(?:e[sd])?|resolve[sd]?|refs?|references?"
BOUNTY_REF_RE = re.compile(
rf"\b(?:{LINKED_BOUNTY_VERBS})\s+#(\d+)(?![A-Za-z0-9_-])",
re.IGNORECASE,
)
10 changes: 8 additions & 2 deletions scripts/pr_queue_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@
import subprocess
import sys
from collections import defaultdict
from pathlib import Path
from typing import Any

BOUNTY_REF_RE = re.compile(r"\b(?:bounty|refs?|fixes|closes|claims?)\s+#(\d+)", re.IGNORECASE)
if __package__ in {None, ""}:
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from scripts.bounty_refs import BOUNTY_REF_RE

NOISY_TITLE_PREFIX_RE = re.compile(r"^\s*(?:\[[^\]]+\]\s*)+")
UNSTABLE_MERGE_STATES = {"blocked", "conflicting", "dirty", "unknown", "unstable"}
GH_TIMEOUT_SECONDS = 30
Expand Down Expand Up @@ -116,7 +121,8 @@ def analyze_queue(data: dict[str, Any]) -> dict[str, Any]:
_issue(
pr,
"missing_bounty_reference",
"No Bounty #<issue>, Refs #<issue>, or /claim #<issue> found",
"No bounty reference such as Bounty #<issue>, Refs #<issue>, "
"Fixes #<issue>, or /claim #<issue> found",
)
)
for ref in pr["refs"]:
Expand Down
10 changes: 8 additions & 2 deletions scripts/submission_quality_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,16 @@
import sys
from datetime import UTC, datetime, timedelta
from difflib import SequenceMatcher
from pathlib import Path
from typing import Any
from urllib.error import HTTPError, URLError
from urllib.request import urlopen

BOUNTY_REF_RE = re.compile(r"\b(?:bounty|refs?|fixes|closes|claims?)\s+#(\d+)", re.IGNORECASE)
if __package__ in {None, ""}:
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from scripts.bounty_refs import BOUNTY_REF_RE

EVIDENCE_RE = re.compile(
r"\b(pytest|ruff|mypy|validation|verified|test evidence|checks? passed)\b",
re.IGNORECASE,
Expand Down Expand Up @@ -220,7 +225,8 @@ def evaluate_submission(data: dict[str, Any]) -> dict[str, Any]:
_check(
"bounty_reference",
"fail",
"submission text must include Bounty #<issue>, Refs #<issue>, or /claim #<issue>",
"submission text must include a bounty reference such as "
"Bounty #<issue>, Refs #<issue>, Fixes #<issue>, or /claim #<issue>",
)
)
else:
Expand Down
80 changes: 79 additions & 1 deletion tests/test_pr_queue_health.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@

import json
import subprocess
import sys
from pathlib import Path

import pytest

from scripts import pr_queue_health
from scripts.pr_queue_health import analyze_queue, format_markdown_report, format_text_report, main

ROOT = Path(__file__).resolve().parents[1]


def test_pr_queue_health_flags_required_queue_cases(tmp_path, capsys) -> None:
fixture = {
Expand Down Expand Up @@ -84,6 +88,19 @@ def test_pr_queue_health_flags_required_queue_cases(tmp_path, capsys) -> None:
assert output["summary"]["pull_requests"] == 4


def test_pr_queue_health_script_entrypoint_loads_shared_parser() -> None:
result = subprocess.run(
[sys.executable, "scripts/pr_queue_health.py", "--help"],
cwd=ROOT,
capture_output=True,
text=True,
check=False,
)

assert result.returncode == 0
assert "usage:" in result.stdout


def test_pr_queue_health_text_report_is_pasteable() -> None:
report = analyze_queue(
{
Expand Down Expand Up @@ -127,6 +144,66 @@ def test_pr_queue_health_accepts_claim_command_reference() -> None:
assert report["missing_bounty_references"] == []


def test_pr_queue_health_accepts_github_linking_keywords() -> None:
references = (
"Bounty #310",
"Claim #310",
"Claims #310",
"Ref #310",
"Refs #310",
"Reference #310",
"References #310",
"Fix #310",
"Fixes #310",
"Fixed #310",
"Close #310",
"Closes #310",
"Closed #310",
"Resolve #310",
"Resolves #310",
"Resolved #310",
)
report = analyze_queue(
{
"bounties": [{"number": 310, "state": "OPEN", "awards_remaining": 1}],
"pull_requests": [
{
"number": index,
"title": f"Harden bounty queue checks {index}",
"body": reference,
"merge_state": "clean",
"labels": [],
}
for index, reference in enumerate(references, start=1)
],
}
)

assert report["summary"]["missing_bounty_references"] == 0
assert report["missing_bounty_references"] == []
Comment thread
coderabbitai[bot] marked this conversation as resolved.


@pytest.mark.parametrize("reference", ("Fixes #310abc", "Fixes #310_abc", "Fixes #310-abc"))
def test_pr_queue_health_rejects_linking_keyword_issue_suffix(reference: str) -> None:
report = analyze_queue(
{
"bounties": [{"number": 310, "state": "OPEN", "awards_remaining": 1}],
"pull_requests": [
{
"number": 8,
"title": "Harden bounty queue checks",
"body": reference,
"merge_state": "clean",
"labels": [],
}
],
}
)

assert report["summary"]["missing_bounty_references"] == 1
assert report["missing_bounty_references"][0]["pull_request"] == 8


def test_pr_queue_health_markdown_report_includes_required_sections() -> None:
report = analyze_queue(
{
Expand Down Expand Up @@ -183,7 +260,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 #<issue>, Refs #<issue>, or /claim #<issue> found)"
"Improve bounty filters (No bounty reference such as Bounty #<issue>, "
"Refs #<issue>, Fixes #<issue>, or /claim #<issue> found)"
) in markdown
assert "### Dirty or unstable merge state" in markdown
assert "Merge state is dirty" in markdown
Expand Down
78 changes: 77 additions & 1 deletion tests/test_submission_quality_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@

import json
import subprocess
import sys
from pathlib import Path

import pytest

from scripts import submission_quality_gate
from scripts.submission_quality_gate import evaluate_submission, main

ROOT = Path(__file__).resolve().parents[1]


def test_submission_quality_gate_passes_open_bounty_with_evidence(capsys, tmp_path) -> None:
fixture = {
Expand Down Expand Up @@ -40,6 +46,19 @@ def test_submission_quality_gate_passes_open_bounty_with_evidence(capsys, tmp_pa
assert json.loads(capsys.readouterr().out)["status"] == "pass"


def test_submission_quality_gate_script_entrypoint_loads_shared_parser() -> None:
result = subprocess.run(
[sys.executable, "scripts/submission_quality_gate.py", "--help"],
cwd=ROOT,
capture_output=True,
text=True,
check=False,
)

assert result.returncode == 0
assert "usage:" in result.stdout


def test_submission_quality_gate_accepts_claim_command_reference() -> None:
result = evaluate_submission(
{
Expand All @@ -66,6 +85,62 @@ def test_submission_quality_gate_accepts_claim_command_reference() -> None:
} in result["checks"]


def test_submission_quality_gate_accepts_github_linking_keywords() -> None:
references = (
"Bounty #319",
"Claim #319",
"Claims #319",
"Ref #319",
"Refs #319",
"Reference #319",
"References #319",
"Fix #319",
"Fixes #319",
"Fixed #319",
"Close #319",
"Closes #319",
"Closed #319",
"Resolve #319",
"Resolves #319",
"Resolved #319",
)
for reference in references:
result = evaluate_submission(
{
"submission_text": f"""
Summary:
Harden the bounty reference parser.

{reference}

Validation:
- pytest passed.
""",
"bounties": [{"number": 319, "state": "OPEN", "awards_remaining": 1}],
"pull_requests": [],
}
)

assert result["status"] == "pass", reference
assert result["bounty_reference"] == 319
Comment thread
coderabbitai[bot] marked this conversation as resolved.


@pytest.mark.parametrize("reference", ("Fixes #319abc", "Fixes #319_abc", "Fixes #319-abc"))
def test_submission_quality_gate_rejects_linking_keyword_issue_suffix(reference: str) -> None:
result = evaluate_submission(
{
"submission_text": (
f"Summary: add validation\n\n{reference}\n\nValidation: pytest passed"
),
"bounties": [{"number": 319, "state": "OPEN", "awards_remaining": 1}],
"pull_requests": [],
}
)

assert result["status"] == "fail"
assert result["bounty_reference"] is None


def test_submission_quality_gate_fails_missing_reference() -> None:
result = evaluate_submission(
{
Expand All @@ -80,7 +155,8 @@ def test_submission_quality_gate_fails_missing_reference() -> None:
"name": "bounty_reference",
"status": "fail",
"message": (
"submission text must include Bounty #<issue>, Refs #<issue>, or /claim #<issue>"
"submission text must include a bounty reference such as "
"Bounty #<issue>, Refs #<issue>, Fixes #<issue>, or /claim #<issue>"
),
}

Expand Down
Loading