Skip to content
Merged
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
19 changes: 15 additions & 4 deletions src/auto_fixer.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
REFIX_RUNNING_LABEL = "refix:running"
REFIX_DONE_LABEL = "refix:done"
REFIX_MERGED_LABEL = "refix:merged"
REFIX_AUTO_MERGE_REQUESTED_LABEL = "refix:auto-merge-requested"
CODERABBIT_PROCESSING_MARKER = "Currently processing new changes in this PR."
CODERABBIT_RATE_LIMIT_MARKER = "Rate limit exceeded"
CODERABBIT_REVIEW_FAILED_MARKER = "## Review failed"
Expand All @@ -110,6 +111,7 @@
REFIX_RUNNING_LABEL_COLOR = "FBCA04"
REFIX_DONE_LABEL_COLOR = "0E8A16"
REFIX_MERGED_LABEL_COLOR = "1D76DB"
REFIX_AUTO_MERGE_REQUESTED_LABEL_COLOR = "C2E0C6"
FAILED_CI_CONCLUSIONS = {"FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "CANCELLED", "STALE", "STARTUP_FAILURE"}
FAILED_CI_STATES = {"ERROR", "FAILURE"}
GITHUB_ACTIONS_RUN_URL_PATTERN = re.compile(r"/actions/runs/(\d+)")
Expand Down Expand Up @@ -1237,6 +1239,12 @@ def _ensure_refix_labels(repo: str) -> None:
color=REFIX_MERGED_LABEL_COLOR,
description="PR has been merged after Refix auto-merge.",
)
_ensure_repo_label_exists(
repo,
REFIX_AUTO_MERGE_REQUESTED_LABEL,
color=REFIX_AUTO_MERGE_REQUESTED_LABEL_COLOR,
description="Refix has requested auto-merge for this PR.",
)


def _edit_pr_label(repo: str, pr_number: int, *, add: bool, label: str) -> bool:
Expand Down Expand Up @@ -1288,6 +1296,7 @@ def _set_pr_done_label(repo: str, pr_number: int) -> None:
def _set_pr_merged_label(repo: str, pr_number: int) -> None:
_ensure_refix_labels(repo)
_edit_pr_label(repo, pr_number, add=False, label=REFIX_RUNNING_LABEL)
_edit_pr_label(repo, pr_number, add=False, label=REFIX_AUTO_MERGE_REQUESTED_LABEL)
_edit_pr_label(repo, pr_number, add=True, label=REFIX_MERGED_LABEL)


Expand All @@ -1303,7 +1312,7 @@ def _pr_has_label(pr_data: dict[str, Any], label_name: str) -> bool:

def _mark_pr_merged_label_if_needed(repo: str, pr_number: int) -> bool:
"""Add refix:merged label when PR is merged and eligible."""
cmd = ["gh", "pr", "view", str(pr_number), "--repo", repo, "--json", "mergedAt,labels,autoMergeRequest"]
cmd = ["gh", "pr", "view", str(pr_number), "--repo", repo, "--json", "mergedAt,labels"]
result = subprocess.run(
cmd,
capture_output=True,
Expand Down Expand Up @@ -1333,9 +1342,9 @@ def _mark_pr_merged_label_if_needed(repo: str, pr_number: int) -> bool:
return False
if not _pr_has_label(pr_data, REFIX_DONE_LABEL):
return False
if _pr_has_label(pr_data, REFIX_MERGED_LABEL):
if not _pr_has_label(pr_data, REFIX_AUTO_MERGE_REQUESTED_LABEL):
return False
if not pr_data.get("autoMergeRequest"):
if _pr_has_label(pr_data, REFIX_MERGED_LABEL):
return False

print(f"PR #{pr_number} is merged; adding {REFIX_MERGED_LABEL} label.")
Expand All @@ -1345,7 +1354,7 @@ def _mark_pr_merged_label_if_needed(repo: str, pr_number: int) -> bool:

def _backfill_merged_labels(repo: str, *, limit: int = 100) -> int:
"""Backfill refix:merged label for merged PRs already marked refix:done."""
search_query = f'label:"{REFIX_DONE_LABEL}" -label:"{REFIX_MERGED_LABEL}"'
search_query = f'label:"{REFIX_DONE_LABEL}" label:"{REFIX_AUTO_MERGE_REQUESTED_LABEL}" -label:"{REFIX_MERGED_LABEL}"'
cmd = [
"gh",
"pr",
Expand Down Expand Up @@ -1410,13 +1419,15 @@ def _trigger_pr_auto_merge(repo: str, pr_number: int) -> bool:
)
if result.returncode == 0:
print(f"Auto-merge requested for PR #{pr_number}.")
_edit_pr_label(repo, pr_number, add=True, label=REFIX_AUTO_MERGE_REQUESTED_LABEL)
return True

stderr_text = (result.stderr or "").strip()
stdout_text = (result.stdout or "").strip()
combined_lower = f"{stdout_text}\n{stderr_text}".lower()
if "already merged" in combined_lower:
print(f"PR #{pr_number} is already merged.")
_edit_pr_label(repo, pr_number, add=True, label=REFIX_AUTO_MERGE_REQUESTED_LABEL)
return True

details = stderr_text or stdout_text or "unknown error"
Expand Down
21 changes: 17 additions & 4 deletions tests/test_auto_fixer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1683,6 +1683,7 @@ def test_set_pr_merged_label_ensures_labels_before_edit(self):
mock_edit.assert_has_calls(
[
call("owner/repo", 12, add=False, label="refix:running"),
call("owner/repo", 12, add=False, label="refix:auto-merge-requested"),
call("owner/repo", 12, add=True, label="refix:merged"),
]
)
Expand All @@ -1692,7 +1693,7 @@ def test_trigger_pr_auto_merge_executes_gh_merge(self):
ok = auto_fixer._trigger_pr_auto_merge("owner/repo", 7)

assert ok is True
mock_run.assert_called_once_with(
mock_run.assert_any_call(
["gh", "pr", "merge", "7", "--repo", "owner/repo", "--auto", "--merge"],
capture_output=True,
text=True,
Expand All @@ -1712,8 +1713,7 @@ def test_trigger_pr_auto_merge_treats_already_merged_as_success(self):
def test_mark_pr_merged_label_if_needed_adds_label_for_done_merged_pr(self):
pr_view = {
"mergedAt": "2026-03-11T00:00:00Z",
"labels": [{"name": "refix:done"}],
"autoMergeRequest": {"enabledBy": {"login": "bot"}},
"labels": [{"name": "refix:done"}, {"name": "refix:auto-merge-requested"}],
}
with (
patch("auto_fixer.subprocess.run", return_value=Mock(returncode=0, stdout=json.dumps(pr_view), stderr="")),
Expand All @@ -1726,7 +1726,7 @@ def test_mark_pr_merged_label_if_needed_adds_label_for_done_merged_pr(self):
def test_mark_pr_merged_label_if_needed_skips_when_not_merged(self):
pr_view = {
"mergedAt": None,
"labels": [{"name": "refix:done"}],
"labels": [{"name": "refix:done"}, {"name": "refix:auto-merge-requested"}],
}
with (
patch("auto_fixer.subprocess.run", return_value=Mock(returncode=0, stdout=json.dumps(pr_view), stderr="")),
Expand All @@ -1736,6 +1736,19 @@ def test_mark_pr_merged_label_if_needed_skips_when_not_merged(self):
assert ok is False
mock_set_merged.assert_not_called()

def test_mark_pr_merged_label_if_needed_skips_when_auto_merge_not_requested(self):
pr_view = {
"mergedAt": "2026-03-11T00:00:00Z",
"labels": [{"name": "refix:done"}],
}
with (
patch("auto_fixer.subprocess.run", return_value=Mock(returncode=0, stdout=json.dumps(pr_view), stderr="")),
patch("auto_fixer._set_pr_merged_label") as mock_set_merged,
):
ok = auto_fixer._mark_pr_merged_label_if_needed("owner/repo", 23)
assert ok is False
mock_set_merged.assert_not_called()

def test_backfill_merged_labels_applies_label_to_matching_prs(self):
merged_prs = [{"number": 31}, {"number": 32}]
with (
Expand Down
Loading