はじめに
LayerX バクラク事業部 Platform Engineering 部 Enabling グループの shibutani です。
CIのテストが落ちたとき、開発者がやることは意外と多いです。ログを読み、原因を特定し、担当者を探し修正依頼 or 自分で修正する。これがrace conditionやflaky testのように再現しにくいものだと、対応はさらに後回しにされがちです。
今回、Go testの失敗を検知したらClaudeが自動でログを分析し、担当チームに通知し、修正PRまで作成する仕組みを構築しました。本記事ではその設計と実装を紹介します。
-race フラグの分離と、その先の課題
出発点はPull Request作成時のCIの速度改善でした。これまではPull Request作成時のCIで -race フラグ付きの go test を実行していましたが、-race フラグはGoのrace detectorを有効にするオプションで、公式ドキュメントによるとメモリ使用量5〜10倍、実行時間2〜20倍のオーバーヘッドが発生します。Pull Requestのたびにこのコストがかかり、開発者のフィードバックループを遅くしていました。
Agentic codingの普及によりPull Requestの量が増えつつある今、CIのthroughputを上げることの重要性は高まっています(参考: ハーネスエンジニアリング:エージェントファーストの世界における Codex の活用)。そこで -race フラグをmainブランチへのpush後のCIに移し、Pull Request作成時のCIは -race なしで高速に実行する構成に変更しました。
しかし、単純に分離するだけでは新たな問題が生まれます。Pull Request作成時のCIで即座にフィードバックされていたrace conditionやflaky testが、mainブランチにマージされて初めて検出されるようになります。なお、バクラクではmainブランチへのマージが即本番デプロイされるわけではなく、QAなどの工程を経てリリースされるため、mainで検出しても本番への影響を防ぐ余地はあります。とはいえ、race conditionは「稀にしか起きない」「再現しにくい」、flaky testは「もう一回走らせたら通る」という性質から、mainブランチで失敗しても対応が後回しにされがちです。放置すればrace conditionは本番で影響の大きいバグとして発現し、flaky testはCIの信頼性を徐々に損ないます。
分離によるCI高速化のメリットを享受しつつ、検知した失敗が放置されない仕組みが必要でした。そこで、失敗の分析・担当チームへの通知・修正PRの作成までを自動化するパイプラインを構築しました。
全体のフロー
mainブランチへのpushをトリガーに、以下のフローで動作します。
flowchart TD
A[mainへのpush] --> B["go test -race<br/>testwrapper経由"]
B --> C{失敗あり?}
C -- No --> D[通知なし]
C -- Yes --> E[Claudeによる失敗ログ分析]
E --> F{"既知のflaky<br/>のみ?"}
F -- Yes --> G[通知なし]
F -- No --> H[CODEOWNERSからオーナーチームを特定]
H --> I[Slackにグループメンション付きで通知]
I --> K{"DATA RACE<br/>あり?"}
K -- Yes --> L[修正 Draft PR を作成]
K -- No --> M["Flaky Mark PR<br/>+ 修正 Draft PR を作成"]
ポイントは、すべての失敗を検知しつつ、既知のflaky testによるノイズは通知しないことです。通知の信頼性を保つことで、「またflakyか」と無視される状態を防ぎ、本当に対処が必要な失敗に開発者の注意を集中させます。
testwrapperによるテスト実行
通知の信頼性を保つためには、既知のflaky testと新規の失敗を区別する仕組みが必要です。
go test を直接実行する代わりに、社内で整備している testwrapper というCLIラッパーを経由して実行しています。この仕組みは Tailscaleのtestwrapper/flakytest を参考にしたもので、弊社の @upamuneが Go Conference 2025での発表 で詳しく解説しています。テスト関数内で flakytest.Mark() を呼ぶことで「このテストはflakyである」と宣言し、testwrapperがその情報を検知して自動的に最大3回までリトライします。
func TestSomething(t *testing.T) { flakytest.Mark(t, "https://github.com/org/repo/issues/123") // ... }
flakytest.Mark() の第2引数にはTracking IssueのURLを渡します。なぜflaky化されているのか、いつ解消する予定かをIssueで管理する運用です。
既知のflaky testのみで失敗した場合は、testwrapperが自動リトライを試みます。リトライで通ればそのまま成功として扱い、リトライしても失敗が残った場合でもSlack通知やPR作成は行いません。既知のflaky testとして管理されている以上、対応済みと判断するためです。それ以外の失敗が含まれる場合に、次のClaudeによる分析フローに進みます。
Claudeによる分析と修復
テスト失敗を検知したら、Claudeがログ分析から修正PRの作成までを一気通貫で行います。この自動化はGitHub Actionsのカスタムアクションとして実装しており、Anthropic SDKを通じてClaude APIを呼び出しています。
ログの絞り込み
CIのログをそのままClaudeに渡すのではなく、関連度の高い部分に絞り込んでからプロンプトに含めています。DATA RACEブロックが検出された場合はそのブロックを優先的に抽出し、そうでない場合は --- FAIL: の前後20行を抽出します。
ログ全体を渡すとコンテキストウィンドウを消費するだけでなく、関係のない情報にClaudeが引きずられるリスクもあります。ノイズの除去が分析精度の向上に直結するため、この前処理は重要です。
オーナーチームへの通知
失敗したテストのパッケージパスからCODEOWNERSを逆引きしてオーナーチームを特定し、GitHubチームとSlackグループのマッピングを通じてグループメンションを送ります。「テストが失敗した → 担当チームに通知が届く」という流れを自動化することで、誰にも気づかれないまま放置されるリスクを減らしています。

失敗種別に応じたPR自動作成
Claudeは失敗の種別に応じて、異なるPRを自動作成します。
DATA RACEが検出された場合、Claudeがソースコードを分析し、race conditionを修正するDraft PRを作成します。
DATA RACEなしだが未マークのテスト失敗の場合は、2種類のPRを作成します。
- FlakyをMarkするPR: 該当テストに
flakytest.Mark()を追加し、tracking Issueも同時に作成します。このPRをマージすると、次回以降はtestwrapperが自動リトライするようになり、Slackへの通知も止まります。 - Flakyを修正するDraft PR: テストの非決定性を除去する修正案をClaudeが生成します。
いずれもDraft PRはエンジニアがレビューするまでマージされません。Claudeが自動でコードを書きますが、最終的な判断は人間が行う設計です。
FlakyをMarkするPRをマージするだけで、そのテストに起因する通知が次回から止まります。これにより、対処すればするほどノイズが減り、通知の信頼性が上がっていく好循環が生まれます。
おわりに
今回構築した仕組みのポイントをまとめます。
-raceフラグをmainブランチのCIに分離し、Pull Request作成時のCIの高速化とrace condition検知を両立した- testwrapperと
flakytest.Mark()で既知のflaky testを自動リトライし、通知のノイズを除去した - Claudeによるログ分析・PR自動作成で、検知から修正提案までを自動化した
- CODEOWNERSの逆引きでオーナーチームにグループメンションし、通知の見落としを防いだ
race conditionもflaky testも、放置されがちな問題です。原因の特定が難しく、影響がすぐには見えにくいため、目の前のタスクに押されて後回しになりがちです。この仕組みでは、検知・通知・修正提案を自動化することで対処のハードルを下げ、開発者が本来の開発に集中できる環境を目指しました。