GitHub Actions の concurrency でワークフローを制御する

概要

GitHub Actions を使っていると、「同じブランチへの連続 push で古い CI と新しい CI が競合する」「本番デプロイが二重に走る」といった問題に遭遇します。これらは concurrency キーで解決できます。

この記事では、concurrency の基本から、よく使うパターン、ハマりやすいポイントまでを入門者向けに整理します。

concurrency とは

concurrency は「同時に走ってよいワークフロー実行のグループ」を定義する仕組みです。同じ concurrency グループに属する実行は同時に 1 つしか走らず、新しい実行が来たときの挙動を制御できます。

設定するキーは 2 つだけです。

  • group: 同時実行を排他制御するグループ名(文字列・式が使える)
  • cancel-in-progress: true のとき、同じグループで実行中のものをキャンセルして新しい実行を優先する(既定は false)

発生しがちな問題

1. 連続 push で CI が無駄に積み上がる

PR にコミットを立て続けに push すると、古いコミットに対する CI がまだ走っているのに新しい CI が始まり、実行キューが膨らみます。古いコミットの結果はもう不要なことが多いです。

2. デプロイが二重に走る

main への push でデプロイするワークフローが短時間に 2 回トリガーされると、デプロイ処理が並行して走り、成果物が競合します。

対応方法

パターン A: PR の古い CI を自動キャンセルする

ブランチ単位でグループ化し、新しい実行が来たら古いものをキャンセルします。

name: CI

on:
  pull_request:

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: echo "run tests"

github.ref を使うことで PR ブランチごとに別グループになり、別の PR の CI まで巻き込んでキャンセルしてしまうことを防げます。

パターン B: 本番デプロイを直列化する(キャンセルしない)

デプロイは途中でキャンセルすると中途半端な状態になりかねないので、cancel-in-progress: false にして「前のデプロイが終わるまで次を待たせる」のが安全です。

name: Deploy

on:
  push:
    branches: [main]

concurrency:
  group: deploy-production
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: echo "deploy to production"

グループ名を固定文字列 deploy-production にすることで、どの実行も同じグループに入り、必ず直列に処理されます。

パターン C: ジョブ単位で concurrency を使う

concurrency はワークフロー全体だけでなく、ジョブ単位でも指定できます。CI は並行で走らせつつ、デプロイジョブだけ直列化したい場合に便利です。

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: echo "tests run in parallel"

  deploy:
    needs: test
    runs-on: ubuntu-latest
    concurrency:
      group: deploy-${{ github.ref }}
      cancel-in-progress: false
    steps:
      - run: echo "deploy serialized"

環境ごとのグループ設計

複数環境(staging / production)がある場合、グループ名に環境やブランチを織り込むと混線しません。

concurrency:
  group: deploy-${{ github.ref_name }}
  cancel-in-progress: false

github.ref_name(ブランチ・タグ名)を使うと、main と develop が別グループになり、片方のデプロイがもう片方を待たせることがなくなります。

確認方法

  1. 同じブランチに短い間隔で 2 回 push する
  2. Actions タブで、古い実行が Canceled(パターン A の場合)または待機状態(パターン B の場合)になっているかを確認する
  3. cancel-in-progress: true なら古い実行にキャンセルマークが付き、false なら新しい実行が「waiting for ...」と待機状態になる

注意点

  • グループ名の設計がすべて。固定文字列にすれば全実行が直列化、github.ref を含めればブランチごとに分離される。意図せず広いグループにすると無関係な実行まで待たされる。
  • キャンセルしていいかを区別する。CI・Lint はキャンセル可、デプロイ・マイグレーションなど途中断で壊れる処理はキャンセル不可が原則。
  • 待機中の実行は最新 1 件だけが残る。cancel-in-progress: false の場合でも、待機中に新しい実行が来ると、待機していた古い方はキャンセルされ、キューには最新 1 件だけが残る。
  • 追加コストなし。ランナー時間を余計に消費せず、むしろ無駄な実行を減らせるのでコスト削減にもつながる。

まとめ

  • concurrency は group と cancel-in-progress の 2 つだけで同時実行を制御できる
  • PR の CI は「ブランチ単位グループ + キャンセルあり」で無駄を削る
  • 本番デプロイは「固定グループ + キャンセルなし」で直列化し二重デプロイを防ぐ
  • グループ名の設計が挙動を決めるので、どこまでを 1 グループにするかを最初に決めるのがコツ

小さな設定で CI/CD の安定性とコストが両方改善します。まずは PR の CI にキャンセルありを入れるところから試すのがおすすめです。

\ 最新情報をチェック /

コメントを残す

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください。