needsは「待ち合わせ」であり、待つほどCIは遅くなる

GitHub Actionsの needs は、ジョブ間に依存(=待ち合わせ)を作るキーワードです。便利な反面、needs を書くたびにCIの実行時間は伸びる可能性がある——これが本記事の出発点です。needs は「安全側」に倒して書かれがちで、本当は並列でよいジョブまで直列にしてしまうケースが頻発します。

この記事は、needs を中心にしたジョブ依存関係の設計パターンとアンチパターンを一覧表で整理したリファレンスです。既存のYAMLがどのパターンに当てはまるか照らし合わせながら読んでみてください。

パターン早見表

パターン効果使いどころ
ダイヤモンド型分岐並列実行でCI時間短縮lint/test/typecheckが互いに依存しない場合
concurrency グループ古い実行の自動キャンセル同じPRへの連続push
fail-fast: false1つの失敗で他を巻き込まないmatrix実行で全組み合わせの結果を見たい
再利用可能ワークフロー(workflow_call重複定義の排除複数リポジトリ・複数ワークフローで同じジョブ列
if: always() の通知ジョブ失敗時にも確実に通知Slack通知・ステータスレポート
部分的matrix待ち一部の組み合わせだけ早期デプロイカナリア的な先行実行

以下、それぞれを実例で見ていきます。

パターン1:ダイヤモンド型分岐で直列を崩す

最もよく見るアンチパターンは、本来independentなジョブを一直線に並べてしまうことです。

# アンチパターン: lint → unit-test → typecheck → build が全部直列
jobs:
  lint:
    runs-on: ubuntu-latest
  unit-test:
    needs: lint
  typecheck:
    needs: unit-test
  build:
    needs: typecheck

unit-testtypecheck は、どちらも「コードがそこにある」以外の前提を共有していません。lint の完了だけを待てば十分なら、両方を lint の直下にぶら下げてダイヤモンド型にできます。

# 改善: lint の後、unit-test と typecheck は並列
jobs:
  lint:
    runs-on: ubuntu-latest
  unit-test:
    needs: lint
  typecheck:
    needs: lint
  build:
    needs: [unit-test, typecheck]   # 両方の完了を待つ

buildneeds を配列にすることで「両方完了したら実行」を表現できます。ジョブ数が同じでも、依存グラフの横幅(並列度)がそのままCI時間の短縮につながります。GitHub Actions Visualizerでワークフローを貼り付けると、この「無駄な一直線」がグラフの見た目からすぐ分かります。

パターン2:concurrencyで「前の実行」をキャンセルする

needs の設計を最適化しても、同じブランチへの連続pushで古い実行がキューに残ると待ち時間は増えたままです。concurrency はこの無駄を消します。

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

groupgithub.ref(ブランチ名やPR番号)を含めることで、同じブランチの新しいpushが来たら古い実行を自動キャンセルします。PRに3回連続でpushしたら、意味のある最後の1回だけが最後まで走る——というのが理想の挙動です。

:::message
group にジョブ名やワークフロー名を含めないと、意図せず別ワークフロー同士がキャンセルし合うことがあります。${{ github.workflow }}-${{ github.ref }} のように、ワークフロー単位で区別するのが安全です。
:::

パターン3:fail-fastの使い分け

strategy.matrix を使うと、1つのジョブ定義から複数の実行(Node 18/20/22 × OS 3種、など)が展開されます。ここでの fail-fast の設定が、CIの挙動を大きく左右します。

strategy:
  fail-fast: false   # 既定値は true
  matrix:
    node: [18, 20, 22]
    os: [ubuntu-latest, windows-latest, macos-latest]
設定挙動向いている場面
fail-fast: true(既定)いずれか1組み合わせが失敗したら、残りを即キャンセル高速フィードバック優先。失敗が分かればそれ以上の情報は不要
fail-fast: false1組み合わせが失敗しても、残り全部を最後まで実行「Node 20だけ失敗?OSは関係ある?」を1回のCIで把握したい

新機能のリリース前など全組み合わせの状況を一度に知りたいときfalse にする価値があります。逆に「日常のPRチェックはとにかく速く」が目的なら既定の true のままにして、早い失敗検出を優先します。

パターン4:matrixジョブをneedsで待つときの罠

buildtest(matrix展開済み)に依存する場合、needs: test と書くだけでmatrixの全組み合わせの完了を待ってくれます。ここまでは直感通りですが、注意点が2つあります。

  • 1組み合わせでも失敗すると needs 側もスキップされる(fail-fast: true 併用時は特に、途中結果だけで後続が止まる)
  • 一部の組み合わせだけ先に進めたい(例: Linuxだけ先にデプロイし、Windows/macOSの結果は後追いで確認する)場合、needs では表現できず、ジョブ自体を分割する必要がある
# Linux先行デプロイのためにジョブを分ける例
jobs:
  test-linux:
    runs-on: ubuntu-latest
  test-other-os:
    strategy:
      matrix:
        os: [windows-latest, macos-latest]
    runs-on: ${{ matrix.os }}
  deploy:
    needs: test-linux   # Windows/macOSの結果を待たずにデプロイ

「全部待つか、一部だけ待つか」は needs の書き方ではなくジョブの切り方で決まる、というのがこのパターンの要点です。

パターン5:再利用可能ワークフローで依存グラフごと共有する

複数のワークフロー(あるいは複数リポジトリ)で同じジョブ列を使い回すなら、workflow_call で呼び出し先を1箇所にまとめられます。

# .github/workflows/ci.yml(呼び出し側)
jobs:
  call-shared-ci:
    uses: ./.github/workflows/shared-test-suite.yml
    with:
      node-version: '20'

呼び出される側(shared-test-suite.yml)の中の needs 構造ごと再利用されるため、「lintとtestの依存関係」をリポジトリ間でコピペしなくて済みます。依存グラフの設計をパターン化できたら、次はこの仕組みで設計そのものを部品化するのが仕上げの一手です。

通知ジョブはneedsの外側で「必ず動く」設計にする

失敗検知の通知ジョブは、needs の依存関係の性質上、依存先が失敗すると自動的にスキップされます。通知だけは例外的に動かしたいので、if: always() を明示します。

notify:
  needs: [lint, test, build]
  if: always()   # 依存ジョブの成否に関わらず必ず実行
  runs-on: ubuntu-latest
  steps:
    - run: echo "Result: ${{ needs.build.result }}"

needs.<job>.result で個々のジョブの結果(success / failure / cancelled / skipped)を参照できるので、「何が失敗したか」を含めた通知文を組み立てられます。

まとめ:依存グラフの見直し手順

  1. ワークフローYAMLを GitHub Actions Visualizer に貼り付けて依存グラフを可視化する
  2. 一直線になっている箇所を探し、本当に順序が必要か確認する(パターン1)
  3. 連続pushでの重複実行が気になるなら concurrency を追加する(パターン2)
  4. matrixを使っているジョブは fail-fast の値が目的と合っているか確認する(パターン3)
  5. 複数ワークフロー間で同じ依存構造を繰り返しているなら、再利用可能ワークフローへの切り出しを検討する(パターン5)

CIの実行時間はステップの中身を削るより、依存グラフの形を直すほうが効果が大きいことが少なくありません。手元のワークフローを一度図にして、無駄な直列がないか確認してみてください。

よくある質問

needsを増やすと必ずCIは遅くなりますか?

needs そのものが直接遅くするわけではなく、「待つ必要のない依存」を書いてしまうことが問題です。本当に前提となる成果物(ビルド済みアーティファクトなど)を待つ needs は必要な依存であり、削るべきではありません。見直すべきは「lintの完了さえ待てば十分なのに、前のジョブの完了まで待っている」ような不要な連鎖です。

concurrencyのcancel-in-progressは常にtrueにすべきですか?

PRの継続的なpushに対するCIでは基本的に有効です。ただしmainブランチへのマージ後のデプロイワークフローでは、キャンセルされると中途半端な状態でデプロイが止まるリスクがあるため、cancel-in-progress: false にするか、そもそもデプロイ系ワークフローには concurrency を付けない選択も検討してください。

fail-fastをfalseにするとCI時間は伸びますか?

伸びる可能性があります。fail-fast: true(既定)なら最初の失敗で残りのジョブを打ち切るため、平均的なCI時間は短くなります。false はすべての組み合わせの結果を知りたい場面(リリース前の全環境確認など)に限定して使うのが実務的なバランスです。

依存関係の見直しに使ったワークフローYAMLは外部に送信されますか?

いいえ。GitHub Actions Visualizer はすべてブラウザ内で処理するため、社内のワークフロー定義を貼り付けてもサーバーには送信されません。