これまで GitHub Actions の並列化といえば、needsmatrix による job 単位の話でした。同一 job 内の steps は上から順にしか動きません。非同期にしたいときは、シェルの & でバックグラウンド実行する程度の回避策しかありませんでした。& はログが混ざりやすく、ステップ単位の成否や出力の扱いも弱いです。

2026 年 6 月下旬から、同一 job 内で steps を並列実行できます。キーワードは backgroundwait / wait-allcancel、そして糖衣の parallel です。ログはステップごとに分かれます。長時間サービスを起動してから本処理を走らせ、終わったら止める、といったパターンも YAML 上で明示できます。

#構文

#parallel

独立したステップ群をまとめて同時実行し、すべて終わるまで次へ進まない書き方です。各ステップは内部的に background になり、グループ末尾で暗黙の wait が走ります。

yaml
steps:
  - uses: actions/checkout@v6

  - parallel:
      - name: Build frontend
        run: npm run build:frontend

      - name: Build backend
        run: npm run build:backend

      - name: Build docs
        run: npm run build:docs

  - name: Run tests after all builds complete
    run: npm test

「この塊だけ同時に走らせ、全部終わったら次へ」というパターンには、parallel が簡潔です。

#background / wait / wait-all / cancel

細かい制御が必要なら background: true を使います。起動したステップはすぐ次へ進み、必要なタイミングで待つ・止めます。

yaml
steps:
  - name: Build frontend
    id: build-frontend
    run: npm run build:frontend
    background: true

  - name: Build backend
    id: build-backend
    run: npm run build:backend
    background: true

  - name: Run linter while builds run
    run: npm run lint

  - name: Wait for both builds to finish
    wait: [build-frontend, build-backend]

  - name: Run tests
    run: npm test
  • wait: 文字列、または配列で指定した id の background ステップ完了を待つ
  • wait-all: 実行中の background ステップすべてを待つ。引数はありません
  • cancel: 指定 id の background ステップへ終了信号を送る。まず SIGTERM、終了しなければ SIGKILL

parallel は便利ですが、表現力は限られます。サーバを立てたまま後続ステップを動かし、最後に止める流れは background 向きです。

#公式上の制約

次を押さえておくと事故が減ります。

  • 同一 job で同時に走れる background ステップは最大 10。超えた分はスロットが空くまでキューに入ります。foreground のステップは、background のスロットが埋まっていてもすぐ実行されます
  • composite action の中では background / parallel を宣言できません。composite 自体を background ステップとして走らせることはできます
  • wait / wait-all / cancelif は付けられません。常に実行されます
  • background ステップの outputs や環境変更は、それを含む wait / wait-all の後でのみ後続から使えます
  • background が失敗すると、それを待つ次の wait / wait-all で job が失敗します。当該ステップに continue-on-error がある場合は除きます
  • ジョブ終了前の post 処理の前に、未 wait の background に対する暗黙の wait-all が走ります

#job 並列との使い分け

観点job 並列(needs / matrixsteps 並列(parallel / background
ランナー別ランナー同一ランナー
セットアップ各 job で重複しやすい1 回の setup を共有できる
ファイルシステム分離共有
向いてる処理CPU / メモリを食い合う重い独立処理I/O 待ちの重ね合わせ、軽い独立チェック、サービス起動
具体例Go / Rust のクロスコンパイル、重い production build、CPU を張り付くテスト一式、複数 OS / Node 版の matrix独立したデプロイの安定待ち、lint / format / 軽い typecheck、ビルド中の lint、dev server 起動後のテスト

CPU をほぼ使い切る独立タスクは、従来どおり matrix や別 job の方が終わるまでの時間が短くなりがちです。待ち時間が支配的な処理や、同一ワークスペース上の軽いチェックは steps 並列の方が向きます。

表の job 並列には、別 workflow に分けて同じイベントで並走させる場合も含めて考えてよいです。ランナーが分かれ、setup が重複し、所要時間とコストのバランスも別 job と同型だからです。steps 並列へ寄せる価値が出るのは、トリガーが同じで権限を共有してよく、軽い検査同士を 1 回の setup 後に畳みたいときです。待ちの多い処理と軽い検査の組み合わせでも同様です。主に効くのはコストです。所要時間は、別 workflow のまま並走していた場合と大きく変わらないことが多いです。次のようなときは分けたままの方がよいです。

  • トリガーが違う。たとえば PR では lint のみ、本番ブランチだけデプロイする、など
  • デプロイ用の強い secrets や environment を lint 側に載せたくない
  • 必須チェックや失敗の意味を workflow 単位で分けたい

#最適化パターン

効果の見方は 2 つあります。ワークフローや job が終わるまでの所要時間と、動いた runner の合計時間です。後者は課金の目安になるコストです。同じ parallel でも、パターンによって前者を縮めるもの、後者を抑えるもの、どちらもあまり期待できないものがあります。

#A. 独立ビルドを並べる

互いに依存しないビルドを直列に並べているなら、parallel で最長の 1 本に近づけます。狙いは所要時間の短縮です。同一ランナー上なので、job が短くなればその job 分のコストも連動して下がることがあります。

yaml
steps:
  - uses: actions/checkout@v6
  - uses: actions/setup-node@v4
    with:
      node-version: "22"
      cache: npm
  - run: npm ci

  - parallel:
      - name: Build frontend
        run: npm run build:frontend
      - name: Build backend
        run: npm run build:backend
      - name: Build docs
        run: npm run build:docs

#B. I/O・待ち時間の重ね合わせ

デプロイ先の安定待ちのように、ランナー上ではほとんど CPU を使わず外部完了を待つステップは、互いに独立なら並列化の効果が大きいです。直列だと待ち時間が足し算になり、並列だと長い方に律速されます。ここでも主に縮むのは所要時間です。待ちの足し算が消えるぶん、その job のコストも下がりやすいです。

yaml
steps:
  # task definition の準備などは省略
  - parallel:
      - name: Deploy web service
        uses: aws-actions/amazon-ecs-deploy-task-definition@v2
        with:
          task-definition: ${{ steps.render-web.outputs.task-definition }}
          wait-for-service-stability: true

      - name: Deploy worker service
        uses: aws-actions/amazon-ecs-deploy-task-definition@v2
        with:
          task-definition: ${{ steps.render-worker.outputs.task-definition }}
          wait-for-service-stability: true

たとえば基盤のあとアプリ、といった依存があるデプロイは並列にしません。

#C. サービス起動・本処理・停止

アプリ本体の dev server やローカルプロセスを立ててからテストし、終わったら止めます。主眼は利便と制御です。ログがステップごとに分かれ、成否や停止も YAML 上で明示できます。所要時間やコストを大きく削る手法というより、シェルの & や無理な job 分割をやめるための書き方です。もともとサーバ起動用に別 job を立てていたなら、1 job に寄せることでコストは下がることがあります。

yaml
steps:
  - uses: actions/checkout@v6
  - run: npm ci

  - name: Start server
    id: server
    run: npm start
    background: true

  - name: Run tests against the server
    run: npm test

  - name: Stop the server
    cancel: server

cancelif は付けられませんが、制御フロー用ステップとして常に実行されます。テスト失敗時もサーバ停止の意図は残ります。コンテナ化された依存だけなら jobs.<job_id>.services も選択肢です。リポジトリ内の npm start のような通常ステップには background が合います。

#D. ビルドと Lint のインターリーブ

ビルドを background で走らせている間に Lint など別系統の作業を進め、必要な成果物の完了だけ待ちます。ビルド待ちの隙間を埋めるので、所要時間が縮みやすいです。job が短くなれば、そのぶんコストも連動して下がることがあります。

yaml
steps:
  - uses: actions/checkout@v6
  - run: npm ci

  - name: Build frontend
    id: build-frontend
    run: npm run build:frontend
    background: true

  - name: Build backend
    id: build-backend
    run: npm run build:backend
    background: true

  - name: Lint while builds run
    run: npm run lint

  - wait: [build-frontend, build-backend]

  - name: Integration tests
    run: npm test

#E. 非ブロッキングな副作業

本処理と独立したアップロードやテレメトリ送信を background で始め、パッケージングなどを続け、最後に揃えます。本処理と副作業の待ちを重ねるので、所要時間の短縮が主目的です。job が短くなればコストも連動して下がることがあります。

yaml
steps:
  - name: Upload telemetry
    id: telemetry
    run: ./scripts/upload-telemetry.sh
    background: true

  - name: Package release
    run: ./scripts/package.sh

  - wait: telemetry

副作業の失敗で job を落としたくない場合は、background 側に continue-on-error: true を検討します。

#F. 軽いチェックの畳み込み

もともと別 job で並走していた lint / format / typecheck を、1 job の setup 後に parallel でまとめます。別ランナーでの並走です。終わるまでの所要時間は、もともと job 並列だった場合と大きく変わらないことが多いです。ここで効くのはコスト側です。checkout や install の重複が減り、runner の合計時間が下がります。

yaml
jobs:
  checks:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: npm
      - run: npm ci

      - parallel:
          - name: Lint
            run: npm run lint
          - name: Format check
            run: npm run format:check
          - name: Type check
            run: npm run typecheck

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: npm
      - run: npm ci
      - run: npm test

重い test やメモリを食う build まで同じランナーに詰め込むと、次のアンチパターンになりがちです。軽い独立チェックだけ畳み、重い処理は別 job に残す併用が現実的です。

#G. アンチパターン: 低スペックへの詰め込み

ubuntu-slim は 1 CPU / メモリ 5 GB の軽量ランナーです。ここに CPU やメモリを食う複数ステップを parallel で載せると、互いに奪い合って各ステップが単独実行より遅くなり、job 全体が最長ステップに引きずられます。所要時間もコストも改善せず、悪化することがあります。

#H. アンチパターン: CPU ヘビーな steps 並列

Go のクロスコンパイルのように、各ステップが CPU をほぼ使い切る独立ビルドを同一ランナーで parallel しても、コア数が足りなければコンテキストスイッチが増えるだけで所要時間はほとんど縮まないことがあります。steps 並列だけでは、所要時間もコストもあまり期待できません。終わるまでの時間を優先するなら、strategy.matrix や別 job でランナーを分ける方が速くなりがちです。そのぶん runner 合計時間と課金は増えやすいです。

#まとめ

steps 並列で狙える効果は大きく次の 2 つです。

  1. 直列だった独立作業を並べ、終わるまでの所要時間を最長のものに近づける。待ち時間の重ね合わせが特に効きます
  2. 別ランナーに分散していた軽いチェックを 1 job に畳み、セットアップ重複を減らしてコストを抑える

どちらも、同一ランナーの CPU とメモリを共有する制約を抱えます。重い処理は job 並列、別 workflow も含めてそちら側です。軽い独立作業と I/O 待ちは steps 並列です。適切に使い分けましょう。

#参考文献