Skip to content

refactor: modularize auto_fixer.py#76

Merged
HappyOnigiri merged 3 commits intomainfrom
refactor/modularize-auto-fixer
Mar 13, 2026
Merged

refactor: modularize auto_fixer.py#76
HappyOnigiri merged 3 commits intomainfrom
refactor/modularize-auto-fixer

Conversation

@HappyOnigiri
Copy link
Owner

@HappyOnigiri HappyOnigiri commented Mar 13, 2026

Summary

This PR refactors the monolithic (4,174 lines) into smaller, specialized modules to reduce complexity and improve maintainability.

Module Breakdown

  • config.py: Configuration loading and validation
  • pr_label.py: PR label management
  • coderabbit.py: CodeRabbit rate limit and auto-resume logic
  • ci_check.py: CI status checks and CI-fix prompt generation
  • claude_runner.py: Claude CLI execution
  • prompt_builder.py: Prompt generation logic
  • git_ops.py: Git operations
  • report.py: Runtime report management

Other Changes

  • Moved --list-commands logic to Makefile.
  • Significantly reduced auto_fixer.py to 1,521 lines.
  • Updated all associated tests to pass with the new architecture.

CI checks and tests (173/173) are passing.

Summary by CodeRabbit

  • Refactor

    • 大規模な分割リファクタリングでコアロジックを専用モジュールに分離し、保守性と再利用性を向上しました。
  • New Features

    • PR向けのCI失敗解析・修復プロンプト生成、CodeRabbitの自動再開制御、Claude実行ラッパー、プロンプト生成器、実行レポート機能を追加しました。
  • Tests

    • テスト群を新しいモジュール境界に合わせて更新しました。
  • Chores

    • Makefileのヘルプ出力を日本語/英語で改善しました。

- Extract logic to config.py, pr_label.py, coderabbit.py, ci_check.py, claude_runner.py, prompt_builder.py, git_ops.py, and report.py
- Reduce auto_fixer.py from 4174 to 1521 lines
- Move --list-commands to Makefile
- Update tests to support new module structure and patch targets
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 13, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2e5f36ee-6b4f-49ad-b31b-4097d3a3ce13

📥 Commits

Reviewing files that changed from the base of the PR and between d30060d and 3874acc.

📒 Files selected for processing (2)
  • src/ci_check.py
  • src/git_ops.py

📝 Walkthrough

Walkthrough

auto_fixer.py を小型のオーケストレータへ縮小し、CI チェック、Claude 実行、CodeRabbit 統合、設定、Git 操作、PR ラベル、プロンプト生成、レポートをそれぞれの新規モジュールへ分割。Makefile の help ターゲット表示を簡素化。

Changes

Cohort / File(s) Summary
Makefile ヘルプ更新
Makefile
help / help-en の実行ロジックを cd src && python auto_fixer.py --list-commands* から複数の echo ステートメントによる固定表示へ置換。
オーケストレータ縮小
src/auto_fixer.py
大部分の内包ロジック(設定展開、CLI コマンド列挙、各種ユーティリティ)を削除し、外部モジュール(config, ci_check, coderabbit, pr_label, prompt_builder, claude_runner, git_ops, report 等)へ委譲するコーディネータに変更。
CI 関連
src/ci_check.py
失敗コンテキスト抽出、gh ログ抜粋・ダイジェスト生成、CI 修正プロンプト構築、CI 成否判定(グレース期含む)などの実装を追加。
Claude 実行
src/claude_runner.py
Claude CLI 実行、.claude 設定のマージ/永続化、プロンプト出力ファイル管理、タイムアウト/エラー分類、実行差分検出、ランタイムレポート出力を追加。
CodeRabbit 統合
src/coderabbit.py
ログイン判定、タイムスタンプ解析、レート制限/review-failed 抽出、再開ポスト/自動再開ロジック、処理マーカー検出などの実装を追加。
設定管理
src/config.py
YAML 設定の読み込み・検証・デフォルト適用、owner/* ワイルドカードリポジトリ展開、PR ラベルキー解決ユーティリティを追加。
Git 操作ユーティリティ
src/git_ops.py
リポジトリ準備(クローン/更新/チェックアウト/リセット)、ブランチ比較取得、ベースマージ必要判定、マージ実行(コンフリクト検出含む)を追加。
PR ラベル管理
src/pr_label.py
ラベル存在保証・作成、running/done/merged/auto-merge-requested 等のラベル遷移、バックフィル、オートマージ起動ロジック、ラベル更新用ユーティリティを追加。
プロンプト構築
src/prompt_builder.py
XML エスケープ、レビュー重要度推定、レビューデータ整形、PR 用プロンプト生成(generate_prompt)を追加。
レポート管理
src/report.py
フェーズ別レポート読み取り・整形・マージ・永続化、PR 状態コメント用ブロック生成、サイレント時の出力制御を追加。
テスト修正
tests/test_auto_fixer.py
機能分割に伴いテストを更新:auto_fixer 由来の関数参照を新モジュール(prompt_builder, config, ci_check, coderabbit, pr_label, claude_runner 等)へ差替え、subprocess 呼び出し参照をモジュール化。

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

refix:done

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed PRのタイトル「refactor: modularize auto_fixer.py」は、この変更セットの主要な目的を的確に要約しています。単一ファイルの大規模なモジュール化リファクタリングという中心的な変更が明確に示されています。
Description check ✅ Passed PR説明は詳細で充実しています。モジュール分割の内訳、各モジュールの役割、テスト状況が記載されていますが、提供されたテンプレートの「変更内容」「テスト」「関連issues」セクションの形式は厳密には従っていません。

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch refactor/modularize-auto-fixer
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/auto_fixer.py (2)

975-1018: ⚠️ Potential issue | 🟠 Major

dirty worktree を捨てた後に state を更新しないでください。

ここは Claude が未コミット変更を残したときに reset --hard / clean -fd で差分を破棄したうえで、そのまま後続の state comment 更新と thread 解決へ進みます。AI が修正を一部だけ commit して残りを未コミットで残したケースだと、必要な差分を落としたまま review を処理済み扱いにしてしまいます。

修正例
-            elif dirty_check.stdout.strip():
-                print(
-                    "Cleaning worktree (uncommitted work files; per assumption: correct work is committed)."
-                )
-                git_path = shutil.which("git")
-                if git_path is None:
-                    print(
-                        "Warning: git not found in PATH; skipping cleanup and state update.",
-                        file=sys.stderr,
-                    )
-                    should_update_state = False
-                else:
-                    try:
-                        subprocess.run(
-                            [git_path, "reset", "--hard", "HEAD"],
-                            cwd=str(works_dir),
-                            check=True,
-                            capture_output=True,
-                        )
-                        subprocess.run(
-                            [git_path, "clean", "-fd"],
-                            cwd=str(works_dir),
-                            check=True,
-                            capture_output=True,
-                        )
-                    except subprocess.CalledProcessError as e:
-                        print(
-                            f"Warning: git clean failed; skipping state update to allow retry: {e}",
-                            file=sys.stderr,
-                        )
-                        should_update_state = False
+            elif dirty_check.stdout.strip():
+                print(
+                    "Warning: worktree still dirty after Claude run; skipping state update to allow retry.",
+                    file=sys.stderr,
+                )
+                should_update_state = False
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/auto_fixer.py` around lines 975 - 1018, When an uncommitted dirty
worktree is detected (dirty_check.stdout non-empty) do not proceed to update
state even if you successfully run the git cleanup commands; specifically,
inside the branch that handles dirty_check.stdout.strip() set
should_update_state = False regardless of whether the git_path is found or the
subprocess.run reset/clean commands succeed (and still log warnings on
failures). Update the logic around dirty_check, works_dir, git_path and the
subprocess.run(reset/clean) calls so that cleanup is only attempted to restore
the worktree but the variable should_update_state is always cleared after
detecting uncommitted changes.

700-710: ⚠️ Potential issue | 🟠 Major

コンフリクト解消コミットが上限管理から漏れ、merge 完了も確認できていません。

conflict_commits が出ても committed_prs.add((repo, pr_number)) を呼んでいないので、max_committed_prs_per_run をこの経路だけ素通りできます。加えて、成功判定が not _has_merge_conflicts(works_dir) だけなので、Claude が競合マーカーを消しただけで merge commit を作らず MERGE_HEAD を残した場合も「resolved」と扱われます。

修正例
                 if conflict_commits:
                     commits_by_phase.append(conflict_commits)
+                    committed_prs.add((repo, pr_number))
                 claude_prs.add((repo, pr_number))
-                conflict_resolved = not _has_merge_conflicts(works_dir)
+                merge_head_result = subprocess.run(
+                    ["git", "rev-parse", "-q", "--verify", "MERGE_HEAD"],
+                    cwd=str(works_dir),
+                    capture_output=True,
+                    text=True,
+                    check=False,
+                )
+                conflict_resolved = (
+                    not _has_merge_conflicts(works_dir)
+                    and merge_head_result.returncode != 0
+                )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/auto_fixer.py` around lines 700 - 710, When conflict_commits is
non-empty, add the PR to committed_prs (e.g., committed_prs.add((repo,
pr_number))) so this path respects max_committed_prs_per_run, and tighten the
conflict_resolved check to require both !_has_merge_conflicts(works_dir) and
verification that a merge commit was actually created (e.g., ensure MERGE_HEAD
is cleared or confirm a merge commit exists in git history for works_dir) before
treating the PR as resolved; update the block that currently only adds to
claude_prs and checks _has_merge_conflicts to also add to committed_prs and
perform the merge-commit existence check using repo, pr_number, works_dir, and
_has_merge_conflicts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ci_check.py`:
- Around line 201-280: The <instructions> block currently includes
user-controlled PR title (escaped_title) and other CI-derived text, which can be
interpreted as directives by an LLM; modify the function that builds the return
string (the code that composes checks_block, digest_block, logs_block and then
returns the f-string with <instructions>) to keep <instructions> as a fixed,
static string only, and move all dynamic/pr-controlled values (escaped_title,
pr_number and extra_data) into separate data-only blocks (e.g., a new <pr_meta>
or reuse existing <ci_failure_logs>/<ci_error_digest>) appended after
<instructions>; update the return assembly so <instructions> contains no
interpolated variables and all user-controlled content is included only in the
dedicated data blocks (refer to variables escaped_title, pr_number, extra_data,
checks_block, digest_block, logs_block).

In `@src/claude_runner.py`:
- Around line 168-176: The call to subprocess.Popen followed by
process.communicate() in claude_runner.py has no timeout and can hang; update
the code that creates and communicates with the subprocess (the block using
subprocess.Popen and process.communicate) to pass a reasonable timeout (matching
summarizer.py), catch subprocess.TimeoutExpired, terminate/kill the child
process, read any remaining output, and raise a ClaudeCommandFailedError with
context; ensure stderr/stdout handling and cleanup mirror the pattern used in
src/summarizer.py so the job is aborted instead of blocking indefinitely.

In `@src/git_ops.py`:
- Around line 31-43: The current update path runs git reset --hard and git fetch
but leaves untracked files behind; after the existing subprocess.run calls for
["git", "reset", "--hard"] (and before or after fetch) run a git clean to remove
untracked/ignored files and directories for the reused worktree: invoke a
subprocess.run that executes git clean with flags to force removal of files and
directories (e.g., -f -d and -x as needed) using the same cwd=works_dir and
check=True, referencing the existing repo and works_dir variables so leftover
artifacts from prior PRs are fully removed.
- Around line 21-23: The code constructs works_dir from Path("../works") which
depends on the process CWD; change it to resolve relative to this module (so it
matches the same base used by main()/config). Replace Path("../works") with a
path computed from Path(__file__).resolve().parent (e.g.
Path(__file__).resolve().parent.parent / "works" or whatever ancestor matches
your project root), then use that base to build works_dir (where owner and
repo_name are used) and still call mkdir(parents=True, exist_ok=True); update
references to works_dir, owner, repo_name accordingly.

In `@src/prompt_builder.py`:
- Around line 193-202: The prompt currently injects the raw PR title directly
into the top-level <instructions> block (see the f-string return in
generate_prompt / prompt_builder.py), which allows user-controlled text to be
interpreted as instructions; remove {title} from inside the <instructions> block
and instead pass the title in a separate, clearly delimited data block (e.g.
<pr_title> or <metadata> placed outside <instructions>) alongside the existing
PR number and base_branch, so the instruction block remains invariant and the
title is treated as data.

---

Outside diff comments:
In `@src/auto_fixer.py`:
- Around line 975-1018: When an uncommitted dirty worktree is detected
(dirty_check.stdout non-empty) do not proceed to update state even if you
successfully run the git cleanup commands; specifically, inside the branch that
handles dirty_check.stdout.strip() set should_update_state = False regardless of
whether the git_path is found or the subprocess.run reset/clean commands succeed
(and still log warnings on failures). Update the logic around dirty_check,
works_dir, git_path and the subprocess.run(reset/clean) calls so that cleanup is
only attempted to restore the worktree but the variable should_update_state is
always cleared after detecting uncommitted changes.
- Around line 700-710: When conflict_commits is non-empty, add the PR to
committed_prs (e.g., committed_prs.add((repo, pr_number))) so this path respects
max_committed_prs_per_run, and tighten the conflict_resolved check to require
both !_has_merge_conflicts(works_dir) and verification that a merge commit was
actually created (e.g., ensure MERGE_HEAD is cleared or confirm a merge commit
exists in git history for works_dir) before treating the PR as resolved; update
the block that currently only adds to claude_prs and checks _has_merge_conflicts
to also add to committed_prs and perform the merge-commit existence check using
repo, pr_number, works_dir, and _has_merge_conflicts.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f1d7a542-7082-4302-8581-5e218d37b1a5

📥 Commits

Reviewing files that changed from the base of the PR and between 87bde2b and fb172a4.

📒 Files selected for processing (11)
  • Makefile
  • src/auto_fixer.py
  • src/ci_check.py
  • src/claude_runner.py
  • src/coderabbit.py
  • src/config.py
  • src/git_ops.py
  • src/pr_label.py
  • src/prompt_builder.py
  • src/report.py
  • tests/test_auto_fixer.py

- ci_check/prompt_builder: <instructions>からユーザー制御値を<pr_meta>に分離
- claude_runner: process.communicate()にタイムアウトを追加
- git_ops: CWD依存のパスをモジュール相対パスに変更、git clean追加
- auto_fixer: dirty worktree時のstate update防止、コンフリクト解消時のcommitted_prs追加とMERGE_HEAD検証
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ci_check.py`:
- Around line 303-329: The subprocess.run calls that fetch PR head SHA (assigned
to head_result) and check runs (assigned to result) inside
_collect_ci_failure_materials lack a timeout; add timeout=60 to both
subprocess.run invocations and wrap the calls (or the surrounding logic) to
catch subprocess.TimeoutExpired, logging a clear message and returning None
(same behavior as other failure paths) so the function doesn't hang if gh api
blocks.
- Around line 359-371: この subprocess.run 呼び出し (変数 commit_result に代入している箇所) に
timeout パラメータを追加し、タイムアウト発生時に subprocess.TimeoutExpired
を捕捉して適切にログ出力またはフォールバックするように修正してください;具体的には commit_result を作る subprocess.run(...,
timeout=<適切な秒数>, ...) に timeout 引数を追加し、同じ関数内で try/except
subprocess.TimeoutExpired: を追加してエラーメッセージを出力(またはエラー処理/リトライ/デフォルト値設定)するようにしてください。

In `@src/git_ops.py`:
- Around line 165-168: The current check inspects merge_result.stdout/stderr for
the English phrase "already up to date", which is locale-dependent; instead
capture the commit id (or tree) before the merge and compare it to the commit id
after the merge to detect whether anything changed. Concretely: before invoking
the merge save pre_merge_head = run_git("rev-parse", "HEAD"), perform the merge
(merge_result), then get post_merge_head = run_git("rev-parse", "HEAD") (or use
reflog HEAD@{1} if needed); if pre_merge_head == post_merge_head treat as no
changes (return (False, False) or equivalent), otherwise treat as merged
changes. Replace the current merge_output / "already up to date" string check
with this HEAD comparison using the existing merge_result, pre_merge_head and
post_merge_head variables.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 373f186a-bc26-4f88-9255-99f4025edade

📥 Commits

Reviewing files that changed from the base of the PR and between fb172a4 and d30060d.

📒 Files selected for processing (5)
  • src/auto_fixer.py
  • src/ci_check.py
  • src/claude_runner.py
  • src/git_ops.py
  • src/prompt_builder.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/prompt_builder.py

@HappyOnigiri HappyOnigiri added the refix:running Refix is currently processing review fixes. label Mar 13, 2026
@HappyOnigiri
Copy link
Owner Author

🤖 Refix Status

対応済みレビュー一覧
Comment ID 処理日時
r3940776875 2026-03-13 13:31:36 JST
r3941438628 2026-03-13 13:31:36 JST
discussion_r2928256967 2026-03-13 13:31:37 JST
discussion_r2928256990 2026-03-13 13:31:37 JST

@HappyOnigiri HappyOnigiri added refix:done Refix finished review checks/fixes for now. and removed refix:running Refix is currently processing review fixes. labels Mar 13, 2026
@HappyOnigiri HappyOnigiri merged commit 0e23935 into main Mar 13, 2026
3 checks passed
@HappyOnigiri HappyOnigiri deleted the refactor/modularize-auto-fixer branch March 13, 2026 04:40
@HappyOnigiri HappyOnigiri added refix:auto-merge-requested Refix has requested auto-merge for this PR. refix:merged PR has been merged after Refix auto-merge. and removed refix:auto-merge-requested Refix has requested auto-merge for this PR. labels Mar 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

refix:done Refix finished review checks/fixes for now. refix:merged PR has been merged after Refix auto-merge.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant