From 3355dd05353d2b57a72b138e1376fc3dbad565a5 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 12 Mar 2026 10:30:13 +0000 Subject: [PATCH 01/18] Add configurable enabled PR labels Co-authored-by: HappyOnigiri --- .refix.yaml.sample | 10 + README.ja.md | 24 +++ README.md | 27 +++ src/auto_fixer.py | 427 ++++++++++++++++++++++++++++++++------- tests/test_auto_fixer.py | 111 +++++++++- 5 files changed, 529 insertions(+), 70 deletions(-) diff --git a/.refix.yaml.sample b/.refix.yaml.sample index 5499348..13d5381 100644 --- a/.refix.yaml.sample +++ b/.refix.yaml.sample @@ -16,6 +16,16 @@ ci_log_max_lines: 120 # Default: false auto_merge: false +# Enable only selected Refix PR labels (Optional) +# Allowed: running, done, merged, auto_merge_requested +# Default: all labels enabled +# Set [] to disable all Refix label operations +enabled_pr_labels: + - running + - done + - merged + - auto_merge_requested + # Automatically post `@coderabbitai resume` when CodeRabbit can be resumed # (after rate-limit wait expires or when a "Review failed" head-commit-changed status appears) (Optional) # Default: false diff --git a/README.ja.md b/README.ja.md index bc33a0e..85e31ef 100644 --- a/README.ja.md +++ b/README.ja.md @@ -93,6 +93,12 @@ ci_log_max_lines: 120 auto_merge: false +enabled_pr_labels: + - running + - done + - merged + - auto_merge_requested + coderabbit_auto_resume: false coderabbit_auto_resume_max_per_run: 1 @@ -151,6 +157,17 @@ PR が `refix:done` 状態になった際に自動マージします。 有効にすると、`refix` は修正適用後に GitHub の auto-merge をトリガーします。auto-merge は必須のステータスチェックがすべて通過した後に完了します。 +#### `enabled_pr_labels` + +Refix が有効化する PR ラベルを選択します。 + +- 型: 文字列のリスト +- 必須: いいえ +- デフォルト: `["running", "done", "merged", "auto_merge_requested"]` +- 許可値: `running`, `done`, `merged`, `auto_merge_requested` + +この設定は ON 方式です。指定したラベルだけを `refix` が作成・付与・除去します。`[]` を指定すると Refix のラベル操作をすべて無効化できます。 + #### `process_draft_prs` ドラフト PR を処理対象に含めるかどうかを設定します。 @@ -238,6 +255,7 @@ PR の状態管理コメントに記録する `処理日時` のタイムゾー - YAML のルートはマッピングである必要があります。 - `repositories` は必須で、1 件以上の要素が必要です。 - 未知のキーは即エラーではなく、警告を出して無視されます。 +- `enabled_pr_labels` は `running` / `done` / `merged` / `auto_merge_requested` のみを含むリストである必要があります。 - `state_comment_timezone` は有効な IANA タイムゾーン名(または `JST` エイリアス)である必要があります。 - `models.summarize` で要約処理で使用するモデルを指定します。この設定は環境変数 `REFIX_MODEL_SUMMARIZE` より優先されます。 - `models.fix` で修正処理で使用するモデルを指定します。 @@ -295,6 +313,12 @@ ci_log_max_lines: 120 auto_merge: false +enabled_pr_labels: + - running + - done + - merged + - auto_merge_requested + coderabbit_auto_resume: false coderabbit_auto_resume_max_per_run: 1 diff --git a/README.md b/README.md index 59fc511..ba0ebcf 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,15 @@ ci_log_max_lines: 120 # When merge completes, the refix:merged label is applied auto_merge: false +# Enable only selected Refix PR labels (optional, default: all enabled) +# Allowed values: running, done, merged, auto_merge_requested +# Use [] to disable all Refix label operations +enabled_pr_labels: + - running + - done + - merged + - auto_merge_requested + # Automatically post `@coderabbitai resume` when CodeRabbit can be resumed automatically # (rate-limit wait expiry or "Review failed" status caused by head commit changes) # (optional, default false) @@ -163,6 +172,17 @@ Automatically merge the fix PR when it reaches the `refix:done` state. When enabled, `refix` will trigger GitHub's auto-merge on the PR after applying fixes. Auto-merge only completes once all required status checks pass. +#### `enabled_pr_labels` + +Select which Refix PR labels are enabled. + +- Type: list of strings +- Required: no +- Default: `["running", "done", "merged", "auto_merge_requested"]` +- Allowed values: `running`, `done`, `merged`, `auto_merge_requested` + +This is an opt-in list: only listed labels are managed (created/added/removed) by `refix`. Set `[]` to disable all Refix label operations. + #### `process_draft_prs` Whether to include draft PRs in the processing targets. @@ -268,6 +288,7 @@ If omitted, `refix` falls back to the effective Git identity available in the ex - The YAML root must be a mapping. - `repositories` must be present and must contain at least one entry. - Unknown keys are ignored with warnings rather than treated as hard errors. +- `enabled_pr_labels` must be a list containing only `running`, `done`, `merged`, and/or `auto_merge_requested`. - `state_comment_timezone` must be a valid IANA timezone name (or `JST` alias). - `models.summarize` in YAML takes priority over the `REFIX_MODEL_SUMMARIZE` environment variable when selecting the summarization model. - The `coderabbit_auto_resume` option applies to active CodeRabbit rate-limit comments and active `Review failed` status comments (head commit changed during review). Duplicate `@coderabbitai resume` comments are avoided when one has already been posted after the latest matching status comment. @@ -322,6 +343,12 @@ models: ci_log_max_lines: 120 +enabled_pr_labels: + - running + - done + - merged + - auto_merge_requested + state_comment_timezone: "JST" repositories: diff --git a/src/auto_fixer.py b/src/auto_fixer.py index a83ea59..23e23f8 100644 --- a/src/auto_fixer.py +++ b/src/auto_fixer.py @@ -102,6 +102,16 @@ REFIX_DONE_LABEL = "refix:done" REFIX_MERGED_LABEL = "refix:merged" REFIX_AUTO_MERGE_REQUESTED_LABEL = "refix:auto-merge-requested" +PR_LABEL_KEY_TO_NAME: dict[str, str] = { + "running": REFIX_RUNNING_LABEL, + "done": REFIX_DONE_LABEL, + "merged": REFIX_MERGED_LABEL, + "auto_merge_requested": REFIX_AUTO_MERGE_REQUESTED_LABEL, +} +PR_LABEL_NAME_TO_KEY: dict[str, str] = { + label_name: label_key for label_key, label_name in PR_LABEL_KEY_TO_NAME.items() +} +DEFAULT_ENABLED_PR_LABEL_KEYS: tuple[str, ...] = tuple(PR_LABEL_KEY_TO_NAME.keys()) CODERABBIT_PROCESSING_MARKER = "Currently processing new changes in this PR." CODERABBIT_RATE_LIMIT_MARKER = "Rate limit exceeded" CODERABBIT_REVIEW_FAILED_MARKER = "## Review failed" @@ -131,6 +141,7 @@ }, "ci_log_max_lines": 120, "auto_merge": False, + "enabled_pr_labels": list(DEFAULT_ENABLED_PR_LABEL_KEYS), "coderabbit_auto_resume": False, "coderabbit_auto_resume_max_per_run": 1, "process_draft_prs": False, @@ -144,6 +155,7 @@ "models", "ci_log_max_lines", "auto_merge", + "enabled_pr_labels", "coderabbit_auto_resume", "coderabbit_auto_resume_max_per_run", "process_draft_prs", @@ -203,6 +215,35 @@ def get_process_draft_prs( ) +def get_enabled_pr_label_keys( + runtime_config: dict[str, Any], + default_config: dict[str, Any], +) -> set[str]: + """Extract enabled PR label keys from runtime config.""" + configured_labels = runtime_config.get( + "enabled_pr_labels", default_config["enabled_pr_labels"] + ) + if not isinstance(configured_labels, list): + configured_labels = default_config["enabled_pr_labels"] + return { + label_key + for label_key in configured_labels + if isinstance(label_key, str) and label_key in PR_LABEL_KEY_TO_NAME + } + + +def _resolve_enabled_pr_label_keys( + enabled_pr_label_keys: set[str] | None = None, +) -> set[str]: + if enabled_pr_label_keys is None: + return set(DEFAULT_ENABLED_PR_LABEL_KEYS) + return { + label_key + for label_key in enabled_pr_label_keys + if label_key in PR_LABEL_KEY_TO_NAME + } + + def load_config(filepath: str) -> dict[str, Any]: """Load and validate YAML config.""" try: @@ -229,6 +270,7 @@ def load_config(filepath: str) -> dict[str, Any]: "models": dict(DEFAULT_CONFIG["models"]), "ci_log_max_lines": DEFAULT_CONFIG["ci_log_max_lines"], "auto_merge": DEFAULT_CONFIG["auto_merge"], + "enabled_pr_labels": list(DEFAULT_CONFIG["enabled_pr_labels"]), "coderabbit_auto_resume": DEFAULT_CONFIG["coderabbit_auto_resume"], "coderabbit_auto_resume_max_per_run": DEFAULT_CONFIG[ "coderabbit_auto_resume_max_per_run" @@ -280,6 +322,34 @@ def load_config(filepath: str) -> dict[str, Any]: sys.exit(1) config["auto_merge"] = auto_merge + enabled_pr_labels = parsed.get("enabled_pr_labels") + if enabled_pr_labels is not None: + if not isinstance(enabled_pr_labels, list): + print("Error: enabled_pr_labels must be a list.", file=sys.stderr) + sys.exit(1) + normalized_enabled_labels: list[str] = [] + seen_enabled_labels: set[str] = set() + allowed_label_keys = ", ".join(sorted(PR_LABEL_KEY_TO_NAME.keys())) + for index, label_key in enumerate(enabled_pr_labels): + if not isinstance(label_key, str) or not label_key.strip(): + print( + f"Error: enabled_pr_labels[{index}] must be a non-empty string.", + file=sys.stderr, + ) + sys.exit(1) + normalized_label_key = label_key.strip() + if normalized_label_key not in PR_LABEL_KEY_TO_NAME: + print( + f"Error: enabled_pr_labels[{index}] must be one of: {allowed_label_keys}.", + file=sys.stderr, + ) + sys.exit(1) + if normalized_label_key in seen_enabled_labels: + continue + seen_enabled_labels.add(normalized_label_key) + normalized_enabled_labels.append(normalized_label_key) + config["enabled_pr_labels"] = normalized_enabled_labels + coderabbit_auto_resume = parsed.get("coderabbit_auto_resume") if coderabbit_auto_resume is not None: if not isinstance(coderabbit_auto_resume, bool): @@ -1341,34 +1411,53 @@ def _ensure_repo_label_exists( return False -def _ensure_refix_labels(repo: str) -> None: - _ensure_repo_label_exists( - repo, - REFIX_RUNNING_LABEL, - color=REFIX_RUNNING_LABEL_COLOR, - description="Refix is currently processing review fixes.", - ) - _ensure_repo_label_exists( - repo, - REFIX_DONE_LABEL, - color=REFIX_DONE_LABEL_COLOR, - description="Refix finished review checks/fixes for now.", - ) - _ensure_repo_label_exists( - repo, - REFIX_MERGED_LABEL, - 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 _ensure_refix_labels( + repo: str, *, enabled_pr_label_keys: set[str] | None = None +) -> None: + enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) + if "running" in enabled: + _ensure_repo_label_exists( + repo, + REFIX_RUNNING_LABEL, + color=REFIX_RUNNING_LABEL_COLOR, + description="Refix is currently processing review fixes.", + ) + if "done" in enabled: + _ensure_repo_label_exists( + repo, + REFIX_DONE_LABEL, + color=REFIX_DONE_LABEL_COLOR, + description="Refix finished review checks/fixes for now.", + ) + if "merged" in enabled: + _ensure_repo_label_exists( + repo, + REFIX_MERGED_LABEL, + color=REFIX_MERGED_LABEL_COLOR, + description="PR has been merged after Refix auto-merge.", + ) + if "auto_merge_requested" in enabled: + _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, + enabled_pr_label_keys: set[str] | None = None, +) -> bool: + enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) + label_key = PR_LABEL_NAME_TO_KEY.get(label) + if label_key is not None and label_key not in enabled: + return True -def _edit_pr_label(repo: str, pr_number: int, *, add: bool, label: str) -> bool: label_arg = "--add-label" if add else "--remove-label" cmd = [ "gh", @@ -1407,44 +1496,143 @@ def _edit_pr_label(repo: str, pr_number: int, *, add: bool, label: str) -> bool: def _set_pr_running_label( - repo: str, pr_number: int, *, pr_data: dict[str, Any] | None = None + repo: str, + pr_number: int, + *, + pr_data: dict[str, Any] | None = None, + enabled_pr_label_keys: set[str] | None = None, ) -> None: """Set refix:running, remove refix:done. Skips no-op edits to avoid updating PR.""" + enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) + running_enabled = "running" in enabled + done_enabled = "done" in enabled + if not running_enabled and not done_enabled: + return if ( pr_data - and _pr_has_label(pr_data, REFIX_RUNNING_LABEL) - and not _pr_has_label(pr_data, REFIX_DONE_LABEL) + and (not running_enabled or _pr_has_label(pr_data, REFIX_RUNNING_LABEL)) + and (not done_enabled or not _pr_has_label(pr_data, REFIX_DONE_LABEL)) ): return - _ensure_refix_labels(repo) - if pr_data is None or _pr_has_label(pr_data, REFIX_DONE_LABEL): - _edit_pr_label(repo, pr_number, add=False, label=REFIX_DONE_LABEL) - if pr_data is None or not _pr_has_label(pr_data, REFIX_RUNNING_LABEL): - _edit_pr_label(repo, pr_number, add=True, label=REFIX_RUNNING_LABEL) + if enabled_pr_label_keys is None: + _ensure_refix_labels(repo) + else: + _ensure_refix_labels(repo, enabled_pr_label_keys=enabled) + if done_enabled and (pr_data is None or _pr_has_label(pr_data, REFIX_DONE_LABEL)): + if enabled_pr_label_keys is None: + _edit_pr_label(repo, pr_number, add=False, label=REFIX_DONE_LABEL) + else: + _edit_pr_label( + repo, + pr_number, + add=False, + label=REFIX_DONE_LABEL, + enabled_pr_label_keys=enabled, + ) + if running_enabled and ( + pr_data is None or not _pr_has_label(pr_data, REFIX_RUNNING_LABEL) + ): + if enabled_pr_label_keys is None: + _edit_pr_label(repo, pr_number, add=True, label=REFIX_RUNNING_LABEL) + else: + _edit_pr_label( + repo, + pr_number, + add=True, + label=REFIX_RUNNING_LABEL, + enabled_pr_label_keys=enabled, + ) def _set_pr_done_label( - repo: str, pr_number: int, *, pr_data: dict[str, Any] | None = None + repo: str, + pr_number: int, + *, + pr_data: dict[str, Any] | None = None, + enabled_pr_label_keys: set[str] | None = None, ) -> None: """Set refix:done, remove refix:running. Skips no-op edits to avoid updating PR.""" + enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) + done_enabled = "done" in enabled + running_enabled = "running" in enabled + if not done_enabled and not running_enabled: + return if ( pr_data - and _pr_has_label(pr_data, REFIX_DONE_LABEL) - and not _pr_has_label(pr_data, REFIX_RUNNING_LABEL) + and (not done_enabled or _pr_has_label(pr_data, REFIX_DONE_LABEL)) + and (not running_enabled or not _pr_has_label(pr_data, REFIX_RUNNING_LABEL)) ): return - _ensure_refix_labels(repo) - if pr_data is None or _pr_has_label(pr_data, REFIX_RUNNING_LABEL): - _edit_pr_label(repo, pr_number, add=False, label=REFIX_RUNNING_LABEL) - if pr_data is None or not _pr_has_label(pr_data, REFIX_DONE_LABEL): - _edit_pr_label(repo, pr_number, add=True, label=REFIX_DONE_LABEL) + if enabled_pr_label_keys is None: + _ensure_refix_labels(repo) + else: + _ensure_refix_labels(repo, enabled_pr_label_keys=enabled) + if running_enabled and ( + pr_data is None or _pr_has_label(pr_data, REFIX_RUNNING_LABEL) + ): + if enabled_pr_label_keys is None: + _edit_pr_label(repo, pr_number, add=False, label=REFIX_RUNNING_LABEL) + else: + _edit_pr_label( + repo, + pr_number, + add=False, + label=REFIX_RUNNING_LABEL, + enabled_pr_label_keys=enabled, + ) + if done_enabled and ( + pr_data is None or not _pr_has_label(pr_data, REFIX_DONE_LABEL) + ): + if enabled_pr_label_keys is None: + _edit_pr_label(repo, pr_number, add=True, label=REFIX_DONE_LABEL) + else: + _edit_pr_label( + repo, + pr_number, + add=True, + label=REFIX_DONE_LABEL, + enabled_pr_label_keys=enabled, + ) -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) +def _set_pr_merged_label( + repo: str, pr_number: int, *, enabled_pr_label_keys: set[str] | None = None +) -> None: + enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) + if not ( + "running" in enabled or "auto_merge_requested" in enabled or "merged" in enabled + ): + return + if enabled_pr_label_keys is 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) + else: + _ensure_refix_labels(repo, enabled_pr_label_keys=enabled) + _edit_pr_label( + repo, + pr_number, + add=False, + label=REFIX_RUNNING_LABEL, + enabled_pr_label_keys=enabled, + ) + _edit_pr_label( + repo, + pr_number, + add=False, + label=REFIX_AUTO_MERGE_REQUESTED_LABEL, + enabled_pr_label_keys=enabled, + ) + _edit_pr_label( + repo, + pr_number, + add=True, + label=REFIX_MERGED_LABEL, + enabled_pr_label_keys=enabled, + ) def _pr_has_label(pr_data: dict[str, Any], label_name: str) -> bool: @@ -1457,8 +1645,13 @@ def _pr_has_label(pr_data: dict[str, Any], label_name: str) -> bool: return False -def _mark_pr_merged_label_if_needed(repo: str, pr_number: int) -> bool: +def _mark_pr_merged_label_if_needed( + repo: str, pr_number: int, *, enabled_pr_label_keys: set[str] | None = None +) -> bool: """Add refix:merged label when PR is merged and eligible.""" + enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) + if "merged" not in enabled: + return False cmd = [ "gh", "pr", @@ -1496,20 +1689,35 @@ def _mark_pr_merged_label_if_needed(repo: str, pr_number: int) -> bool: merged_at = str(pr_data.get("mergedAt") or "").strip() if not merged_at: return False - if not _pr_has_label(pr_data, REFIX_DONE_LABEL): + if "done" in enabled and not _pr_has_label(pr_data, REFIX_DONE_LABEL): return False - if not _pr_has_label(pr_data, REFIX_AUTO_MERGE_REQUESTED_LABEL): + if "auto_merge_requested" in enabled and not _pr_has_label( + pr_data, REFIX_AUTO_MERGE_REQUESTED_LABEL + ): return False if _pr_has_label(pr_data, REFIX_MERGED_LABEL): return False print(f"PR #{pr_number} is merged; adding {REFIX_MERGED_LABEL} label.") - _set_pr_merged_label(repo, pr_number) + if enabled_pr_label_keys is None: + _set_pr_merged_label(repo, pr_number) + else: + _set_pr_merged_label(repo, pr_number, enabled_pr_label_keys=enabled) return True -def _backfill_merged_labels(repo: str, *, limit: int = 100) -> int: +def _backfill_merged_labels( + repo: str, + *, + limit: int = 100, + enabled_pr_label_keys: set[str] | None = None, +) -> int: """Backfill refix:merged label for merged PRs already marked refix:done.""" + enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) + if "merged" not in enabled: + return 0 + if "done" not in enabled or "auto_merge_requested" not in enabled: + return 0 search_query = f'label:"{REFIX_DONE_LABEL}" label:"{REFIX_AUTO_MERGE_REQUESTED_LABEL}" -label:"{REFIX_MERGED_LABEL}"' cmd = [ "gh", @@ -1557,14 +1765,22 @@ def _backfill_merged_labels(repo: str, *, limit: int = 100) -> int: pr_number = pr.get("number") if not isinstance(pr_number, int): continue - if _mark_pr_merged_label_if_needed(repo, pr_number): + if enabled_pr_label_keys is None: + marked = _mark_pr_merged_label_if_needed(repo, pr_number) + else: + marked = _mark_pr_merged_label_if_needed( + repo, pr_number, enabled_pr_label_keys=enabled + ) + if marked: count += 1 if count: print(f"Backfilled {REFIX_MERGED_LABEL} on {count} merged PR(s) in {repo}.") return count -def _trigger_pr_auto_merge(repo: str, pr_number: int) -> bool: +def _trigger_pr_auto_merge( + repo: str, pr_number: int, *, enabled_pr_label_keys: set[str] | None = None +) -> bool: cmd = ["gh", "pr", "merge", str(pr_number), "--repo", repo, "--auto", "--merge"] result = subprocess.run( cmd, @@ -1576,7 +1792,11 @@ def _trigger_pr_auto_merge(repo: str, pr_number: int) -> bool: if result.returncode == 0: print(f"Auto-merge requested for PR #{pr_number}.") return _edit_pr_label( - repo, pr_number, add=True, label=REFIX_AUTO_MERGE_REQUESTED_LABEL + repo, + pr_number, + add=True, + label=REFIX_AUTO_MERGE_REQUESTED_LABEL, + enabled_pr_label_keys=enabled_pr_label_keys, ) stderr_text = (result.stderr or "").strip() @@ -1585,7 +1805,11 @@ def _trigger_pr_auto_merge(repo: str, pr_number: int) -> bool: 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 + repo, + pr_number, + add=True, + label=REFIX_AUTO_MERGE_REQUESTED_LABEL, + enabled_pr_label_keys=enabled_pr_label_keys, ) return True @@ -2059,6 +2283,7 @@ def _update_done_label_if_completed( auto_merge_enabled: bool = False, coderabbit_rate_limit_active: bool = False, coderabbit_review_failed_active: bool = False, + enabled_pr_label_keys: set[str] | None = None, ) -> None: if dry_run or summarize_only: return @@ -2100,17 +2325,47 @@ def _update_done_label_if_completed( print( f"PR #{pr_number} meets completion conditions; switching label to {REFIX_DONE_LABEL}." ) - _set_pr_done_label(repo, pr_number, pr_data=pr_data) + if enabled_pr_label_keys is None: + _set_pr_done_label(repo, pr_number, pr_data=pr_data) + else: + _set_pr_done_label( + repo, + pr_number, + pr_data=pr_data, + enabled_pr_label_keys=enabled_pr_label_keys, + ) if auto_merge_enabled: - merge_requested = _trigger_pr_auto_merge(repo, pr_number) + if enabled_pr_label_keys is None: + merge_requested = _trigger_pr_auto_merge(repo, pr_number) + else: + merge_requested = _trigger_pr_auto_merge( + repo, + pr_number, + enabled_pr_label_keys=enabled_pr_label_keys, + ) if merge_requested: - _mark_pr_merged_label_if_needed(repo, pr_number) + if enabled_pr_label_keys is None: + _mark_pr_merged_label_if_needed(repo, pr_number) + else: + _mark_pr_merged_label_if_needed( + repo, + pr_number, + enabled_pr_label_keys=enabled_pr_label_keys, + ) return print( f"PR #{pr_number} is not completed yet; switching label to {REFIX_RUNNING_LABEL}." ) - _set_pr_running_label(repo, pr_number, pr_data=pr_data) + if enabled_pr_label_keys is None: + _set_pr_running_label(repo, pr_number, pr_data=pr_data) + else: + _set_pr_running_label( + repo, + pr_number, + pr_data=pr_data, + enabled_pr_label_keys=enabled_pr_label_keys, + ) def _process_single_pr( @@ -2127,6 +2382,7 @@ def _process_single_pr( auto_resume_run_state: dict[str, int], process_draft_prs: bool, state_comment_timezone: str, + enabled_pr_label_keys: set[str], max_modified_prs: int, max_committed_prs: int, max_claude_prs: int, @@ -2279,7 +2535,12 @@ def _process_single_pr( f"(wait={active_rate_limit['wait_text']}, resume_after={active_rate_limit['resume_after'].isoformat()})" ) if not dry_run and not summarize_only: - _set_pr_running_label(repo, pr_number, pr_data=pr_data) + _set_pr_running_label( + repo, + pr_number, + pr_data=pr_data, + enabled_pr_label_keys=enabled_pr_label_keys, + ) modified_prs.add((repo, pr_number)) posted_resume_comment = _maybe_auto_resume_coderabbit_review( repo=repo, @@ -2307,7 +2568,12 @@ def _process_single_pr( f"CodeRabbit review failed status is active for PR #{pr_number}; head commit changed during review." ) if not dry_run and not summarize_only: - _set_pr_running_label(repo, pr_number, pr_data=pr_data) + _set_pr_running_label( + repo, + pr_number, + pr_data=pr_data, + enabled_pr_label_keys=enabled_pr_label_keys, + ) modified_prs.add((repo, pr_number)) can_attempt_resume = True if active_rate_limit and active_rate_limit["resume_after"] > datetime.now( @@ -2358,6 +2624,7 @@ def _process_single_pr( auto_merge_enabled=auto_merge_enabled, coderabbit_rate_limit_active=bool(active_rate_limit), coderabbit_review_failed_active=bool(active_review_failed), + enabled_pr_label_keys=enabled_pr_label_keys, ) modified_prs.add((repo, pr_number)) return False, count_pr, None @@ -2690,6 +2957,7 @@ def _process_single_pr( auto_merge_enabled=auto_merge_enabled, coderabbit_rate_limit_active=bool(active_rate_limit), coderabbit_review_failed_active=bool(active_review_failed), + enabled_pr_label_keys=enabled_pr_label_keys, ) if commits_by_phase: return False, True, (repo, pr_number, "\n".join(commits_by_phase)) @@ -2734,6 +3002,7 @@ def _process_single_pr( auto_merge_enabled=auto_merge_enabled, coderabbit_rate_limit_active=bool(active_rate_limit), coderabbit_review_failed_active=bool(active_review_failed), + enabled_pr_label_keys=enabled_pr_label_keys, ) modified_prs.add((repo, pr_number)) if commits_by_phase: @@ -2812,7 +3081,12 @@ def _process_single_pr( else: _remove_running_on_exit = False try: - _set_pr_running_label(repo, pr_number, pr_data=pr_data) + _set_pr_running_label( + repo, + pr_number, + pr_data=pr_data, + enabled_pr_label_keys=enabled_pr_label_keys, + ) _remove_running_on_exit = True review_fix_started = True review_report_path = _build_phase_report_path( @@ -2965,7 +3239,13 @@ def _process_single_pr( print(f" stderr: {e.stderr.strip()}", file=sys.stderr) finally: if _remove_running_on_exit: - _edit_pr_label(repo, pr_number, add=False, label=REFIX_RUNNING_LABEL) + _edit_pr_label( + repo, + pr_number, + add=False, + label=REFIX_RUNNING_LABEL, + enabled_pr_label_keys=enabled_pr_label_keys, + ) _update_done_label_if_completed( repo=repo, @@ -2984,6 +3264,7 @@ def _process_single_pr( auto_merge_enabled=auto_merge_enabled, coderabbit_rate_limit_active=bool(active_rate_limit), coderabbit_review_failed_active=bool(active_review_failed), + enabled_pr_label_keys=enabled_pr_label_keys, ) modified_prs.add((repo, pr_number)) @@ -3033,6 +3314,7 @@ def process_repo( runtime_config, DEFAULT_CONFIG, auto_resume_run_state ) process_draft_prs = get_process_draft_prs(runtime_config, DEFAULT_CONFIG) + enabled_pr_label_keys = get_enabled_pr_label_keys(runtime_config, DEFAULT_CONFIG) state_comment_timezone = ( str( runtime_config.get( @@ -3105,7 +3387,11 @@ def process_repo( backfill_limit = ( max(0, max_modified_prs - prev_total) if max_modified_prs > 0 else 100 ) - backfilled_count = _backfill_merged_labels(repo, limit=backfill_limit) + backfilled_count = _backfill_merged_labels( + repo, + limit=backfill_limit, + enabled_pr_label_keys=enabled_pr_label_keys, + ) if global_backfilled_count is not None: global_backfilled_count[0] += backfilled_count total_backfilled = ( @@ -3138,6 +3424,7 @@ def process_repo( auto_resume_run_state=auto_resume_run_state, process_draft_prs=process_draft_prs, state_comment_timezone=state_comment_timezone, + enabled_pr_label_keys=enabled_pr_label_keys, max_modified_prs=max_modified_prs, max_committed_prs=max_committed_prs, max_claude_prs=max_claude_prs, @@ -3172,11 +3459,15 @@ def process_repo( if max_modified_prs > 0: remaining = max_modified_prs - len(modified_prs) - total_backfilled if remaining > 0: - additional = _backfill_merged_labels(repo, limit=remaining) + additional = _backfill_merged_labels( + repo, + limit=remaining, + enabled_pr_label_keys=enabled_pr_label_keys, + ) if global_backfilled_count is not None: global_backfilled_count[0] += additional else: - _backfill_merged_labels(repo) + _backfill_merged_labels(repo, enabled_pr_label_keys=enabled_pr_label_keys) return commits_added_to diff --git a/tests/test_auto_fixer.py b/tests/test_auto_fixer.py index e3cd7d6..48e05a5 100644 --- a/tests/test_auto_fixer.py +++ b/tests/test_auto_fixer.py @@ -201,6 +201,12 @@ def test_valid_config_with_all_keys(self, tmp_path): }, "ci_log_max_lines": 250, "auto_merge": True, + "enabled_pr_labels": [ + "running", + "done", + "merged", + "auto_merge_requested", + ], "coderabbit_auto_resume": True, "coderabbit_auto_resume_max_per_run": 3, "process_draft_prs": False, @@ -236,6 +242,12 @@ def test_optional_keys_use_defaults(self, tmp_path): assert config["models"]["fix"] == "sonnet" assert config["ci_log_max_lines"] == 120 assert config["auto_merge"] is False + assert config["enabled_pr_labels"] == [ + "running", + "done", + "merged", + "auto_merge_requested", + ] assert config["coderabbit_auto_resume"] is False assert config["coderabbit_auto_resume_max_per_run"] == 1 assert config["process_draft_prs"] is False @@ -252,6 +264,48 @@ def test_auto_merge_requires_boolean(self, tmp_path): config_file.write_text( """ auto_merge: "true" +repositories: + - repo: owner/repo1 +""".strip() + ) + with pytest.raises(SystemExit) as exc_info: + auto_fixer.load_config(str(config_file)) + assert exc_info.value.code == 1 + + def test_enabled_pr_labels_can_be_subset(self, tmp_path): + config_file = tmp_path / "config.yaml" + config_file.write_text( + """ +enabled_pr_labels: + - running + - auto_merge_requested + - running +repositories: + - repo: owner/repo1 +""".strip() + ) + config = auto_fixer.load_config(str(config_file)) + assert config["enabled_pr_labels"] == ["running", "auto_merge_requested"] + + def test_enabled_pr_labels_can_be_empty(self, tmp_path): + config_file = tmp_path / "config.yaml" + config_file.write_text( + """ +enabled_pr_labels: [] +repositories: + - repo: owner/repo1 +""".strip() + ) + config = auto_fixer.load_config(str(config_file)) + assert config["enabled_pr_labels"] == [] + + def test_enabled_pr_labels_must_be_known_values(self, tmp_path): + config_file = tmp_path / "config.yaml" + config_file.write_text( + """ +enabled_pr_labels: + - running + - unknown repositories: - repo: owner/repo1 """.strip() @@ -1107,7 +1161,11 @@ def test_auto_merge_enabled_backfills_merged_labels_even_without_open_prs(self): patch("auto_fixer._backfill_merged_labels") as mock_backfill, ): auto_fixer.process_repo({"repo": "owner/repo"}, config=config) - mock_backfill.assert_called_once_with("owner/repo", limit=100) + mock_backfill.assert_called_once_with( + "owner/repo", + limit=100, + enabled_pr_label_keys={"running", "done", "merged", "auto_merge_requested"}, + ) def test_draft_pr_is_skipped_by_default(self): prs = [{"number": 1, "title": "Draft PR", "isDraft": True}] @@ -1726,7 +1784,12 @@ def test_review_fix_start_sets_running_label(self, tmp_path): ): auto_fixer.process_repo({"repo": "owner/repo"}) - mock_set_running.assert_called_once_with("owner/repo", 1, pr_data=pr_data) + mock_set_running.assert_called_once_with( + "owner/repo", + 1, + pr_data=pr_data, + enabled_pr_label_keys={"running", "done", "merged", "auto_merge_requested"}, + ) def test_process_repo_passes_state_comment_timezone_to_create_state_entry( self, tmp_path @@ -1884,6 +1947,50 @@ def test_set_pr_merged_label_ensures_labels_before_edit(self): ] ) + def test_set_pr_running_label_noop_when_running_and_done_disabled(self): + with ( + patch("auto_fixer._ensure_refix_labels") as mock_ensure, + patch("auto_fixer._edit_pr_label") as mock_edit, + ): + auto_fixer._set_pr_running_label( + "owner/repo", + 9, + enabled_pr_label_keys={"merged", "auto_merge_requested"}, + ) + mock_ensure.assert_not_called() + mock_edit.assert_not_called() + + def test_set_pr_running_label_removes_done_when_running_disabled(self): + pr_data = {"labels": [{"name": "refix:done"}]} + with ( + patch("auto_fixer._ensure_refix_labels") as mock_ensure, + patch("auto_fixer._edit_pr_label") as mock_edit, + ): + auto_fixer._set_pr_running_label( + "owner/repo", + 9, + pr_data=pr_data, + enabled_pr_label_keys={"done"}, + ) + mock_ensure.assert_called_once_with( + "owner/repo", enabled_pr_label_keys={"done"} + ) + mock_edit.assert_called_once_with( + "owner/repo", + 9, + add=False, + label="refix:done", + enabled_pr_label_keys={"done"}, + ) + + def test_backfill_merged_labels_skips_when_merged_label_disabled(self): + with patch("auto_fixer.subprocess.run") as mock_run: + count = auto_fixer._backfill_merged_labels( + "owner/repo", enabled_pr_label_keys={"running", "done"} + ) + assert count == 0 + mock_run.assert_not_called() + def test_trigger_pr_auto_merge_executes_gh_merge(self): with patch( "auto_fixer.subprocess.run", From c5182b1e310f3354467eafd71988a0fb47859663 Mon Sep 17 00:00:00 2001 From: HappyOnigiri <253838257+NodeMeld@users.noreply.github.com> Date: Thu, 12 Mar 2026 10:42:11 +0000 Subject: [PATCH 02/18] =?UTF-8?q?fix(auto=5Ffixer):=20=5Fmark=5Fpr=5Fmerge?= =?UTF-8?q?d=5Flabel=5Fif=5Fneeded=20=E3=82=92=E3=80=8Cmerged=E3=80=8D?= =?UTF-8?q?=E7=84=A1=E5=8A=B9=E3=81=A7=E3=82=82=20running/auto=5Fmerge=5Fr?= =?UTF-8?q?equested=20=E3=82=92=E9=99=A4=E5=8E=BB=E3=81=A7=E3=81=8D?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 「merged」が無効でも running/auto_merge_requested が有効なら _set_pr_merged_label() を呼び出す。 --- src/auto_fixer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auto_fixer.py b/src/auto_fixer.py index 23e23f8..9beee0e 100644 --- a/src/auto_fixer.py +++ b/src/auto_fixer.py @@ -1650,7 +1650,7 @@ def _mark_pr_merged_label_if_needed( ) -> bool: """Add refix:merged label when PR is merged and eligible.""" enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) - if "merged" not in enabled: + if not ({"running", "auto_merge_requested", "merged"} & enabled): return False cmd = [ "gh", From 99ee1a6eaddbf2b798ca2511ca81ca5e941e0f97 Mon Sep 17 00:00:00 2001 From: HappyOnigiri <253838257+NodeMeld@users.noreply.github.com> Date: Thu, 12 Mar 2026 10:42:26 +0000 Subject: [PATCH 03/18] =?UTF-8?q?fix(auto=5Ffixer):=20=5Fbackfill=5Fmerged?= =?UTF-8?q?=5Flabels=20=E3=82=92=20done/auto=5Fmerge=5Frequested=20?= =?UTF-8?q?=E3=81=AE=E7=89=87=E6=96=B9=E3=81=A0=E3=81=91=E6=9C=89=E5=8A=B9?= =?UTF-8?q?=E3=81=AA=E5=A0=B4=E5=90=88=E3=82=82=E5=8B=95=E4=BD=9C=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 条件を OR に変更し、search_query を有効なラベルのみで動的構築する。 --- src/auto_fixer.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/auto_fixer.py b/src/auto_fixer.py index 9beee0e..958ee03 100644 --- a/src/auto_fixer.py +++ b/src/auto_fixer.py @@ -1716,9 +1716,15 @@ def _backfill_merged_labels( enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) if "merged" not in enabled: return 0 - if "done" not in enabled or "auto_merge_requested" not in enabled: + if "done" not in enabled and "auto_merge_requested" not in enabled: return 0 - search_query = f'label:"{REFIX_DONE_LABEL}" label:"{REFIX_AUTO_MERGE_REQUESTED_LABEL}" -label:"{REFIX_MERGED_LABEL}"' + search_parts = [] + if "done" in enabled: + search_parts.append(f'label:"{REFIX_DONE_LABEL}"') + if "auto_merge_requested" in enabled: + search_parts.append(f'label:"{REFIX_AUTO_MERGE_REQUESTED_LABEL}"') + search_parts.append(f'-label:"{REFIX_MERGED_LABEL}"') + search_query = " ".join(search_parts) cmd = [ "gh", "pr", From c56cf927cb03679368b2305a54a5ad01d5f4842b Mon Sep 17 00:00:00 2001 From: HappyOnigiri <253838257+NodeMeld@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:12:17 +0000 Subject: [PATCH 04/18] =?UTF-8?q?fix(auto=5Ffixer):=20=5Ftrigger=5Fpr=5Fau?= =?UTF-8?q?to=5Fmerge=20=E3=81=8C=20=5Fensure=5Frefix=5Flabels=20=E3=82=92?= =?UTF-8?q?=E5=91=BC=E3=81=B0=E3=81=9A=E3=81=AB=20=5Fedit=5Fpr=5Flabel=20?= =?UTF-8?q?=E3=82=92=E5=AE=9F=E8=A1=8C=E3=81=97=E3=81=A6=E3=81=84=E3=81=9F?= =?UTF-8?q?=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit auto_merge_requested 単独など merged が無効な設定で refix:auto-merge-requested ラベルが 未作成のまま gh pr edit --add-label を実行し失敗していた。 merge 成功・already merged の各パスで _ensure_refix_labels を呼ぶよう修正した。 --- src/auto_fixer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/auto_fixer.py b/src/auto_fixer.py index 958ee03..645d89d 100644 --- a/src/auto_fixer.py +++ b/src/auto_fixer.py @@ -1787,6 +1787,7 @@ def _backfill_merged_labels( def _trigger_pr_auto_merge( repo: str, pr_number: int, *, enabled_pr_label_keys: set[str] | None = None ) -> bool: + enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) cmd = ["gh", "pr", "merge", str(pr_number), "--repo", repo, "--auto", "--merge"] result = subprocess.run( cmd, @@ -1797,6 +1798,7 @@ def _trigger_pr_auto_merge( ) if result.returncode == 0: print(f"Auto-merge requested for PR #{pr_number}.") + _ensure_refix_labels(repo, enabled_pr_label_keys=enabled) return _edit_pr_label( repo, pr_number, @@ -1810,6 +1812,7 @@ def _trigger_pr_auto_merge( combined_lower = f"{stdout_text}\n{stderr_text}".lower() if "already merged" in combined_lower: print(f"PR #{pr_number} is already merged.") + _ensure_refix_labels(repo, enabled_pr_label_keys=enabled) _edit_pr_label( repo, pr_number, From c9c81332b05219d7bade8a19fd5212836dd0650d Mon Sep 17 00:00:00 2001 From: HappyOnigiri <253838257+NodeMeld@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:14:52 +0000 Subject: [PATCH 05/18] =?UTF-8?q?fix(auto=5Ffixer):=20setter=20=E3=81=8C?= =?UTF-8?q?=20no-op=20=E3=81=A7=E3=82=82=20modified=5Fprs.add()=20?= =?UTF-8?q?=E3=81=8C=E5=91=BC=E3=81=B0=E3=82=8C=20max=5Fmodified=5Fprs=5Fp?= =?UTF-8?q?er=5Frun=20=E3=82=92=E6=B6=88=E8=B2=BB=E3=81=99=E3=82=8B?= =?UTF-8?q?=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _set_pr_running_label / _set_pr_done_label / _update_done_label_if_completed が no-op 早期リターンする場合に False を返すよう変更し、各呼び出し元で戻り値が True のときのみ modified_prs.add() を実行するようにした。 enabled_pr_labels: [] など一部ラベルが無効な設定で上限が不当に消費される問題を解消する。 --- src/auto_fixer.py | 72 ++++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/src/auto_fixer.py b/src/auto_fixer.py index 645d89d..f94d7eb 100644 --- a/src/auto_fixer.py +++ b/src/auto_fixer.py @@ -1501,19 +1501,19 @@ def _set_pr_running_label( *, pr_data: dict[str, Any] | None = None, enabled_pr_label_keys: set[str] | None = None, -) -> None: +) -> bool: """Set refix:running, remove refix:done. Skips no-op edits to avoid updating PR.""" enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) running_enabled = "running" in enabled done_enabled = "done" in enabled if not running_enabled and not done_enabled: - return + return False if ( pr_data and (not running_enabled or _pr_has_label(pr_data, REFIX_RUNNING_LABEL)) and (not done_enabled or not _pr_has_label(pr_data, REFIX_DONE_LABEL)) ): - return + return False if enabled_pr_label_keys is None: _ensure_refix_labels(repo) else: @@ -1542,6 +1542,7 @@ def _set_pr_running_label( label=REFIX_RUNNING_LABEL, enabled_pr_label_keys=enabled, ) + return True def _set_pr_done_label( @@ -1550,19 +1551,19 @@ def _set_pr_done_label( *, pr_data: dict[str, Any] | None = None, enabled_pr_label_keys: set[str] | None = None, -) -> None: +) -> bool: """Set refix:done, remove refix:running. Skips no-op edits to avoid updating PR.""" enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) done_enabled = "done" in enabled running_enabled = "running" in enabled if not done_enabled and not running_enabled: - return + return False if ( pr_data and (not done_enabled or _pr_has_label(pr_data, REFIX_DONE_LABEL)) and (not running_enabled or not _pr_has_label(pr_data, REFIX_RUNNING_LABEL)) ): - return + return False if enabled_pr_label_keys is None: _ensure_refix_labels(repo) else: @@ -1593,6 +1594,7 @@ def _set_pr_done_label( label=REFIX_DONE_LABEL, enabled_pr_label_keys=enabled, ) + return True def _set_pr_merged_label( @@ -2293,9 +2295,9 @@ def _update_done_label_if_completed( coderabbit_rate_limit_active: bool = False, coderabbit_review_failed_active: bool = False, enabled_pr_label_keys: set[str] | None = None, -) -> None: +) -> bool: if dry_run or summarize_only: - return + return False is_completed = True if review_fix_failed: @@ -2335,14 +2337,15 @@ def _update_done_label_if_completed( f"PR #{pr_number} meets completion conditions; switching label to {REFIX_DONE_LABEL}." ) if enabled_pr_label_keys is None: - _set_pr_done_label(repo, pr_number, pr_data=pr_data) + done_changed = _set_pr_done_label(repo, pr_number, pr_data=pr_data) else: - _set_pr_done_label( + done_changed = _set_pr_done_label( repo, pr_number, pr_data=pr_data, enabled_pr_label_keys=enabled_pr_label_keys, ) + merge_triggered = False if auto_merge_enabled: if enabled_pr_label_keys is None: merge_requested = _trigger_pr_auto_merge(repo, pr_number) @@ -2353,6 +2356,7 @@ def _update_done_label_if_completed( enabled_pr_label_keys=enabled_pr_label_keys, ) if merge_requested: + merge_triggered = True if enabled_pr_label_keys is None: _mark_pr_merged_label_if_needed(repo, pr_number) else: @@ -2361,20 +2365,19 @@ def _update_done_label_if_completed( pr_number, enabled_pr_label_keys=enabled_pr_label_keys, ) - return + return done_changed or merge_triggered print( f"PR #{pr_number} is not completed yet; switching label to {REFIX_RUNNING_LABEL}." ) if enabled_pr_label_keys is None: - _set_pr_running_label(repo, pr_number, pr_data=pr_data) - else: - _set_pr_running_label( - repo, - pr_number, - pr_data=pr_data, - enabled_pr_label_keys=enabled_pr_label_keys, - ) + return _set_pr_running_label(repo, pr_number, pr_data=pr_data) + return _set_pr_running_label( + repo, + pr_number, + pr_data=pr_data, + enabled_pr_label_keys=enabled_pr_label_keys, + ) def _process_single_pr( @@ -2544,13 +2547,13 @@ def _process_single_pr( f"(wait={active_rate_limit['wait_text']}, resume_after={active_rate_limit['resume_after'].isoformat()})" ) if not dry_run and not summarize_only: - _set_pr_running_label( + if _set_pr_running_label( repo, pr_number, pr_data=pr_data, enabled_pr_label_keys=enabled_pr_label_keys, - ) - modified_prs.add((repo, pr_number)) + ): + modified_prs.add((repo, pr_number)) posted_resume_comment = _maybe_auto_resume_coderabbit_review( repo=repo, pr_number=pr_number, @@ -2577,13 +2580,13 @@ def _process_single_pr( f"CodeRabbit review failed status is active for PR #{pr_number}; head commit changed during review." ) if not dry_run and not summarize_only: - _set_pr_running_label( + if _set_pr_running_label( repo, pr_number, pr_data=pr_data, enabled_pr_label_keys=enabled_pr_label_keys, - ) - modified_prs.add((repo, pr_number)) + ): + modified_prs.add((repo, pr_number)) can_attempt_resume = True if active_rate_limit and active_rate_limit["resume_after"] > datetime.now( timezone.utc @@ -2616,7 +2619,7 @@ def _process_single_pr( f"No unresolved reviews, not behind, and no failing CI for PR #{pr_number}" ) count_pr = bool(active_rate_limit) - _update_done_label_if_completed( + if _update_done_label_if_completed( repo=repo, pr_number=pr_number, has_review_targets=False, @@ -2634,8 +2637,8 @@ def _process_single_pr( coderabbit_rate_limit_active=bool(active_rate_limit), coderabbit_review_failed_active=bool(active_review_failed), enabled_pr_label_keys=enabled_pr_label_keys, - ) - modified_prs.add((repo, pr_number)) + ): + modified_prs.add((repo, pr_number)) return False, count_pr, None # B上限チェック: コミット追加PR数の上限に達しているか @@ -2994,7 +2997,7 @@ def _process_single_pr( f"Skipping review-fix for PR #{pr_number} because {skip_review_fix_reason}; " "CI repair and merge-base handling already ran." ) - _update_done_label_if_completed( + if _update_done_label_if_completed( repo=repo, pr_number=pr_number, has_review_targets=has_review_targets, @@ -3012,8 +3015,8 @@ def _process_single_pr( coderabbit_rate_limit_active=bool(active_rate_limit), coderabbit_review_failed_active=bool(active_review_failed), enabled_pr_label_keys=enabled_pr_label_keys, - ) - modified_prs.add((repo, pr_number)) + ): + modified_prs.add((repo, pr_number)) if commits_by_phase: return False, True, (repo, pr_number, "\n".join(commits_by_phase)) return False, True, None @@ -3256,7 +3259,7 @@ def _process_single_pr( enabled_pr_label_keys=enabled_pr_label_keys, ) - _update_done_label_if_completed( + if _update_done_label_if_completed( repo=repo, pr_number=pr_number, has_review_targets=has_review_targets, @@ -3274,9 +3277,8 @@ def _process_single_pr( coderabbit_rate_limit_active=bool(active_rate_limit), coderabbit_review_failed_active=bool(active_review_failed), enabled_pr_label_keys=enabled_pr_label_keys, - ) - - modified_prs.add((repo, pr_number)) + ): + modified_prs.add((repo, pr_number)) if commits_by_phase: return False, True, (repo, pr_number, "\n".join(commits_by_phase)) return False, True, None From 19dfa140cdeb41f19622ca88d39ee774e24372b2 Mon Sep 17 00:00:00 2001 From: HappyOnigiri <253838257+NodeMeld@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:15:38 +0000 Subject: [PATCH 06/18] =?UTF-8?q?fix(auto=5Ffixer):=20=5Fbackfill=5Fmerged?= =?UTF-8?q?=5Flabels=20=E3=81=8C=20merged=20=E7=84=A1=E5=8A=B9=E3=83=BBdon?= =?UTF-8?q?e/auto=5Fmerge=5Frequested=20=E7=84=A1=E5=8A=B9=E3=81=AE?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E3=81=A7=E3=82=B9=E3=82=AD=E3=83=83=E3=83=97?= =?UTF-8?q?=E3=81=95=E3=82=8C=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit line 1717 の guard を "merged" 必須から {"auto_merge_requested","merged"} いずれか存在に緩和し、 ["auto_merge_requested"] や ["running","auto_merge_requested"] でも backfill が動作するようにした。 line 1719 の guard に "running" を追加し ["running","merged"] でも動作するよう修正した。 search_parts の構築にも "running" ラベルを fallback として追加した。 --- src/auto_fixer.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/auto_fixer.py b/src/auto_fixer.py index f94d7eb..f11099a 100644 --- a/src/auto_fixer.py +++ b/src/auto_fixer.py @@ -1716,15 +1716,17 @@ def _backfill_merged_labels( ) -> int: """Backfill refix:merged label for merged PRs already marked refix:done.""" enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) - if "merged" not in enabled: + if not ({"auto_merge_requested", "merged"} & enabled): return 0 - if "done" not in enabled and "auto_merge_requested" not in enabled: + if "done" not in enabled and "auto_merge_requested" not in enabled and "running" not in enabled: return 0 search_parts = [] if "done" in enabled: search_parts.append(f'label:"{REFIX_DONE_LABEL}"') if "auto_merge_requested" in enabled: search_parts.append(f'label:"{REFIX_AUTO_MERGE_REQUESTED_LABEL}"') + if not search_parts and "running" in enabled: + search_parts.append(f'label:"{REFIX_RUNNING_LABEL}"') search_parts.append(f'-label:"{REFIX_MERGED_LABEL}"') search_query = " ".join(search_parts) cmd = [ From 59b3314faeecc199b8e04cfdac7cdfd76b3b3b6c Mon Sep 17 00:00:00 2001 From: HappyOnigiri <253838257+NodeMeld@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:22:07 +0000 Subject: [PATCH 07/18] =?UTF-8?q?fix(auto=5Ffixer):=20=5Fupdate=5Fdone=5Fl?= =?UTF-8?q?abel=5Fif=5Fcompleted=20=E3=81=AE=E6=88=BB=E3=82=8A=E5=80=A4?= =?UTF-8?q?=E3=81=8C=E7=A0=B4=E6=A3=84=E3=81=95=E3=82=8C=20modified=5Fprs?= =?UTF-8?q?=20=E3=81=8C=E6=9B=B4=E6=96=B0=E3=81=95=E3=82=8C=E3=81=AA?= =?UTF-8?q?=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auto_fixer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/auto_fixer.py b/src/auto_fixer.py index f11099a..c4804e5 100644 --- a/src/auto_fixer.py +++ b/src/auto_fixer.py @@ -2954,7 +2954,7 @@ def _process_single_pr( f"commits may not be pushed to origin/{branch_name}. " f"details: {unpushed_info}" ) - _update_done_label_if_completed( + if _update_done_label_if_completed( repo=repo, pr_number=pr_number, has_review_targets=False, @@ -2972,7 +2972,8 @@ def _process_single_pr( coderabbit_rate_limit_active=bool(active_rate_limit), coderabbit_review_failed_active=bool(active_review_failed), enabled_pr_label_keys=enabled_pr_label_keys, - ) + ): + modified_prs.add((repo, pr_number)) if commits_by_phase: return False, True, (repo, pr_number, "\n".join(commits_by_phase)) return False, True, None From 74f6ee2dbaf23fe3305c6e4ac1eb3378b1c61e7d Mon Sep 17 00:00:00 2001 From: HappyOnigiri <253838257+NodeMeld@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:22:44 +0000 Subject: [PATCH 08/18] =?UTF-8?q?fix(auto=5Ffixer):=20=5Fbackfill=5Fmerged?= =?UTF-8?q?=5Flabels=20=E3=81=AE=E3=82=B2=E3=83=BC=E3=83=88=E3=83=81?= =?UTF-8?q?=E3=82=A7=E3=83=83=E3=82=AF=E3=81=AB=20running=20=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0=E3=81=97=20running=20=E3=81=AE=E3=81=BF?= =?UTF-8?q?=E6=9C=89=E5=8A=B9=E3=81=AA=E8=A8=AD=E5=AE=9A=E3=81=A7=E3=82=82?= =?UTF-8?q?=E3=83=A9=E3=83=99=E3=83=AB=E3=82=AF=E3=83=AA=E3=83=BC=E3=83=B3?= =?UTF-8?q?=E3=82=A2=E3=83=83=E3=83=97=E3=81=8C=E5=8B=95=E4=BD=9C=E3=81=99?= =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auto_fixer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auto_fixer.py b/src/auto_fixer.py index c4804e5..61a12e9 100644 --- a/src/auto_fixer.py +++ b/src/auto_fixer.py @@ -1716,7 +1716,7 @@ def _backfill_merged_labels( ) -> int: """Backfill refix:merged label for merged PRs already marked refix:done.""" enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) - if not ({"auto_merge_requested", "merged"} & enabled): + if not ({"running", "auto_merge_requested", "merged"} & enabled): return 0 if "done" not in enabled and "auto_merge_requested" not in enabled and "running" not in enabled: return 0 From 5cbe1ee55e959e506998c67d44f250a811f9b406 Mon Sep 17 00:00:00 2001 From: HappyOnigiri <253838257+NodeMeld@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:32:42 +0000 Subject: [PATCH 09/18] =?UTF-8?q?fix(auto=5Ffixer):=20=5Fbackfill=5Fmerged?= =?UTF-8?q?=5Flabels=20=E3=81=8C=20merged=20=E7=84=A1=E5=8A=B9=E3=83=BBrun?= =?UTF-8?q?ning=20=E6=9C=89=E5=8A=B9=E3=81=AE=E8=A8=AD=E5=AE=9A=E3=81=A7?= =?UTF-8?q?=20subprocess.run=20=E3=82=92=E5=91=BC=E3=81=B6=E5=95=8F?= =?UTF-8?q?=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 第1ゲートチェックを {running, auto_merge_requested, merged} & enabled から 'merged' not in enabled に変更し、merged ラベルが無効な場合は即座にスキップするよう修正。 --- src/auto_fixer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auto_fixer.py b/src/auto_fixer.py index 61a12e9..2b979f5 100644 --- a/src/auto_fixer.py +++ b/src/auto_fixer.py @@ -1716,7 +1716,7 @@ def _backfill_merged_labels( ) -> int: """Backfill refix:merged label for merged PRs already marked refix:done.""" enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) - if not ({"running", "auto_merge_requested", "merged"} & enabled): + if "merged" not in enabled: return 0 if "done" not in enabled and "auto_merge_requested" not in enabled and "running" not in enabled: return 0 From 0c7d89a3fdf9dfd9f14f0262943bcaa87c2e6149 Mon Sep 17 00:00:00 2001 From: HappyOnigiri <253838257+NodeMeld@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:17:55 +0000 Subject: [PATCH 10/18] =?UTF-8?q?fix(auto=5Ffixer):=20=5Fbackfill=5Fmerged?= =?UTF-8?q?=5Flabels=20=E3=81=AE=E6=97=A9=E6=9C=9F=E3=83=AA=E3=82=BF?= =?UTF-8?q?=E3=83=BC=E3=83=B3=E6=9D=A1=E4=BB=B6=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit enabled_pr_labels に "running" や "auto_merge_requested" のみが含まれる場合も バックフィルが動作し、マージ済み PR のステールラベルを削除できるようにする。 Fixes: r3935883603 --- src/auto_fixer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auto_fixer.py b/src/auto_fixer.py index 729dd16..fe5f400 100644 --- a/src/auto_fixer.py +++ b/src/auto_fixer.py @@ -1817,7 +1817,7 @@ def _backfill_merged_labels( ) -> int: """Backfill refix:merged label for merged PRs already marked refix:done.""" enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) - if "merged" not in enabled: + if not ({"running", "auto_merge_requested", "merged"} & enabled): return 0 if "done" not in enabled and "auto_merge_requested" not in enabled and "running" not in enabled: return 0 From 3e1d7764769c32c81a2dbaf59d438b2b5c9e977a Mon Sep 17 00:00:00 2001 From: HappyOnigiri <253838257+NodeMeld@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:21:42 +0000 Subject: [PATCH 11/18] =?UTF-8?q?fix(auto=5Ffixer):=20=E3=83=A9=E3=83=99?= =?UTF-8?q?=E3=83=AB=E6=9B=B4=E6=96=B0=E3=83=98=E3=83=AB=E3=83=91=E3=83=BC?= =?UTF-8?q?=E3=81=8C=20gh=20pr=20edit=20=E5=A4=B1=E6=95=97=E6=99=82?= =?UTF-8?q?=E3=82=82=20True=20=E3=82=92=E8=BF=94=E3=81=99=E5=95=8F?= =?UTF-8?q?=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _set_pr_running_label / _set_pr_done_label / _set_pr_merged_label が _edit_pr_label の戻り値を無視して常に True を返していた問題を修正。 少なくとも 1 件のラベル変更に成功した場合のみ True を返すようにし、 modified_prs / backfill カウントの誤加算を防ぐ。 Fixes: discussion_r2924023703 --- src/auto_fixer.py | 76 ++++++++++++++++++++++++---------------- tests/test_auto_fixer.py | 12 +++---- 2 files changed, 52 insertions(+), 36 deletions(-) diff --git a/src/auto_fixer.py b/src/auto_fixer.py index fe5f400..569def5 100644 --- a/src/auto_fixer.py +++ b/src/auto_fixer.py @@ -1619,31 +1619,36 @@ def _set_pr_running_label( _ensure_refix_labels(repo) else: _ensure_refix_labels(repo, enabled_pr_label_keys=enabled) + changed = False if done_enabled and (pr_data is None or _pr_has_label(pr_data, REFIX_DONE_LABEL)): if enabled_pr_label_keys is None: - _edit_pr_label(repo, pr_number, add=False, label=REFIX_DONE_LABEL) + if _edit_pr_label(repo, pr_number, add=False, label=REFIX_DONE_LABEL): + changed = True else: - _edit_pr_label( + if _edit_pr_label( repo, pr_number, add=False, label=REFIX_DONE_LABEL, enabled_pr_label_keys=enabled, - ) + ): + changed = True if running_enabled and ( pr_data is None or not _pr_has_label(pr_data, REFIX_RUNNING_LABEL) ): if enabled_pr_label_keys is None: - _edit_pr_label(repo, pr_number, add=True, label=REFIX_RUNNING_LABEL) + if _edit_pr_label(repo, pr_number, add=True, label=REFIX_RUNNING_LABEL): + changed = True else: - _edit_pr_label( + if _edit_pr_label( repo, pr_number, add=True, label=REFIX_RUNNING_LABEL, enabled_pr_label_keys=enabled, - ) - return True + ): + changed = True + return changed def _set_pr_done_label( @@ -1669,73 +1674,86 @@ def _set_pr_done_label( _ensure_refix_labels(repo) else: _ensure_refix_labels(repo, enabled_pr_label_keys=enabled) + changed = False if running_enabled and ( pr_data is None or _pr_has_label(pr_data, REFIX_RUNNING_LABEL) ): if enabled_pr_label_keys is None: - _edit_pr_label(repo, pr_number, add=False, label=REFIX_RUNNING_LABEL) + if _edit_pr_label(repo, pr_number, add=False, label=REFIX_RUNNING_LABEL): + changed = True else: - _edit_pr_label( + if _edit_pr_label( repo, pr_number, add=False, label=REFIX_RUNNING_LABEL, enabled_pr_label_keys=enabled, - ) + ): + changed = True if done_enabled and ( pr_data is None or not _pr_has_label(pr_data, REFIX_DONE_LABEL) ): if enabled_pr_label_keys is None: - _edit_pr_label(repo, pr_number, add=True, label=REFIX_DONE_LABEL) + if _edit_pr_label(repo, pr_number, add=True, label=REFIX_DONE_LABEL): + changed = True else: - _edit_pr_label( + if _edit_pr_label( repo, pr_number, add=True, label=REFIX_DONE_LABEL, enabled_pr_label_keys=enabled, - ) - return True + ): + changed = True + return changed def _set_pr_merged_label( repo: str, pr_number: int, *, enabled_pr_label_keys: set[str] | None = None -) -> None: +) -> bool: enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) if not ( "running" in enabled or "auto_merge_requested" in enabled or "merged" in enabled ): - return + return False + changed = False if enabled_pr_label_keys is None: _ensure_refix_labels(repo) - _edit_pr_label(repo, pr_number, add=False, label=REFIX_RUNNING_LABEL) - _edit_pr_label( + if _edit_pr_label(repo, pr_number, add=False, label=REFIX_RUNNING_LABEL): + changed = True + if _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) + ): + changed = True + if _edit_pr_label(repo, pr_number, add=True, label=REFIX_MERGED_LABEL): + changed = True else: _ensure_refix_labels(repo, enabled_pr_label_keys=enabled) - _edit_pr_label( + if _edit_pr_label( repo, pr_number, add=False, label=REFIX_RUNNING_LABEL, enabled_pr_label_keys=enabled, - ) - _edit_pr_label( + ): + changed = True + if _edit_pr_label( repo, pr_number, add=False, label=REFIX_AUTO_MERGE_REQUESTED_LABEL, enabled_pr_label_keys=enabled, - ) - _edit_pr_label( + ): + changed = True + if _edit_pr_label( repo, pr_number, add=True, label=REFIX_MERGED_LABEL, enabled_pr_label_keys=enabled, - ) + ): + changed = True + return changed def _pr_has_label(pr_data: dict[str, Any], label_name: str) -> bool: @@ -1803,10 +1821,8 @@ def _mark_pr_merged_label_if_needed( print(f"PR #{pr_number} is merged; adding {REFIX_MERGED_LABEL} label.") if enabled_pr_label_keys is None: - _set_pr_merged_label(repo, pr_number) - else: - _set_pr_merged_label(repo, pr_number, enabled_pr_label_keys=enabled) - return True + return _set_pr_merged_label(repo, pr_number) + return _set_pr_merged_label(repo, pr_number, enabled_pr_label_keys=enabled) def _backfill_merged_labels( diff --git a/tests/test_auto_fixer.py b/tests/test_auto_fixer.py index 9526d72..bc0513a 100644 --- a/tests/test_auto_fixer.py +++ b/tests/test_auto_fixer.py @@ -1957,7 +1957,7 @@ def test_ensure_repo_label_exists_creates_when_missing(self): def test_set_pr_running_label_ensures_labels_before_edit(self): with ( patch("auto_fixer._ensure_refix_labels") as mock_ensure, - patch("auto_fixer._edit_pr_label") as mock_edit, + patch("auto_fixer._edit_pr_label", return_value=True) as mock_edit, ): auto_fixer._set_pr_running_label("owner/repo", 9) @@ -1996,7 +1996,7 @@ def test_set_pr_done_label_skips_edit_when_already_has_done(self): def test_set_pr_done_label_ensures_labels_before_edit(self): with ( patch("auto_fixer._ensure_refix_labels") as mock_ensure, - patch("auto_fixer._edit_pr_label") as mock_edit, + patch("auto_fixer._edit_pr_label", return_value=True) as mock_edit, ): auto_fixer._set_pr_done_label("owner/repo", 11) @@ -2011,7 +2011,7 @@ def test_set_pr_done_label_ensures_labels_before_edit(self): def test_set_pr_merged_label_ensures_labels_before_edit(self): with ( patch("auto_fixer._ensure_refix_labels") as mock_ensure, - patch("auto_fixer._edit_pr_label") as mock_edit, + patch("auto_fixer._edit_pr_label", return_value=True) as mock_edit, ): auto_fixer._set_pr_merged_label("owner/repo", 12) @@ -2060,10 +2060,10 @@ def test_set_pr_running_label_removes_done_when_running_disabled(self): enabled_pr_label_keys={"done"}, ) - def test_backfill_merged_labels_skips_when_merged_label_disabled(self): + def test_backfill_merged_labels_skips_when_no_merge_related_labels_enabled(self): with patch("auto_fixer.subprocess.run") as mock_run: count = auto_fixer._backfill_merged_labels( - "owner/repo", enabled_pr_label_keys={"running", "done"} + "owner/repo", enabled_pr_label_keys={"done"} ) assert count == 0 mock_run.assert_not_called() @@ -2105,7 +2105,7 @@ def test_mark_pr_merged_label_if_needed_adds_label_for_done_merged_pr(self): "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, + patch("auto_fixer._set_pr_merged_label", return_value=True) as mock_set_merged, ): ok = auto_fixer._mark_pr_merged_label_if_needed("owner/repo", 21) assert ok is True From 7b163eed392f397868fecc824ded6ab67c8ced45 Mon Sep 17 00:00:00 2001 From: HappyOnigiri <253838257+NodeMeld@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:38:11 +0000 Subject: [PATCH 12/18] =?UTF-8?q?fix(auto=5Ffixer):=20=5Ftrigger=5Fpr=5Fau?= =?UTF-8?q?to=5Fmerge=20=E3=82=92=20(merge=5Fstate=5Freached,=20modified)?= =?UTF-8?q?=20=E3=82=BF=E3=83=97=E3=83=AB=E3=81=AB=E5=A4=89=E6=9B=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit merge コマンドが成功した状態判定とラベル変更有無を分離することで、 auto_merge_requested ラベル操作の失敗時も _mark_pr_merged_label_if_needed が 呼ばれるよう修正。呼び出し元の merge_triggered もラベル実変更のみカウントする。 --- src/auto_fixer.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/auto_fixer.py b/src/auto_fixer.py index fd3778a..8ca6ba5 100644 --- a/src/auto_fixer.py +++ b/src/auto_fixer.py @@ -1914,7 +1914,12 @@ def _backfill_merged_labels( def _trigger_pr_auto_merge( repo: str, pr_number: int, *, enabled_pr_label_keys: set[str] | None = None -) -> bool: +) -> tuple[bool, bool]: + """Returns (merge_state_reached, modified). + + merge_state_reached: True if the GH merge command succeeded or the PR is already merged. + modified: True if a label was actually added/changed. + """ enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) cmd = ["gh", "pr", "merge", str(pr_number), "--repo", repo, "--auto", "--merge"] result = subprocess.run( @@ -1927,13 +1932,14 @@ def _trigger_pr_auto_merge( if result.returncode == 0: print(f"Auto-merge requested for PR #{pr_number}.") _ensure_refix_labels(repo, enabled_pr_label_keys=enabled) - return _edit_pr_label( + modified = _edit_pr_label( repo, pr_number, add=True, label=REFIX_AUTO_MERGE_REQUESTED_LABEL, enabled_pr_label_keys=enabled_pr_label_keys, ) + return True, modified stderr_text = (result.stderr or "").strip() stdout_text = (result.stdout or "").strip() @@ -1941,21 +1947,21 @@ def _trigger_pr_auto_merge( if "already merged" in combined_lower: print(f"PR #{pr_number} is already merged.") _ensure_refix_labels(repo, enabled_pr_label_keys=enabled) - _edit_pr_label( + modified = _edit_pr_label( repo, pr_number, add=True, label=REFIX_AUTO_MERGE_REQUESTED_LABEL, enabled_pr_label_keys=enabled_pr_label_keys, ) - return True + return True, modified details = stderr_text or stdout_text or "unknown error" print( f"Warning: failed to auto-merge PR #{pr_number}: {details}", file=sys.stderr, ) - return False + return False, False def _are_all_ci_checks_successful(repo: str, pr_number: int) -> bool: @@ -2474,15 +2480,14 @@ def _update_done_label_if_completed( merge_triggered = False if auto_merge_enabled: if enabled_pr_label_keys is None: - merge_requested = _trigger_pr_auto_merge(repo, pr_number) + merge_state_reached, label_modified = _trigger_pr_auto_merge(repo, pr_number) else: - merge_requested = _trigger_pr_auto_merge( + merge_state_reached, label_modified = _trigger_pr_auto_merge( repo, pr_number, enabled_pr_label_keys=enabled_pr_label_keys, ) - if merge_requested: - merge_triggered = True + if merge_state_reached: if enabled_pr_label_keys is None: _mark_pr_merged_label_if_needed(repo, pr_number) else: @@ -2491,6 +2496,7 @@ def _update_done_label_if_completed( pr_number, enabled_pr_label_keys=enabled_pr_label_keys, ) + merge_triggered = label_modified return done_changed or merge_triggered print( From bafbb12e0eaa6e619f885b8bb0bc88e73c56f8cb Mon Sep 17 00:00:00 2001 From: HappyOnigiri <253838257+NodeMeld@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:38:36 +0000 Subject: [PATCH 13/18] =?UTF-8?q?fix(auto=5Ffixer):=20=5Fedit=5Fpr=5Flabel?= =?UTF-8?q?=20=E3=81=8C=E7=84=A1=E5=8A=B9=E3=83=A9=E3=83=99=E3=83=AB?= =?UTF-8?q?=E3=81=AB=20True=20=E3=82=92=E8=BF=94=E3=81=99=E5=95=8F?= =?UTF-8?q?=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit enabled に含まれないラベルをスキップする際に True を返すと、呼び出し元 (_set_pr_merged_label など) が changed=True と誤カウントしていた。 False を返すことで、実際の add/remove が発生した場合のみ changed が立つよう修正。 --- src/auto_fixer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/auto_fixer.py b/src/auto_fixer.py index 8ca6ba5..adeb4e9 100644 --- a/src/auto_fixer.py +++ b/src/auto_fixer.py @@ -1564,7 +1564,7 @@ def _edit_pr_label( enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) label_key = PR_LABEL_NAME_TO_KEY.get(label) if label_key is not None and label_key not in enabled: - return True + return False label_arg = "--add-label" if add else "--remove-label" cmd = [ From ded973f75243f62ca7288f56aca5057faa57d81e Mon Sep 17 00:00:00 2001 From: HappyOnigiri <253838257+NodeMeld@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:39:03 +0000 Subject: [PATCH 14/18] =?UTF-8?q?fix(config):=20enabled=5Fpr=5Flabels=20?= =?UTF-8?q?=E3=81=AB=20"merged"=20=E5=8D=98=E7=8B=AC=E6=A7=8B=E6=88=90?= =?UTF-8?q?=E3=82=92=E4=B8=8D=E6=AD=A3=E3=81=A8=E3=81=97=E3=81=A6=E6=A4=9C?= =?UTF-8?q?=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "merged" のみを指定した場合、_backfill_merged_labels が running/done/ auto_merge_requested のいずれも無いため 0 を返し、実質サポートされない。 設定読み込み時に不正構成として検出し、エラーメッセージと exit(1) で終了する。 --- src/auto_fixer.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/auto_fixer.py b/src/auto_fixer.py index adeb4e9..6a28989 100644 --- a/src/auto_fixer.py +++ b/src/auto_fixer.py @@ -370,6 +370,16 @@ def load_config(filepath: str) -> dict[str, Any]: continue seen_enabled_labels.add(normalized_label_key) normalized_enabled_labels.append(normalized_label_key) + if "merged" in seen_enabled_labels and not ( + seen_enabled_labels & {"running", "done", "auto_merge_requested"} + ): + allowed_merge_sub_keys = ", ".join(sorted({"running", "done", "auto_merge_requested"})) + print( + f'Error: enabled_pr_labels includes "merged" but none of: {allowed_merge_sub_keys}. ' + f'At least one of these must be included alongside "merged".', + file=sys.stderr, + ) + sys.exit(1) config["enabled_pr_labels"] = normalized_enabled_labels coderabbit_auto_resume = parsed.get("coderabbit_auto_resume") From 52837af8c86f74a0785b46312593c9f00b97f206 Mon Sep 17 00:00:00 2001 From: HappyOnigiri <253838257+NodeMeld@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:41:57 +0000 Subject: [PATCH 15/18] =?UTF-8?q?fix(tests):=20=5Ftrigger=5Fpr=5Fauto=5Fme?= =?UTF-8?q?rge=20=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E3=82=92=E3=82=BF?= =?UTF-8?q?=E3=83=97=E3=83=AB=E6=88=BB=E3=82=8A=E5=80=A4=E3=81=AB=E5=AF=BE?= =?UTF-8?q?=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _trigger_pr_auto_merge が (merge_state_reached, modified) タプルを返すよう変更されたが、 テスト側が bool を期待したままだったため CI が失敗していた。 --- tests/test_auto_fixer.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_auto_fixer.py b/tests/test_auto_fixer.py index 4be7282..16f09f6 100644 --- a/tests/test_auto_fixer.py +++ b/tests/test_auto_fixer.py @@ -2170,9 +2170,9 @@ def test_trigger_pr_auto_merge_executes_gh_merge(self): "auto_fixer.subprocess.run", return_value=Mock(returncode=0, stdout="", stderr=""), ) as mock_run: - ok = auto_fixer._trigger_pr_auto_merge("owner/repo", 7) + merge_state_reached, _ = auto_fixer._trigger_pr_auto_merge("owner/repo", 7) - assert ok is True + assert merge_state_reached is True mock_run.assert_any_call( ["gh", "pr", "merge", "7", "--repo", "owner/repo", "--auto", "--merge"], capture_output=True, @@ -2188,9 +2188,9 @@ def test_trigger_pr_auto_merge_treats_already_merged_as_success(self): returncode=1, stdout="", stderr="pull request is already merged" ), ): - ok = auto_fixer._trigger_pr_auto_merge("owner/repo", 8) + merge_state_reached, _ = auto_fixer._trigger_pr_auto_merge("owner/repo", 8) - assert ok is True + assert merge_state_reached is True def test_mark_pr_merged_label_if_needed_adds_label_for_done_merged_pr(self): pr_view = { @@ -2321,7 +2321,7 @@ def test_update_done_label_triggers_auto_merge_when_enabled(self): patch("auto_fixer._set_pr_done_label") as mock_set_done, patch("auto_fixer._set_pr_running_label") as mock_set_running, patch( - "auto_fixer._trigger_pr_auto_merge", return_value=True + "auto_fixer._trigger_pr_auto_merge", return_value=(True, False) ) as mock_auto_merge, patch("auto_fixer._mark_pr_merged_label_if_needed") as mock_mark_merged, ): From e553a5dbea16d8dfc1a15ecb819e4317b4ec667d Mon Sep 17 00:00:00 2001 From: HappyOnigiri <253838257+NodeMeld@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:54:06 +0000 Subject: [PATCH 16/18] =?UTF-8?q?fix(auto=5Ffixer):=20review=5Ffix=5Fstart?= =?UTF-8?q?ed=20=E6=99=82=E3=81=AB=20=5Fset=5Fpr=5Fdone=5Flabel=20?= =?UTF-8?q?=E3=81=B8=20None=20=E3=82=92=E6=B8=A1=E3=81=97=20refix:running?= =?UTF-8?q?=20=E6=AE=8B=E7=95=99=E3=82=92=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auto_fixer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/auto_fixer.py b/src/auto_fixer.py index 6a28989..56c3e77 100644 --- a/src/auto_fixer.py +++ b/src/auto_fixer.py @@ -2478,13 +2478,14 @@ def _update_done_label_if_completed( print( f"PR #{pr_number} meets completion conditions; switching label to {REFIX_DONE_LABEL}." ) + current_pr_data = None if review_fix_started else pr_data if enabled_pr_label_keys is None: - done_changed = _set_pr_done_label(repo, pr_number, pr_data=pr_data) + done_changed = _set_pr_done_label(repo, pr_number, pr_data=current_pr_data) else: done_changed = _set_pr_done_label( repo, pr_number, - pr_data=pr_data, + pr_data=current_pr_data, enabled_pr_label_keys=enabled_pr_label_keys, ) merge_triggered = False From cbf0e08de0b63504f3ef86af599972de8c0fda0c Mon Sep 17 00:00:00 2001 From: HappyOnigiri <253838257+NodeMeld@users.noreply.github.com> Date: Thu, 12 Mar 2026 12:54:23 +0000 Subject: [PATCH 17/18] =?UTF-8?q?fix(auto=5Ffixer):=20merged=20=E3=81=8C?= =?UTF-8?q?=E7=84=A1=E5=8A=B9=E3=81=AA=E5=A0=B4=E5=90=88=E3=81=AB=20=5Fbac?= =?UTF-8?q?kfill=5Fmerged=5Flabels=20=E3=82=92=E5=8D=B3=E6=99=82=200=20?= =?UTF-8?q?=E8=BF=94=E3=81=97=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E4=BF=AE?= =?UTF-8?q?=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auto_fixer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/auto_fixer.py b/src/auto_fixer.py index 56c3e77..1480917 100644 --- a/src/auto_fixer.py +++ b/src/auto_fixer.py @@ -1850,6 +1850,8 @@ def _backfill_merged_labels( ) -> int: """Backfill refix:merged label for merged PRs already marked refix:done.""" enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) + if "merged" not in enabled: + return 0 if not ({"running", "auto_merge_requested", "merged"} & enabled): return 0 if "done" not in enabled and "auto_merge_requested" not in enabled and "running" not in enabled: From bd109614291162a13f90dc1d8a952ac8b62e2c4a Mon Sep 17 00:00:00 2001 From: HappyOnigiri <253838257+NodeMeld@users.noreply.github.com> Date: Thu, 12 Mar 2026 13:01:16 +0000 Subject: [PATCH 18/18] =?UTF-8?q?fix(auto=5Ffixer):=20=E5=88=B0=E9=81=94?= =?UTF-8?q?=E4=B8=8D=E8=83=BD=E3=81=AA=E5=86=97=E9=95=B7=E3=83=81=E3=82=A7?= =?UTF-8?q?=E3=83=83=E3=82=AF=EF=BC=881855-1856=E8=A1=8C=EF=BC=89=E3=82=92?= =?UTF-8?q?=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/auto_fixer.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/auto_fixer.py b/src/auto_fixer.py index 1480917..ccf6be4 100644 --- a/src/auto_fixer.py +++ b/src/auto_fixer.py @@ -1852,8 +1852,6 @@ def _backfill_merged_labels( enabled = _resolve_enabled_pr_label_keys(enabled_pr_label_keys) if "merged" not in enabled: return 0 - if not ({"running", "auto_merge_requested", "merged"} & enabled): - return 0 if "done" not in enabled and "auto_merge_requested" not in enabled and "running" not in enabled: return 0 search_parts = []