GitHub ActionsのCIコストを削減する方法|並列PRで増える再実行を減らす
AI駆動開発が進むと、1人の開発者でも複数のタスクを並行して進めやすくなります。
たとえば、Git worktreeを使って、
- 機能A
- 機能B
- バグ修正C
- リファクタリングD
を同時に進め、それぞれPull Requestを作る。
以前なら数人でやっていたような並列開発を、AIエージェントを使いながら1人で進めることも珍しくなくなってきました。
一方で、実際にやってみると別の問題が出てきます。
CIが大量に走るのです。
今回は、並列開発によって増えやすいCIの実行回数を、品質を大きく落とさずに削減する方法について考えてみます。
並列PRでは、なぜCIが何度も走るのか
たとえば4つのPull Requestを同時に作ったとします。
PR-A
PR-B
PR-C
PR-D
GitHub ActionsでPull RequestをトリガーにCIを設定していれば、それぞれについてCIが実行されます。
PR-A → CI
PR-B → CI
PR-C → CI
PR-D → CI
ここまでは問題ありません。
ところがPR-Aをmainへマージすると、mainの状態が変わります。
すると、PR-B、PR-C、PR-Dは「現在のmainより古い状態」をベースにテストしたことになります。
GitHubのBranch Protectionで、
Require branches to be up to date before merging
を有効にしている場合、マージ前に最新のmainを取り込む必要があります。
GitHubのドキュメントでも、このStrictな設定では、他のPRによってベースブランチが更新された場合にheadブランチを更新する必要があるため、ビルド回数が増える可能性があると説明されています。
つまり、
最初
PR-A → CI
PR-B → CI
PR-C → CI
PR-D → CI
のあと、
PR-A Merge
すると、
PR-B → Update branch → CI
PR-C → Update branch → CI
PR-D → Update branch → CI
となります。
さらにPR-Bをマージすると、
PR-C → Update branch → CI
PR-D → Update branch → CI
PR-Cをマージすると、
PR-D → Update branch → CI
となります。
単純化すると、
4 + 3 + 2 + 1 = 10回
CIが必要になります。
4本しかPRがないのに、フルCIが10回走る可能性があるわけです。
AI駆動開発ではこの問題が大きくなる
従来は、
1人
↓
1機能開発
↓
PR
↓
CI
という流れが中心でした。
しかしAIエージェントを使うと、
人間
├─ Agent A → PR-A
├─ Agent B → PR-B
├─ Agent C → PR-C
└─ Agent D → PR-D
という開発が現実的になります。
開発速度は上がります。
しかしCIの設計を以前のままにしていると、
開発速度が上がるほどGitHub Actionsも大量に動く
という状態になります。
特に重いのが、
- Docker build
- Integration Test
- E2E Test
- Playwright
- DB Migration Test
- 複数ブラウザテスト
などです。
1回10分、20分かかるCIを何度も実行すれば、Runnerの利用時間も増えます。
セルフホストRunnerの場合でも、今度はCPU、メモリ、Dockerリソースなどを大量に消費します。
つまり、AIで開発コストを下げたつもりが、CI側の計算コストが増えていくわけです。
解決策:「全部を常にMerge可能にする」のをやめる
ここで考え方を変えます。
4本のPRが存在していたとしても、実際に次にマージするのは1本です。
それなら、
すべてのPRを常に最新mainへ追従させる必要はない
のではないでしょうか。
たとえば、
PR-A
PR-B
PR-C
PR-D
があって、次にPR-Aをマージするとします。
この場合、
PR-Aだけ
↓
Update branch
↓
Full CI
↓
Merge
します。
PR-B、PR-C、PR-Dは放置します。
そして次にPR-Bをマージするときになって初めて、
PR-B
↓
Update branch
↓
Full CI
↓
Merge
します。
この方式なら、重いCIは基本的に、
A → 1回
B → 1回
C → 1回
D → 1回
の4回に近づけられます。
ポイントは、
「PRが存在する」ことと「今すぐマージできる状態である」ことを分ける
ことです。
CIを2段階に分ける
ただし、PRを作った時点で何もテストしないのも危険です。
そこでCIを、
Quick CI
と
Full CI
の2段階に分けます。
Quick CI
Pull Requestを作成したタイミングでは、軽量なチェックだけを自動実行します。
たとえば、
Lint
Type Check
Unit Test
です。
GitHub Actionsなら、
on:
pull_request:
でこれらを実行します。
コードとして最低限成立しているか、型が壊れていないか、基本的なテストが通るか。
ここだけはPRごとに早い段階で確認します。
Full CI
一方で、
Build
Integration Test
E2E
Playwright
Docker Build
Migration Test
などの重い処理は自動実行しません。
マージ直前だけ実行します。
GitHub Actionsでは、
on:
workflow_dispatch:
を設定すると、Actions画面から「Run workflow」を押して手動実行できます。対象ブランチを選択して実行することもできます。
イメージとしては、
PR作成
↓
Quick CI
↓
レビュー
↓
待機
↓
次にマージするPRとして選択
↓
Update branch
↓
Full CI
↓
Merge
です。
mainが更新されても他のPRは放置する
この運用で特に重要なのがここです。
PR-Aをマージしたあと、
PR-B
PR-C
PR-D
にUpdate branchボタンが表示されても、すぐには押しません。
次にマージするPRがPR-Bなら、
PR-Bだけ更新します。
PR-B
↓
Update branch
↓
Quick CI
↓
Full CI
↓
Merge
そしてPR-C、PR-Dはそのままです。
これだけでもCI実行回数は大きく減らせます。
「自動CIをやめる」のではない
この話をすると、
「CIを手動にすると品質が下がるのでは?」
と思うかもしれません。
重要なのは、
CIをなくすことではありません。
CIの役割を分けることです。
PRを作った
↓
Quick CI
マージする
↓
Full CI
とします。
PR作成直後の目的は、
明らかに壊れたコードを早期発見すること。
マージ直前の目的は、
最新mainと組み合わせても本当に動くことを保証すること。
目的が違います。
だから同じCIを毎回実行する必要もありません。
Branch Protectionとの組み合わせ
GitHubのRequired Status Checksも考える必要があります。
Protected Branchでは、指定したstatus checkが成功するまでPRをマージできないようにできます。
たとえば、
Quick CI
をRequired Checkにしておけば、
LintやUnit Testが落ちているPRはマージできません。
一方、
workflow_dispatch
だけで起動するFull CIをRequired Checkにすると運用が少し複雑になります。
そこで最初は、
Quick CI
→ GitHub上のRequired
Full CI
→ チーム運用上のRequired
くらいから始めるのがシンプルです。
慣れてきたら、
ready-to-merge ラベル
↓
Full CI自動起動
↓
成功
↓
Merge可能
のように発展させてもよいでしょう。
concurrencyも設定しておく
もう一つ地味に効くのがGitHub Actionsのconcurrencyです。
たとえば開発中に、
push
↓
CI開始
すぐ修正
↓
push
↓
新しいCI開始
となった場合、古いCIはもう必要ありません。
そこで、
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
のように設定すると、同じ対象に対する古い実行をキャンセルできます。
並列開発では、こうした「もう意味のないCIを止める」設定も積み重ねると効いてきます。
CIは「何回走らせるか」も設計対象になる
AI駆動開発では、
「AIにどうコードを書かせるか」
に目が向きがちです。
しかし並列数が増えてくると、
何本同時に開発するか
何本同時にテストするか
いつFull CIを実行するか
どのPRをMerge Readyにするか
まで設計する必要があります。
CIは単なる品質チェックではなく、
計算資源の配分
でもあります。
開発エージェントを4つ動かせるからといって、CIも4本、8本、10本と無制限に実行すればよいわけではありません。
おすすめの運用
小〜中規模の並列開発であれば、まずは次の形が扱いやすいと思います。
開発
↓
PR作成
↓
Quick CI
・Lint
・Type Check
・Unit Test
↓
レビュー
↓
待機
↓
Merge対象に選択
↓
最新mainを取り込む
↓
Full CI
・Build
・Integration
・E2E
↓
Merge
そして他のPRは、
自分の順番が来るまでUpdate branchしない。
これだけでもかなり無駄なCIを減らせます。
まとめ
並列開発では、開発速度だけを上げるとCI実行回数が急増します。
特に、
複数PR
+
Require branches to be up to date before merging
+
重いE2E
という組み合わせでは、mainが更新されるたびに再検証が発生しやすくなります。
GitHub自身も、Strictなstatus check設定では、ベースブランチの更新に伴ってより多くのビルドが必要になる可能性を説明しています。
そこで、
PR作成時のQuick CI
と
マージ直前のFull CI
を分離します。
そして、
Merge対象になったPRだけ最新mainへ更新する。
この考え方に変えると、品質チェックを維持しながらCIコストを抑えやすくなります。
AIによって開発の並列度が上がるほど、
「どれだけ速くコードを書くか」
だけではなく、
「どのタイミングで、どこまで検証するか」
の設計が重要になります。
並列開発時代のCI/CDでは、テスト内容だけではなく、CIを走らせる回数そのものを最適化することも、開発プロセス設計の一部になっていくのではないでしょうか。