Blogへ戻る

Language: 日本語

その CI、無駄に直列で待っていませんか? 「needs を 1 つ外す」から始める GitHub Actions 高速化

Pull Request を作成してから、GitHub Actions のすべての必須チェックが完了するまで **何分待たされていますか?**

Pull Request を作成してから、GitHub Actions のすべての必須チェックが完了するまで 何分待たされていますか?

「14 分 12 秒……」。

タイポを 1 行直しただけでも、CI のグリーンランプが灯るまでに 15 分近く待たされる。これでは集中力や開発のリズムが完全に途切れてしまい、待ち時間に別作業を始めてはコンテキストスイッチのコストを払い続けることになります。

アノテーション基盤の CI パイプラインを徹底的に見直した結果、PR の所要時間を 14 分 12 秒から 3 分 54 秒(10 分 18 秒削減、約 73% 短縮!)へ高速化 することに成功しました。

しかも、最も大きな効果(約 38% 削減)を出したのは、テストコードのチューニングではなく 「GitHub Actions の needs(直列依存)をたった 1 行外してジョブを並列に走らせたこと」 でした。

この記事では、個別のコマンドに手を加える前に知っておくべき Job Graph(DAG)の依存関係改革と、PR 向けテスト選択・Coverage Matrix 化の実践手法 を解説します。

得られること

  • 不要な needs 直列依存を解除してクリティカルパスを即座に 38% 短縮する DAG 設計がわかる
  • PR 時(変更ファイル関連テストのみ並行実行)とマージ時(4-way matrix + 集約)のスマートな責務分離が学べる
  • 14 分 12 秒から 3 分 54 秒へ、10 分 18 秒(約 73%)削減した実践的チューニング手順が身につく!

改善前のボトルネック: 不要な直列化の連鎖

当時のパイプラインで特にひどかったのが、データベース(DB / Prisma)まわりの CI ジョブ でした。

【Before: 綺麗に直列(ウォーターフォール)に並んだ DB CI】
Prisma static checks (1分37秒)
  ↓ needs
migration check (2分43秒)
  ↓ needs
endpoint E2E (2分55秒)

一見すると「マイグレーションが成功してから E2E を走らせる」という自然な順序に見えます。

しかし、依存関係を詳しく調べてみると、endpoint E2Eわずか 19 秒で終わる E2E plan だけでなく、無関係な migration check まで needs して待っていた のです。

E2E plan は 19 秒でとっくに終わっているのに、2 分 43 秒かかる migration check の完了を指をくわえて待ち続け、その後にようやく 2 分 55 秒の endpoint E2E が開始される……。

個々のジョブが重いのではなく、「ジョブ間の無駄な依存関係」が最悪のクリティカルパスを作り出していました

施策 1: needs を 1 つ外して 38% 短縮

そこで、endpoint E2E から migration check への needs を削除しました。

【After: 依存を分離して完全並列化】
[ 列 A ]  E2E plan (21秒) ──────→ endpoint E2E (2分33秒) ⚡️
[ 列 B ]  Prisma static (1分49秒) ─→ migration check (2分58秒) ⚡️
# GitHub Actions workflow の修正イメージ
jobs:
  endpoint-e2e:
    # ❌ Before: needs: [e2e-plan, migration-check]
    # ⭕️ After: migration-check を待たず、plan 完了直後に即座に開始!
    needs: [e2e-plan]
    steps:
      - run: pnpm test:e2e:endpoint

加えた変更は、設定ファイルの needs から 1 行消しただけです。

これだけで、繰り返し計測において 165〜168 秒(約 38%)の短縮 を叩き出しました。 個別のテストを 1 秒削る努力をする前に、ジョブグラフの直列性を解きほぐすことがいかに絶大なインパクトを持つかがよく分かります。

施策 2: PR では変更ファイルに関連するテストだけを実行

PR の目的は 「開発者に最速でフィードバックを返すこと」 です。PR ごとに毎回全テストスイート(Full Coverage)を律儀に回す必要はありません。

そこで、PR では変更されたソースファイルに関連するテストだけを抽出して並列実行する仕組みを導入しました。

PR 作成 / push

  ├─ 変更ファイルが test 自身 ──────→ そのテストのみ実行 ⚡️
  ├─ 変更ファイルが特定モジュール ────→ 関連する単体テストのみ実行 ⚡️
  └─ 共有パッケージ・設定ファイルの変更 ──→ 影響範囲が広いため Full suite に倒す

フェイルセーフの設計

「テストを間引きすぎてバグを見落としたら困る」という懸念に対しては、以下の 2 つの安全策を講じました。

  1. 迷ったら多めに走らせる: 影響範囲が限定できない共通設定や Prisma スキーマの変更は、安全側に倒して Full suite を起動。
  2. マージ先(develop / main)で網羅性を保証: マージ先への push 時には必ず Full Coverage を実行。PR は速さ優先、統合ブランチは網羅性優先という明確な役割分担を敷きました。

この施策により、代表的な PR run では 14 分 12 秒が 2 分 11 秒へ劇的に短縮 されました。

施策 3: Coverage ジョブの 4-way Matrix 化と集約

main や develop で実行する Full Coverage についても、1 つのジョブで愚直に回すのをやめ、4 つの並列ジョブ(4-way matrix)に分割 しました。

[ backend-cov (matrix 1/4) ] ──┐
[ backend-cov (matrix 2/4) ] ──┼─→ [ cov-check (集約: 44秒) ] ⚡️
[ backend-cov (matrix 3/4) ] ──┤     (4つの artifact をマージして閾値判定)
[ backend-cov (matrix 4/4) ] ──┘

各テストジョブは 2 分台で並列に完了し、後段の cov-check が 44 秒で 4 つのカバレッジ結果をマージして閾値判定を行います。

品質基準やカバレッジ目標を一切緩めることなく、待ち時間を大幅に削減できました。

施策 4: 細かい「待ち」の徹底排除

大きな構造改革に加えて、日常の小さな待ち時間を削るための改善も積み重ねました。

施策内容と効果
concurrency: cancel-in-progress同一 PR に連続 push された際、古い不要な run を自動キャンセル(runner 枯渇を防止)
paths-filter の導入ドキュメント(docs/, *.md)の変更時は、重いビルドやセキュリティスキャンをスキップ
Node.js 環境のテスト分離DOM API を使わないユーティリティテストを jsdom から純粋な Node 環境へ分離して高速化
軽量ジョブの並列化markdown-link-check やフォーマッタなどの軽量チェックを先行して並行稼働

結果: 14 分 12 秒から 3 分 54 秒へ

これらすべての施策を統合した結果、パイプライン全体の所要時間は以下のように劇的な進化を遂げました。

比較項目Before(改善前)After(改善後)改善効果
PR 必須チェック完了時間14 分 12 秒3 分 54 秒10 分 18 秒短縮(72.5% 減 / 3.6倍高速化)⚡️
DB CI クリティカルパス7 分 15 秒(直列)2 分 58 秒(並列)165〜168 秒短縮(約 38% 減)
PR フィードバック構造全テストを直列実行関連テストのみ並行実行待ち時間による開発中断の根絶
マージ先 Coverage 実行単一ジョブで長時間実行4-way matrix + 集約網羅性を維持したまま高速化

まず「Job Graph の直列性」を疑え

CI の高速化というと、「テストコードをリファクタして実行時間を 100ms 削る」「より強いスペックの runner に課金する」といった方向に目が行きがちです。

しかし、個別のコマンドをどれだけ速くしても、その効果はそのジョブの枠内に留まります

一方、Job Graph(DAG)の不要な直列性を解消して並列化すると、その後ろに並んでいたすべてのジョブが一気に前へ動き出します

「CI が遅いな」と感じたら、まずはコードをいじる前に 「このジョブは本当に前のジョブを待つ必要があるのか?」 と Job Graph の依存関係(needs)を疑ってみてください。たった 1 行の修正が、チーム全体の開発ベロシティを激変させるはずです。

参考リンク