メインコンテンツまでスキップ

エージェントが書いたコードのための CI/CD

上級
What you'll learn
  • エージェントによる PR 量の増加で最初に壊れるのが、ランナー予算ではなくマージキューである理由を知る
  • バッチングの算数をやる:失敗率が数ポイント上がるだけで投機的マージがどう崩れるか
  • 4 つのレバーを正しく順位づけする。「ランナーを増やす」は最も弱いレバーだから
  • エージェントのテストが、エージェントの修正なしで本当に失敗することを証明するゲートを出荷する
  • 雰囲気ではなく証拠に基づいてフレークを隔離し、CI を静かに汚染するキャッシュの落とし穴を知る

コーディングエージェントは機能しています。それが問題です。

1 日に 5 件のプルリクエストをマージしていたチームが、いま 40 件マージしているとしても、テストスイートもランナー予算もブランチ保護ルールも変えていません。変わったのは、旧来の量に合わせて調整されていたキューイングシステムへの到着率です。下流のすべて — マージキュー、フレーキーテストのノイズ、ランナーの同時実行数、レビューの注意力 — は人間のスループットに合わせてサイジングされており、キューイングシステムは優雅に劣化してはくれません。持ちこたえ、持ちこたえ、持ちこたえ、そして 1 週間のうちに倒れます。

このページは検証のボトルネックのパイプライン側です。あちらのページは誰がコードをチェックするのかを問います。こちらは、あなたの CI がその量を物理的に吸収できるのか、できないときにどうするかを問います。

問題全体を枠づける 3 つの数字​

−7.2%。 DORA の 2024年版 Accelerate State of DevOps レポートは AI 採用の 25% 増加をモデル化し、デリバリー安定性の 7.2% 低下とスループットの 1.5% 低下を見積もりました。個人の生産性と仕事の満足度は上がりました。デリバリーレベルの成果は下がりました — レポートはこれを、変更セットの大型化とバッチングの規律の弱まりに結びつけています。2025年の続編はこう言い直しています:AI は増幅器であり、あなたのデリバリーシステムが既にそうであるものを拡大する、と。1 日 5 PR で脆かったパイプラインは、40 PR では使い物になりません。

19%。 METR のランダム化試験(2025年7月)は、経験豊富なオープンソースメンテナ 16 人と、彼ら自身のリポジトリからの実際の課題 246 件を対象に、AI ツールの使用可否をランダム化しました。AI ありのほうが、タスクは 19% 長くかかりました。CI 計画にとって重要なのは知覚のギャップです:開発者は 24% の高速化を予測し、遅くなった後でさえ 20% 速くなったと信じていました。あなたのチームの誰も、パイプラインが制約だとは言ってくれません。誰もそれを制約として経験しないからです — 彼らが経験するのは「待ち」であり、それは天気のように感じられます。

1.5% / 16% / 84%。 Google が公開したフレーク性のデータ:テスト実行のおよそ 1.5% がフレーキーな結果を報告し、420 万件のテストのうち約 16% が何らかの時点でフレーク性を示し、観測された pass→fail 遷移のおよそ 84% は本物のリグレッションではなくフレーキーテストによるものです。この 1.5% を覚えておいてください — 1 日 5 PR なら無視できるほど小さく、40 PR では効いてきます。

なぜマージキューが最初に壊れるのか​

たいていのチームは天井が計算資源だと思い込みます。違います。マージキューであり、その理由は、ほとんどの人がドキュメントを読んだことのない仕組みにあります。

GitHub のマージキューは投機的チェックを行います。PR #1、#2、#3 をキューに入れると、一時ブランチを作ります:main + #1、main + #1 + #2、main + #1 + #2 + #3 — そしてそれらすべてを並列にテストします。すべてのチェックが通れば、3 件が 1 ラウンドで着地します。バッチングが N 回の逐次検証を 1 回に変える、というのが価値提案のすべてです。

では失敗経路を読みましょう。PR #1 を含むグループのチェックが失敗すると、GitHub はキューから #1 を取り除きます — そして後ろのグループはすべて #1 の変更を含んでいたので、それらは無効になり、#1 なしで再構築されます。1 件の悪い PR が食うのはスロット 1 つではありません。バッチの残り全部の CI ラウンドを食います。

これによりキューのスループットは合格率の関数になり、しかも直感より急峻です。モデル化しましょう:バッチサイズ N、各 PR が独立に確率 p で失敗し、最初の失敗までの連続した PR をマージするとします。1 ラウンドあたりの期待マージ数は幾何分布の期待値です:

E[merges] = (1 − p) · (1 − (1 − p)^N) / p

N = 10 の場合:

PR ごとの失敗率 p1 ラウンドあたりの期待マージ数10 PR を捌くのに必要なラウンド数
2%8.9~1.1
5%7.6~1.3
10%5.9~1.7
20%3.6~2.8
30%2.3~4.4

たいていのチームが逆に捉えている帰結が 2 つ:

  1. フレーキーなスイートはスループット税であって、迷惑料ではありません。 フレーク性は p に直接上乗せされます。本物の失敗 5% から、本物 5% + フレーク 5% になると、1 ラウンドあたり 7.6 マージから 5.9 マージへ — 何もテストしていなかったテストのせいで、マージ能力のおよそ 23% が失われます。
  2. p が高いときにバッチサイズを上げるのは間違ったレバーです。 N を大きくして効くのは (1 − p)^N が 1 の近くに留まる間だけです。p = 0.2 では、バッチ 10 で 3.6 マージしか出ません。直すべきは p を下げることであって、楽観をもっとキューに積むことではありません。

そしてエージェントによる量の増加は p を悪いほうへ押します — DORA の安定性の知見は、まさに p の上昇についての言明です。

誰もやらない容量の算数​

ランナーを買う前に、これを紙ナプキンの上でやってください。前提は明示します。あなたのチームでこれが外れるとしたら、その前提が原因だからです:

Guided walkthrough1 of 5
  1. 45 分のスイートを 15 の並列ジョブにファンアウトしても、コストは依然として約 45 ジョブ分です。並列化が買うのはレイテンシであって、コストではありません。最も遅いジョブからではなく、直近の実行からスイート全体のジョブ分を取ってください。

要点は「Enterprise を買え」ではありません。制約はバースト時の同時実行数であり、パイプラインが問題なく見える日次平均の数字こそが、問題を隠している数字だ、ということです。

4 つのレバー、レバレッジ順​

チームはこれらを、ほぼ正確に有用な順序の逆から手に取ります。

1. p を下げる(最高レバレッジ、最低コスト)。 取り除いた失敗率の 1 ポイントごとに、バッチングの数式のなかで非線形に報われます。具体的には:PR を開く前にエージェントに高速スイートをローカルで実行させ、PR がキューに入る前にグリーンなプレマージチェックを必須にし、フレークを積極的に隔離する(後述)。これに計算コストはかかりません。

2. 検証 1 回あたりの作業量を削る — テスト選択。 定数ではなく指数を変える唯一のレバーがこれです。すべてを実行する代わりに、その変更が壊し得るものだけを実行します:

  • Develocity Predictive Test Selection は、あなた自身の Build Scan 履歴とプロジェクト横断のパターンからモデルを構築し、有効化する前に節約幅と見逃し失敗率を測れるシミュレータを同梱しています。買わないとしても盗む価値のある安全面の詳細が 2 つ:クラスパターンやアノテーションで必ず実行するテストをピン留めできること、そして最近フレーキーなテストは常に選択されること。フレーキーなテストは定義上、変更からは予測不能だからです。何かが永遠にスキップされないよう、マージ後または夜間の「残りのテスト」実行と組み合わせてください。
  • Bazel / Buck2 はこれを構造的に備えています:ビルドグラフは、変更されたファイルにどのターゲットが依存しているかを知っているので、影響を受けるターゲットに対する bazel test は、タダで手に入るターゲット判定です。
  • ビルドグラフがない? Python には pytest-testmon があり、どのテストがどのコードをカバーしているかを記録して、影響を受けるものだけを再実行します。Jest は --changedSince を標準で備えています。

テスト選択、3 つのエコシステム

# Bazel — test only what the diff can reach
bazel test $(bazel query "rdeps(//..., set($(git diff --name-only origin/main)))" --output=label 2>/dev/null)

# Python — coverage-based impact analysis
pip install pytest-testmon
pytest --testmon          # first run builds the DB, later runs select

# Jest — built in, no plugin
npx jest --changedSince=origin/main

3. 積極的にキャッシュする — そして落とし穴を知る。 リモートキャッシュは、入力・コマンドライン・ホワイトリスト登録された環境のハッシュをキーにしてアクション結果を保存します。Bazel 自身のドキュメントがその罠を指摘しています:ワークスペース外のツールは追跡されないため、コンパイラのバージョンが異なる 2 台のマシンが、本来なら異なる出力を生むはずの作業でキャッシュヒットを得ることがあります。重要なものは --action_env でホワイトリストに入れ、書き込み権限は CI だけに限ってください — 開発者のマシンは読み取り専用にすべきです:

リモートキャッシュ、書き込みは CI のみ

# CI: read and write
bazel test //... --remote_cache=grpcs://cache.internal:443

# Developer machines: read only, never poison the shared cache
bazel test //... --remote_cache=grpcs://cache.internal:443 \
--remote_upload_local_results=false

4. ランナーを増やす(最後)。 線形コストで線形の効果、しかも p については何もしません。最初の 3 つの後、バースト時の同時実行数が実際に分かってから行ってください。

AI 特有の失敗モード:エージェントが自分の答案を自分で採点する​

ここまでの内容は、量の多いパイプラインならどれにも当てはまります。この項目はエージェントが書いたコードに特有で、たいていの CI 構成に空いている穴です。

同じエージェントが修正とその修正のためのテストを書くとき、「テストが通った」が運ぶ情報はあなたが思うよりずっと少なくなります。2 つの失敗の形が絶えず現れます:

  • テストが要件ではなく実装の振る舞いをアサートしている — テスト対象のコードを読んで書かれたから通るのです。
  • 既存のテストが失敗したとき、エージェントがコードを直す代わりにテストを新しい振る舞いに合わせて編集する。差分はグリーンで、リグレッションは出荷されました。この振る舞いには名前と文献があります — モデルがなぜそうするのか、どの防御策に測定された効果量があるのかはコーディングエージェントにおける報酬ハッキングを参照してください。以下に続くのはパイプライン側の対策であって、モデル側のものではありません。

安上がりな構造的ゲート:新しいテストは変更前のコードに対して失敗しなければならない。 これは TDD のレッド・グリーンの規律を機械的チェックに変えたものであり、ミューテーションテストが大規模に形式化しているものです。価値の大半を得るのにミューテーションテストのフレームワークは要りません — 必要なのは CI ジョブ 1 つです:

CI ゲート:新しいテストは変更なしで失敗しなければならない

#!/usr/bin/env bash
# must-fail-first.sh — run in CI on PRs that add tests
set -euo pipefail

NEW_TESTS=$(git diff --name-only origin/main...HEAD -- 'tests/**' 'test/**')
[ -z "$NEW_TESTS" ] && { echo "no new tests, skipping"; exit 0; }

# Keep the new tests, revert everything else to the base commit
git stash push --keep-index -- $NEW_TESTS
git checkout origin/main -- src/
git stash pop

# The new tests MUST fail now. If they pass, they prove nothing.
if pytest $NEW_TESTS -q; then
echo "FAIL: new tests pass against unmodified source — they do not test the change."
exit 1
fi
echo "OK: new tests fail without the fix."

誠実な注意点が 2 つ。これはソースとテストが別のツリーにあるときにしか機能しませんし、間違った理由で失敗するテスト(インポートエラー、フィクスチャ欠落)はこのゲートを通過してしまいます。これは証明ではなく床です — それでも、エージェントが書いた PR がグリーンなのに無価値になる最も一般的な形を捕まえます。

補完となるレビュー側の対策は、作者のコンテキストを共有しないレビュアーです:ハイブリッド AI コードレビュアーと PR レビュー用の GitHub Action を参照してください。

エージェントと接触しても生き残るフレーク隔離​

pass→fail 遷移の 84% がフレークである以上、「再実行しろ」は人間のデフォルトの反応であり、しかも利用可能な選択肢のなかで最も高くつきます — 何も教えてくれるはずのなかったテストのために、検証 1 回分を丸ごと燃やすのですから。

Guided walkthrough1 of 5
  1. フレーキーテストとは、同じコミットで異なる結果を出すテストのことです。変更のない main でスイートを N 回実行し、テストごとの失敗回数を記録してください。それがあなたの証拠であり、開発者の記憶は証拠ではありません。

1 日ずつ食われる落とし穴​

  • merge_group は自動ではありません。 GitHub のドキュメントは明示しています:ワークフローのトリガーに merge_group イベントを必ず追加しなければならず、さもないとキュー内でチェックが発火せず、必須チェックの欠落でマージが失敗します。これはマージキューの設定ミスとして最も一般的なものです。
  • 24 時間のキューキャンセルは無言です。 キューが飽和すると、ジョブは失敗するのではなく消えます。赤いビルドだけでなく、キューの深さにアラートを設定してください。
  • マトリクスジョブはワークフロー実行あたり 256 が上限です。 生成されたマトリクス(エージェントのスキャフォールディングでよくあるパターン)は、役に立つエラーも出さずにこれに当たります。
  • マシンをまたいだキャッシュヒットは誤りであり得ます。 追跡されない外部ツールチェーンが偽ヒットを生みます。ツールチェーンをピン留めし、環境を --action_env でホワイトリスト化してください。
  • ワイルドカードパターンを使ったブランチ保護はマージキューと非互換です。 保護ルールがワイルドカードを使っていると、キューは作動しません。
  • CI に触れるエージェントの PR は、スタイルの問題ではなくセキュリティ境界です。 ワークフローファイルを編集できる PR は CI のシークレットを流出させ得ます — GitHub の課題から CI シークレットへと自律実行のハードニングを参照してください。

誠実な枠づけ​

無制限のエージェント出力を吸収できるパイプラインにする設定など存在しません。上記のレバーが買ってくれるのは、コストがプルリクエストの数ではなく変更の大きさとともに増えるシステムです — テスト選択とキャッシュは劣線形、ランナーは線形、そしてマージキューのバッチングは合格率が高い間だけ劣線形です。

これは本当の制約を、DORA の 2025年の枠づけが置いた場所に戻します:AI は、あなたが既に持っているデリバリーシステムを増幅します。小さなバッチ、速いスイート、低いフレーク率が既にあなたの実践だったなら、エージェントはあなたをはるかに速くします。そうでなかったなら、エージェントがそれを暴きます。

Check yourself

0/6
  1. マージキューのバッチサイズが 10 で、PR ごとの失敗率が 5% から 20% に上がりました。1 ラウンドあたりの期待マージ数はおおよそどうなりますか?
  2. PR #1 が原因でマージグループが失敗しました。その後ろにキューイングされていたグループに対して GitHub は何をしますか?
  3. 定数をずらすだけでなく、CI のコストのスケール方法そのものを変えるレバーはどれですか?
  4. Develocity の Predictive Test Selection が、差分が触れていなくても最近フレーキーなテストを常に実行するのはなぜですか?
  5. エージェントの PR が修正とテストを追加し、CI はグリーンです。「must fail first」ゲートは何をチェックしますか?
  6. 2 台のマシンが同じアクションで Bazel のリモートキャッシュヒットを得ましたが、本来なら異なる出力を生むはずです。最も可能性の高い原因は何ですか?
Enter キーまたはスペースキーでカードを裏返します。左右の矢印キーでカードを移動できます。用語を表示しました。
1 / 12
Key takeaways
  • ランナー予算より先にマージキューが壊れます。投機的バッチングは合格率への賭けであり、1 件の失敗 PR が後ろのグループすべてを無効にします。
  • E[merges] = (1 − p)(1 − (1 − p)^N)/p を自分の実数値で一度計算してください。「CI が遅い気がする」が、どのレバーを引くかという判断に変わります。
  • フレーク性は衛生ではなくスループットの問題です。Google が公開した比率では、ノイズがシグナルより大きい — pass→fail 遷移の 84% はフレークです。
  • 日次平均ではなく、常にバースト同時実行数でサイジングしてください。日次の数字こそ問題を隠す数字であり、飽和したキューは 24 時間で静かにジョブをキャンセルします。
  • テスト選択は唯一の劣線形レバーです:Bazel/Buck2 のターゲット判定、シミュレータ付きの Develocity PTS、pytest-testmon、あるいは jest --changedSince。
  • エージェントが修正とテストの両方を書くとき、グリーンの意味はあなたが思うより小さくなります。変更前のソースに対してテストが失敗することをゲートにしてください。
  • DORA の枠づけは有効です:AI はあなたが既に持っているデリバリーシステムを増幅します。小さなバッチ、速いスイート、低いフレーク率は昔からの助言でした — エージェントはそれを無視するコストを高くしただけです。

出典と参考資料​

次に読む​