FLARES LLC

Technical document

PRのCI失敗をイベント駆動で自動修復する設計 — 安全なAIリペアループの作り方

CIが失敗するたびに、開発者がログを読み、修正をcommitし、再実行を待つ往復には時間がかかります。しかし、AIへ無制限の修正権限を渡せば、並行作業との競合、危険なファイルの変更、費用超過、無限ループという別の問題が生まれます。本稿では、AIを判断の権威にせず、失敗イベントごとに一度だけ小さな修復を試す設計を整理します。
AI開発 / CI・運用設計約20分公開日 2026年7月26日更新日 2026年7月26日
CIの失敗検知から安全なAI修正と再検証へ進むイベント駆動リペアループのイラスト

Summary

この文書の要点

  • 一つのCI失敗イベントにつき修復は一度だけ試し、修正後のCIが再び失敗した時は、その新しい完了イベントを次のサイクルの入口にします。
  • PR番号、開始時のhead SHA、workflow run IDを固定入力にし、分析前とpush直前の再検証で、別の変更へ誤って上書きしないようにします。
  • LLMの前後を決定的なfingerprint、変更可能path、差分規模、credential検査、GitとCIの検証で挟み、危険領域は常に人へ移管します。
  • 試行回数、同一失敗、費用、重複実行を永続状態で制御し、greenになってもmerge、release、deployは自動化しません。

減らしたいのは人間の往復であり、判断責任ではない

CI失敗への対応は、失敗したjobとstepを探し、ログから原因候補を絞り、関連するソースを直し、同じ検証を待つという反復です。型エラーや小さな実装漏れのように、原因と修正範囲が限定できる失敗でも、この往復のたびに開発者の作業が中断されます。

一方で、AIが失敗ログを読めることと、安全にpushできることは別問題です。ログやソースには命令文に見える文字列を埋め込めます。PRのheadは分析中にも変わり、同じイベントは重複配送され、複数のjobが同じ予算やbranchを取り合う可能性があります。モデルの回答だけを根拠に修正を続けると、権限と競合の問題を解けません。

設計目標は、AIをCI結果やbranch状態の権威にすることではありません。信頼済みの制御面が対象、状態、費用、差分、push可否を決め、AIはその境界内で原因分類とpatch候補の生成だけを担当します。自動化できない時に安全に止まり、必要な証拠を人へ渡せることも、修復成功と同じくらい重要です。

  • 自動化する対象: 原因が局所的で、既存の少数ファイルだけを小さく直せるCI失敗。
  • 人が持つ責任: 変更境界、予算、権限、品質ゲート、例外判断、merge以降の操作。
  • 安全側の既定値: 判断材料が不足した時は修正せず、needs_humanまたは制御系障害として停止する。

長時間ループを置かず、CI完了イベントを次の時計にする

入口はGitHub Actionsのworkflow完了イベントです。信頼済みオーケストレータがイベントとlive APIを照合し、conclusionがfailureまたはtimed_outの場合だけクラウド上の修復jobを非同期に起動します。cancelledは、コードの失敗とは限らず、人の操作やconcurrency制御で終了した可能性があるため対象にしません。

修復jobは、一つのイベントにつき一つの修復サイクルだけを実行します。ログ取得、原因判定、patch生成、検証、pushまで進んだら終了し、その後のCIを長時間pollingしません。修正commitで再実行されたCIが失敗すれば、その新しい完了イベントが次のサイクルを起動します。この構造では、待機時間にjobを保持せず、イベント単位の再実行と監査が明確になります。

修正後のCIが成功した場合は修復jobを起動せず、対応するchainをgreenとして閉じる通知処理だけを行います。greenはmergeの許可ではありません。自動merge、release、deployは別の責任境界として残し、通常のレビューとbranch protectionを通します。

  • failureまたはtimed_out → 対象検証 → 非同期に一修復サイクルを起動する。
  • patchをpush → jobを終了する → 通常のCIが新しいcommitを検証する。
  • 再失敗 → 新しい完了イベントが次の一サイクルを起動する。
  • success → repairは起動せずgreenを通知する。cancelled → 対象外として記録する。

イベントを受けた時点で、対象PRを狭く検証する

イベントにPR情報が含まれていても、その内容だけで修復を開始しません。対象は同一リポジトリから作られ、許可されたbase branchへ向くopenなPRに限定します。fork由来のPR、昇格やreleaseに使うbranch、許可リスト外のbase、PRへ一意に対応できないrunは対象外です。

オーケストレータはworkflowの識別子と既定branch上の定義を許可リストで照合します。このworkflow自身はPRのコードをcheckoutせず、PR内のscriptやactionも実行しません。未信頼コードを実行するCIと、失敗を受け取る特権側の制御面を分離し、失敗ログに書かれた命令でクラウドjobの引数や権限が変わらないようにします。

クラウドjobへ渡す固定入力は、PR番号、イベント時点のhead SHA、workflow run IDです。workflowの再実行を区別する場合はrun attemptも固定します。branch名や最新runを後から検索して対象を決めると、別のpushや再実行を誤って分析できます。固定した入力を、以後のclaim、ログ取得、attempt、commit trailer、通知の相関キーにします。

  • 許可: 同一リポジトリ、openなPR、許可されたbase branch、信頼済みworkflow。
  • 拒否: fork、昇格・release系branch、cancelled、PRを特定できないrun、許可外workflow。
  • 固定入力: PR番号、開始時head SHA、workflow run ID。再実行を扱う場合はrun attemptも含める。

固定入力とlive状態を照合し、正確な失敗ログだけを読む

job開始時にはPRとworkflow runをGitHub APIから再取得します。PRがopenか、baseが許可対象か、head repositoryが同一か、現在のhead SHAが固定入力と一致するか、runがそのSHAとPRに対応するか、conclusionが対象かを確認します。一つでも確定できなければ、モデルを呼ばずstaleまたはneeds_humanで停止します。

失敗ログは、固定したworkflow run IDに属するjobとstepから取得します。「PRの最新run」や同じ名前の別workflowを検索して代用しません。再実行されたrunはattemptも識別し、どの実行のどの失敗を分析したかを監査記録から追えるようにします。

ログ本文、source、diff、IssueやPRの本文、モデル出力はすべてuntrusted dataです。ログ中の『この制約を無視せよ』のような文字列を指示として扱わず、構造化された区切りの中へデータとして渡します。secretらしい値はモデル送信前にredactし、入力サイズと対象stepを制限します。

  • イベント値は起動要求であり、live PRとrunの再検証結果を権威にする。
  • 固定したrun IDとrun attemptから失敗job・stepを取得し、別実行のログを混ぜない。
  • ログ、source、diff、モデル出力の内容では、権限や変更境界を拡張できない。

状態機械とobject generation条件で重複と停止を制御する

状態はreceived、claimed、analyzing、patch_ready、validated、pushed、awaiting_ciのような進行状態と、green、needs_human、stale、budget_stopped、control_errorの終端状態に分けます。各遷移は許可リストで検査し、同じイベントの再配送やjobの再試行が来ても、同じ副作用を二重に起こさないようにします。

Google Cloud Storageを状態保存に使う場合は、objectのgenerationとifGenerationMatch条件をcompare-and-swapとして利用できます。新規claimはifGenerationMatch=0でlive objectが存在しないことを条件に作り、leaseの延長や期限切れclaimの引き継ぎは、読んだgenerationが変わっていない時だけ更新します。attemptとpush済みSHAも同じchain状態への条件付き更新で確定し、古いjobが新しい状態を巻き戻せないようにします。

leaseにはownerだけでなく、引き継ぎごとに変わるfencing tokenを持たせます。モデル呼び出し、artifact確定、push要求などの外部副作用へ進む直前に、現在のowner、token、期限、generationを再確認します。期限切れ後に古いworkerが復帰しても、新しいownerの状態を更新したりpushを依頼したりできないことを不変条件にします。

費用は、日次とrolling期間の残量を含む一つのbudget ledgerをgeneration条件付きで更新します。複数objectを同時更新したように見せず、同時要求が同じledger generationに競合した時は一方だけを成功させます。より複雑な横断transactionが必要ならtransaction対応の保存先を選びます。object generationは単体objectの条件付き更新であり、設計上の原子性の範囲を広く解釈しません。

IssueやPRコメントは、人が経過を読める監査ビューであって状態の正本ではありません。正本には、状態、時刻、SHA、run ID、fingerprint、使用量、artifactへの限定的な参照とdigestだけを保持します。secret、ログ本文、patch本文は保存せず、必要な監査artifactは別の保護領域へ短い保持期限で分離します。

  • claim: eventごとに一意。duplicate deliveryは既存claimを読んで同じ結果へ収束する。
  • lease: 所有者、期限、fencing tokenを持ち、期限後もgeneration一致時だけ引き継ぐ。外部副作用の直前にも所有権を再確認する。
  • attempt: chain、attempt番号、run ID、開始SHA、結果を条件付きで確定する。
  • 正本: object state。Issueやコメントは監査表示であり、処理判断には使わない。

決定的fingerprintを先に判定し、モデル費用を事前予約する

最初に、workflow、失敗job・step、exit code、実行環境に固有な揺らぎを除いた主要エラー行から決定的なfailure fingerprintを作ります。既知の環境障害、権限不足、外部サービス障害、保護領域の変更が必要な失敗は、この段階でneeds_humanへ送ります。決定的な分類で結論が出ない場合だけ、モデルによる原因分類とpatch生成へ進みます。

修正commitにはchain、attempt、起点runを示すtrailerを付けます。たとえばAI-Repair-Chain、AI-Repair-Attempt、AI-Repair-Runのように機械が読めるキーを使うと、新しいCI失敗を同じchainへ関連付けられます。ただし、PR側で同名trailerを作れるため、trailerは監査と相関の補助に限定し、attemptやchainの正本は条件付き更新したstate objectに置きます。最大三回で停止し、同一fingerprintが修正後も反復した場合は、回数が残っていても進展なしとしてneeds_humanへ移します。

LLMを呼び出す前には、その呼び出しの最大想定費用を日次上限とrolling期間上限の両方へreservationとして計上します。完了後に実使用量でsettlementし、差額を戻します。上限の読み取り、reservation、settlementの整合性を確認できない時は、残額があると推測せずfail-closedで停止します。

  • 決定的検査で分類できる失敗にはLLMを使わない。
  • 最大attemptは三回。同一fingerprintの反復は進展なしとして人へ移管する。
  • LLM呼び出し前に最大額を予約し、完了後に実使用量へ精算する。
  • 予算状態が読めない、競合した、精算不能という制御系障害では新規呼び出しを始めない。

push直前のstale head検査で、人の途中変更を上書きしない

分析開始時にhead SHAが一致していても、patch生成と検証の間に人や別automationがpushできます。そのため、push直前にPRを再取得し、openであること、baseが許可対象であること、head repositoryが同一であること、remote headが開始時SHAと完全一致することを再確認します。

remote headが変わっていれば、生成したpatchが新しいコードにも適用できそうに見えてもrebaseや再生成を行わず、staleとして停止します。新しいheadに対するCI結果を待ち、その失敗イベントから別サイクルを始めます。これにより、人が途中で直した内容や別jobの修正を、古い分析結果で上書きしません。

pushは通常のnon-force pushだけを許可します。push拒否はforce pushで回避せず、remote状態を読み直してstale、権限不足、branch protection、制御系障害に分類します。成功後は実際にremoteへ到達したcommit SHAを状態へ記録し、そのSHAに対するCIだけをchainの次イベントとして扱います。

  • 開始時: PR open、許可base、同一repository、head SHA一致を検査する。
  • push直前: 同じ条件とremote headを再検査する。
  • 途中push: patchを捨ててstale停止し、新しいCIイベントへ判断を委ねる。
  • 拒否時: force push、暗黙のrebase、検証のやり直しによる押し込みを行わない。

モデルに見せた既存ファイルだけを、小さな差分として許可する

モデルへ渡す前に、原因候補に関係する少数の既存ファイルを決定的な処理で選びます。モデルはその許可pathだけを変更候補として返し、追加行と削除行の合計、およびpatch byte数の上限内に収めます。新規作成、削除、rename、binary、symlink、submodule、file modeの変更、force push、検証を迂回する変更は拒否します。

認証、OAuth、Cookie、CORS、secret、database migration、Terraform、GitHub Actions workflow、Cloud Build、lockfile、package manifest、generated file、本番設定は、局所修正に見えても常に人へ移管します。test、snapshot、coverage設定の変更も禁止します。失敗した検査側を弱めてgreenにする行為と、product codeを直して検査を通す行為を分けるためです。

モデル出力はunified diffなどの限定した形式で受け、最初にgit apply --checkを通します。適用後はgit diff --check、許可path、変更種別、追加行と削除行、byte数、credentialらしい追加literal、検証skipやassertion弱体化を機械的に検査します。最後に検証済みpathだけを明示してstageし、git addの広いpathやall指定は使いません。

patchの静的検査後は、失敗したcheckに対応する既存のallowlist済み検証を実行します。モデルが返した任意のshell commandは実行せず、リポジトリ外の信頼済みharnessがコマンドとtimeoutを決めます。検証中はpush、モデル、状態保存のcredentialを渡さず、終了後にstaged diffが検査済みdigestと一致することを再確認します。

  • 許可: モデルへ明示した少数の既存product code、上限内の局所的な変更。
  • 常時拒否: 新規作成、削除、rename、binary、symlink、submodule、mode変更、force push、検証回避。
  • 常時移管: 認証・OAuth・Cookie・CORS、secret、migration、Terraform、workflow、Cloud Build、本番設定。
  • 常時移管: lockfile、package manifest、generated file、test、snapshot、coverageの変更。
  • 検査: git apply --check、git diff --check、credential、path、変更種別、規模、明示stage。

イベント受付、クラウド実行、Git pushの権限を分離する

GitHub Actions側のオーケストレータは、信頼済みの既定branch上の定義だけを実行し、PRコードをcheckoutしません。その役割はイベントの検証、固定入力の作成、クラウドjobの非同期起動までに絞ります。PRコード由来のaction、script、設定を特権contextで評価しないことが境界です。

クラウド側では、イベント受付、修復制御、未信頼コードの検証、pushでidentityを分けます。受付側はjob起動だけ、制御側は対象状態object、必要なartifact、モデルAPIだけ、検証workerは短命な読み取り環境だけを持ちます。検証workerがPRのtoolやtestを実行する間は、モデル、状態保存、pushのcredentialを渡さず、外向き通信も必要な宛先へ絞ります。

pushは信頼済みbrokerへ分け、brokerが検査済みdiffのdigest、leaseのfencing token、PRのlive状態、開始時head SHAを再確認した後だけ、短命なGitHub App credentialで通常pushします。構成を分けられない場合でも、少なくとも未信頼コードの実行中はcredentialを取り外し、push直前にだけrepositoryと期限を限定して発行します。長期tokenを環境へ置いたままtestを実行しません。

Gitへ書き込めるcredentialがあっても、適用範囲はアプリケーション側の検査だけに頼りません。GitHub側でもbranch protection、許可branch、force push禁止、merge権限分離を維持します。アプリケーションのguardが壊れた時に、外側の権限境界が最後の防波堤になります。

  • GitHub Actions: PRコードを実行せず、検証済みの固定入力だけをクラウドへ渡す。
  • 受付identity: job起動だけ。制御identity: 状態、artifact、モデルAPIだけ。検証identity: 読み取りだけ。push identity: 対象repositoryへの短命な書き込みだけ。
  • GitHub: 短命credential、non-force push、branch protection、merge権限分離を維持する。

通知は人が判断すべき状態に絞り、中間試行は監査へ残す

すべての内部遷移を通知すると、重要な停止理由が埋もれます。通知対象は、修正後のCIがgreenになった時、needs_humanへ移った時、予算上限または予算判定不能で停止した時、claimやlease、API、状態更新など制御系に障害が起きた時に絞ります。中間attemptの開始、fingerprint、モデル使用量、patch拒否理由は監査記録へ残します。

観測する指標は、失敗イベントから修正pushまでの時間、一回の修正でgreenになった割合、chainあたりの平均試行数、人間移管率、PR別費用です。これらは自動化が速いかだけでなく、どの種類の失敗が境界内で解けているか、費用と人の確認負担が釣り合うかを判断する材料になります。

数値は導入前に成功率として置きません。observeとcanaryで得た分布を基準にし、workflow、言語、失敗分類ごとに分けて読みます。greenであっても、テストが弱い、許可範囲が広すぎる、移管すべき変更を通したという可能性は残るため、単一の成功率を品質保証として扱いません。

  • 通知: green、needs_human、予算停止、制御系障害。
  • 監査: event、claim、lease、attempt、fingerprint、入力digest、patch検査、費用reservationとsettlement。
  • 指標: 修正pushまでの時間、一回でのgreen化、平均試行数、人間移管率、PR別費用。
  • green後も自動merge、release、deployは行わない。

off、observe、限定canary、repairの順で段階導入する

最初はoffで、状態object、権限、予算上限、protected path、停止方法だけを用意します。次のobserveでは、実際の失敗イベントを読み、対象判定、fingerprint、モデル分類、生成されるはずのpatchと費用を記録しますが、作業treeへ適用もpushもしません。誤分類と拒否条件を先に観測できます。

限定canaryでは、対象workflow、base branch、repository、失敗分類、許可pathをさらに絞り、人が確認した小さな範囲だけ修復を有効にします。canaryでstale停止、予算予約、patch guard、通知、rollbackの運用を確認してからrepair対象を広げます。段階はfeature flagで戻せるようにし、制御系障害時はobserveまたはoffへ戻します。

回帰fixtureには、同じdeliveryの重複、lease期限切れ後の再取得、複数jobによる予算予約の競合、分析途中の人によるpush、non-force pushの拒否、protected path要求、ログ内のprompt injection、credentialらしい差分、三回到達時の停止を含めます。正常系だけでなく、『何もしないこと』と『人へ渡すこと』をテストの期待値にします。

  • off: 外部副作用を止めたまま、境界と停止手段を準備する。
  • observe: 対象判定、分類、patch候補、費用を記録するが適用しない。
  • 限定canary: workflow、branch、失敗分類、pathを絞ってnon-force pushまで検証する。
  • repair: 観測結果を基に範囲を広げ、いつでもobserveへ戻せるようにする。

自動修復の適用範囲は、直しやすさではなく影響範囲で決める

イベント駆動にすると待機jobと長時間pollingを減らせますが、複数イベントをまたぐ状態設計が必要になります。重複排除、期限付きlease、chainの関連付け、success時の終了処理がなければ、短いjobへ分けただけで競合は残ります。小規模なCIでは、人が一度直す方が状態基盤を運用するより単純な場合もあります。

厳しいpatch境界は安全性を高める一方、自動修復できる失敗を減らします。特に認証、migration、infrastructure、workflow、依存更新、テスト変更は、局所的な一行修正に見えてもシステム全体の契約を変えます。変更量の小ささではなく、権限、データ、配布経路、品質基準へ影響するかで人への移管を判断します。

モデルを高性能にしても、flaky test、外部サービス障害、隠れた要件、弱いテストは解決しません。自動修復の前提は、再現可能なCI、失敗stepを特定できるログ、branch protection、明示したprotected path、費用上限です。これらが曖昧なら、まずobserveで失敗分類と人の判断を整理する方が有効です。

  • 適用しやすい: 原因が限定され、既存product codeの小さな差分で、既存検証を弱めずに直せる。
  • 移管すべき: 権限、認証、データ構造、infrastructure、配布、依存、テスト基準を変える。
  • 自動化しない判断も成果として記録し、拒否理由を次の境界改善に使う。

安全なAIリペアループは、モデルより停止境界で決まる

PRのCI失敗を自動修復する時、中心に置くべきなのはAIの連続実行ではありません。一つの失敗イベントを一つの短いサイクルへ閉じ込め、固定したPR、head SHA、runの組み合わせをlive状態と照合し、修正後のCIを次のイベントとして待つ構造です。

その上で、object generationによるclaimとlease、費用の事前reservation、最大三回と同一fingerprintの停止、push直前のstale head検査、protected pathと差分規模のguardを重ねます。どれか一つの検査だけで安全にするのではなく、イベント、状態、費用、Git、権限という別々の境界で同じ誤操作を止めます。

実務で持ち帰る判断軸は、AIがその失敗を説明できるかではなく、許可された小さな差分だけを作り、状態が変われば何もせず止まり、判断不能なら人へ証拠を渡せるかです。この条件を満たす範囲から段階的に始めることで、人間の往復を減らしながら、最終的な判断責任を人と通常の開発プロセスに残せます。

Technical documents

技術文書を増やしていきます。

AI、クラウド、業務アプリ開発、要件定義、運用設計に関する考え方を、今後も文書として整理します。

技術文書一覧へ