Article
🚦 GitHub Actionsのconcurrency入門:二重デプロイと古いCIを防ぐ実務設定
GitHub Actionsを運用していると、短時間に複数回pushしたときに古いコミットのCIが最後まで走ったり、同じ環境へのデプロイが重なったりすることがあります。
そこで使えるのが concurrency です。concurrencyは「同じグループに属するworkflow/jobを同時に実行させない」ための仕組みです。
この記事では、単に設定例を載せるだけでなく、cancel-in-progress をいつ有効にするべきか、CIとデプロイでどう使い分けるかまで整理します。
まず結論
開発ブランチのCIなら、古い実行を止める次の設定が扱いやすいです。
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
一方、本番デプロイでは「新しいpushが来たから実行中のデプロイを途中で止める」という設計が危険な場合があります。その場合は cancel-in-progress: false を基本に考えます。
concurrencyとは
通常、GitHub Actionsでは複数のworkflow runを並列に実行できます。
例えば次の順でpushしたとします。
commit A -> CI開始
commit B -> CI開始
commit C -> CI開始
処理時間によってはA・B・CのCIが同時に動きます。
テストだけなら無駄なActions時間が増える程度で済むこともありますが、デプロイ処理では問題が大きくなります。
Aをproductionへデプロイ
Bをproductionへデプロイ
Cをproductionへデプロイ
完了順が保証されない処理では、意図しない競合を起こす可能性があります。
concurrency を設定すると、同じconcurrency groupに属するworkflowまたはjobを同時に走らせないよう制御できます。
基本設定
name: CI
on:
push:
branches:
- main
concurrency:
group: ci-main
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
group は排他制御の単位です。
同じ ci-main に属する実行はconcurrencyの制御対象になります。
cancel-in-progress: true は、同じグループで現在実行中の古いrunもキャンセルする設定です。
groupを固定文字列だけにしない
例えば複数ブランチでCIを動かしているのに、次のようにするとします。
concurrency:
group: ci
cancel-in-progress: true
featureブランチAのCI中にfeatureブランチBへpushすると、同じ ci グループなので互いに影響する可能性があります。
そこでGitHubのcontextを使います。
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
github.workflow はworkflow名、github.ref は実行対象のGit refです。
例えば概念的には次のように別グループになります。
CI-refs/heads/main
CI-refs/heads/feature/login
CI-refs/heads/feature/payment
これなら別ブランチのCIを誤ってキャンセルしません。
また、同じリポジトリ内の複数workflowで同じconcurrency group名を使うと、別workflow同士でも影響するため、github.workflow を含める設計は安全側です。
PRのCIで古い実行を止める
Pull Requestへ何度もpushするケースでは cancel-in-progress: true が特に便利です。
name: Pull Request CI
on:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
例えばレビュー指摘を直して3回連続でpushした場合、基本的に確認したいのは最新コミットの結果です。
古いコミットの重いテストを最後まで実行する必要がなければ、キャンセルすることでCI時間を節約できます。
デプロイでは慎重にする
CIとデプロイでは考え方を分ける必要があります。
例えば本番デプロイを次のように設定したとします。
concurrency:
group: production
cancel-in-progress: true
新しいrunが来ると、実行中のデプロイがキャンセル対象になります。
デプロイ途中のキャンセルを安全に扱えない仕組みでは危険です。
そのため、本番では例えば次のようにします。
concurrency:
group: production
cancel-in-progress: false
重要なのは「CIだからtrue、デプロイだからfalse」と機械的に決めることではなく、途中キャンセルしても処理が安全かを判断することです。
workflow全体ではなくjobだけ制御する
concurrency はworkflowレベルだけでなくjobレベルにも指定できます。
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
concurrency:
group: production
cancel-in-progress: false
steps:
- run: ./deploy.sh
この構成ならテストは通常どおり実行しつつ、productionへのdeploy jobだけを直列化できます。
「並列化できる処理まで全部止めたくない」という場合に有効です。
pendingの挙動にも注意する
concurrency groupを使うと、同じグループでは同時実行が制限されます。
GitHubの公式ドキュメントでは、通常は同じグループで最大1つがrunning、1つがpendingとなり、新しいpendingが来ると既存pendingが置き換えられる動作が説明されています。
そのため、concurrencyは単純な「無制限のFIFOキュー」と同じではありません。
「すべての実行を必ず順番に処理したい」という要件では、利用しているGitHub Actionsの現在のqueue設定を含め、公式仕様を確認して設計する必要があります。
よくあるハマりどころ
1. 全ブランチで同じgroupを使う
concurrency:
group: build
これでは無関係なブランチまで同じ排他グループになります。
ブランチ単位なら次のようにします。
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
2. 本番デプロイに無条件でcancel-in-progressを付ける
cancel-in-progress: true
デプロイ処理が途中キャンセルに対応しているか確認します。
DBマイグレーションや外部サービス更新など、途中停止で状態が中途半端になる処理があるなら特に注意が必要です。
3. concurrencyをロック機構だと思い込む
concurrencyはGitHub Actions上のworkflow/job実行を調整する仕組みです。
アプリケーション側の分散ロックやDBの排他制御そのものを置き換えるものではありません。
例えば複数の別システムから同じDB更新処理を呼び出せるなら、アプリケーション側でも整合性を守る必要があります。
CIとデプロイの使い分け
目安としては次のように考えられます。
| 用途 | group例 | cancel-in-progress |
|---|---|---|
| PRのテスト | ${{ github.workflow }}-${{ github.ref }} |
true |
| featureブランチのCI | ${{ github.workflow }}-${{ github.ref }} |
true |
| mainのビルド | ${{ github.workflow }}-${{ github.ref }} |
要件次第 |
| stagingデプロイ | staging |
要件次第 |
| productionデプロイ | production |
原則、途中キャンセルの安全性を確認して決める |
まとめ
GitHub Actionsで短時間に複数回pushされる環境では、concurrency を設定するだけで不要な古いCIやデプロイ競合を減らせます。
特にPRのCIでは、次の形から始めると扱いやすいです。
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
一方、productionデプロイでは「最新だけ残せばよい」とは限りません。途中キャンセル時の安全性を確認したうえで cancel-in-progress を決めることが重要です。
参考
- GitHub Docs: Concurrency
- GitHub Docs: Workflow syntax for GitHub Actions