GitHub Actionsの待機ジョブがキャンセルされる原因とconcurrencyの正しい設定

GitHub Actionsで「先の実行が終わるまで、後続を順番に待たせたい」と考えてconcurrencyを設定しても、待機中のジョブやワークフローがCancelledになることがあります。

原因は、通常のconcurrencyが無制限の待ち行列ではなく、同じグループにつき実行中1件・待機中1件を基本とする仕組みだからです。3件目が到着すると、先に待っていた2件目はキャンセルされ、3件目に置き換わります。

すべての実行を順番に処理したい場合は、cancel-in-progressだけでなく、queue: maxを設定する必要があります。これにより、同じグループ内で最大100件まで待機させられます。([GitHub Docs][1])

目次

GitHub Actionsの待機ジョブが消える理由とconcurrencyの設定

GitHub Actionsのconcurrencyは、同じデプロイ先や共有リソースに対して、複数の処理が同時実行されるのを防ぐ機能です。

たとえば、次のような設定があるとします。

concurrency:
  group: production-deploy

この設定では、production-deployという同じグループに属するワークフローは、同時に1件しか実行されません。

ただし、デフォルトのキュー設定はsingleです。そのため、実際の動きは次のようになります。

実行状態
1件目実行中
2件目待機中
3件目が到着2件目をキャンセルし、3件目が待機
4件目が到着3件目をキャンセルし、4件目が待機

つまり、デフォルト設定は「全部を順番に処理する」のではなく、現在実行中の1件と、最新の待機1件だけを残す動作です。GitHub公式ドキュメントでも、既存のpending実行は、新しくキューに入った実行によってキャンセル・置換されると説明されています。([GitHub Docs][2])

cancel-in-progressをfalseにするだけでは解決しない

よくある誤解が、次のようにcancel-in-progress: falseを指定すれば、すべての実行が順番待ちになるというものです。

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

cancel-in-progressが制御するのは、現在実行中の処理をキャンセルするかどうかです。

一方、待機中の処理を何件保持するかは、queueが制御します。

設定項目制御する対象
cancel-in-progress現在実行中のジョブやワークフロー
queue待機中のジョブやワークフロー

そのため、cancel-in-progress: falseを指定しても、queueを省略した場合はデフォルトのqueue: singleが使われます。実行中の処理は継続しますが、古い待機処理は新しい処理によって置き換えられます。

すべての実行を順番に待たせる設定

後続の実行をキャンセルせず、先の実行が終了するまで順番に待機させるには、queue: maxを指定します。

concurrency:
  group: production-deploy
  queue: max

この設定では、同じグループについて次の状態を保持できます。

  • 実行中:最大1件
  • 待機中:最大100件
  • キューが満杯になった後の追加実行:キャンセル

queue: maxを指定した場合、待機中の実行は、原則としてグループで待機を開始した順番に処理されます。([GitHub Docs][2])

ワークフロー全体を順番に実行する例

ワークフロー全体を直列化したい場合は、concurrencyをトップレベルに記述します。

name: Production deploy

on:
  push:
    branches:
      - main

concurrency:
  group: deploy-${{ github.workflow }}-${{ github.ref }}
  queue: max

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Run deployment
        run: |
          echo "ここにデプロイ処理を記述"

この例では、同じワークフローかつ同じブランチから発生した実行が、1件ずつ順番に処理されます。

${{ github.workflow }}を含めることで、別のワークフローが同じブランチで動いた場合の意図しない競合を避けやすくなります。

${{ github.ref }}を含めると、mainと開発ブランチなどを別々のグループとして扱えます。すべてのブランチを共通の1列で処理したい場合は、固定名を使用します。

concurrency:
  group: production-deploy
  queue: max

特定のジョブだけを順番に実行する例

テストやビルドは並列実行し、デプロイだけを1件ずつ処理したい場合は、ジョブレベルにconcurrencyを設定します。

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Run tests
        run: echo "テストを実行"

  deploy:
    needs: test
    runs-on: ubuntu-latest

    concurrency:
      group: production-deploy
      queue: max

    steps:
      - name: Run deployment
        run: echo "デプロイを実行"

この構成では、testジョブは各ワークフローで進行できますが、deployジョブはproduction-deployグループ内で直列に処理されます。

設定場所適しているケース
ワークフローのトップレベルワークフロー全体を先頭から順番に実行したい
jobs.<job_id>.concurrencyデプロイなど、特定のジョブだけを直列化したい

無駄なビルドやテストも含めて待たせたいならワークフローレベル、共有環境に触れる処理だけを制限したいならジョブレベルが適しています。

queue: maxとcancel-in-progress: trueは併用できない

次の設定は使用できません。

concurrency:
  group: production-deploy
  queue: max
  cancel-in-progress: true

queue: maxは「複数の実行を待機させる」設定です。一方、cancel-in-progress: trueは「新しい実行が来たら、現在実行中の処理もキャンセルする」設定です。

この2つは動作方針が矛盾するため、同時に指定するとワークフローの検証エラーになります。queue: maxを使う場合は、通常はcancel-in-progressを省略します。([GitHub Docs][2])

目的別の設定は次のように整理できます。

実現したい動作設定
実行中は継続し、最新の待機1件だけ残すqueuecancel-in-progressを省略
実行中もキャンセルし、常に最新実行を優先するcancel-in-progress: true
すべての実行を順番に処理するqueue: max
最大100件を超えても絶対に失わないconcurrencyだけでは対応できない

100件を超える可能性があり、1件も失えない処理では、外部キューや専用の実行管理を検討する必要があります。

concurrencyグループ名の衝突を確認する

queue: maxを設定しても、意図しないジョブが同じグループへ入っていると、想定外の待機やキャンセルが発生します。

特に確認したいのは、複数のワークフローファイルで同じ固定名を使用しているケースです。

concurrency:
  group: production

同じリポジトリ内の別ワークフローでもgroup: productionを使っていれば、それらは同じグループとして扱われます。

ワークフロー単位で分離したい場合は、次のようにワークフロー名を含めます。

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  queue: max

反対に、複数のワークフローから同じ本番環境へデプロイしており、絶対に並列実行したくない場合は、意図的に固定のグループ名を共有します。

concurrency:
  group: production-deploy
  queue: max

グループ名は大文字と小文字を区別しません。たとえば、productionProductionPRODUCTIONは同じグループとして扱われます。表記だけを変えて別グループにしたつもりでも分離されないため注意が必要です。([GitHub Docs][2])

FIFOでもイベント発生順が保証されるとは限らない

queue: maxの待機処理はFIFOですが、その基準は、各ジョブやワークフローがconcurrencyグループで待機を開始した時刻です。

次の順番を絶対に保証するものではありません。

  • コミットされた時刻
  • pushイベントが発生した時刻
  • 手動実行ボタンを押した時刻
  • ワークフローが作成された一覧上の順番

ワークフローごとに事前ジョブや条件判定が異なる場合、後から開始されたワークフローが先にconcurrencyグループへ到達する可能性があります。

GitHub公式ドキュメントでも、FIFOはワークフローのdispatch時刻ではなく、グループで待機を開始した時刻を基準とし、実際の開始タイミングによって順序は保証されないとされています。([GitHub Docs][2])

厳密なイベント発生順で処理しなければならないシステムでは、concurrencyだけを順序保証の仕組みとして使わない方が安全です。

待機中のジョブがキャンセルされるときの確認手順

原因を切り分けるときは、次の順番で確認します。

concurrencyの設定場所を確認する

ワークフローファイル内で、concurrencyがトップレベルとジョブレベルのどちらにあるか確認します。

concurrency:

だけでなく、次の形式も検索します。

jobs:
  deploy:
    concurrency:

ジョブレベルの設定だった場合、ワークフロー全体ではなく、そのジョブがグループへ到達した時点から待機制御が始まります。

queueの指定を確認する

次のようにqueueが省略されていれば、待機できるのは1件だけです。

concurrency:
  group: production-deploy

すべて残す必要がある場合は、次のように修正します。

concurrency:
  group: production-deploy
  queue: max

cancel-in-progressを確認する

次の指定があれば、待機中だけでなく、現在実行中の処理もキャンセル対象になります。

cancel-in-progress: true

順番にすべて実行したい場合は削除します。

リポジトリ内の全ワークフローを検索する

.github/workflows配下でconcurrencyやグループ名を検索します。

同じリポジトリの別ワークフローが、同じ固定グループ名を使用していないか確認してください。

.github/workflows/deploy.yml
.github/workflows/manual-deploy.yml
.github/workflows/release.yml

たとえば、これらすべてでgroup: productionを指定していれば、別ファイルであっても同じグループとして競合します。GitHub公式ドキュメントでも、複数ワークフロー間で意図しないキャンセルを防ぐには、グループ名をワークフローごとに一意にする必要があると説明されています。([GitHub Docs][1])

キュー上限を確認する

queue: maxで保持できる待機実行は最大100件です。

短時間に大量の実行が発生する構成では、キューが満杯になり、追加の実行がキャンセルされていないか確認します。

小さな検証用ワークフローで動作を確認する

本番デプロイで直接確認するのではなく、一時的な検証用ワークフローを用意すると原因を切り分けやすくなります。

name: Concurrency queue test

on:
  workflow_dispatch:

concurrency:
  group: concurrency-queue-test
  queue: max

jobs:
  wait:
    runs-on: ubuntu-latest

    steps:
      - name: Wait
        run: sleep 60

このワークフローを短時間に3回手動実行し、1件目が実行中、後続が待機状態になることを確認します。

確認後はテスト用ファイルを削除するか、通常の運用では起動しない状態にしてください。検証画面を共有するときは、組織名、非公開リポジトリ名、トークン、秘密鍵、シークレットの値などを画像やログに含めないよう注意が必要です。

GitHub Enterprise Serverを利用している場合は、GitHub.com向けの説明だけで判断せず、使用しているサーバーバージョンに対応するドキュメントでqueueの利用可否と構文を確認してください。

待機実行を残したいならqueueを明示する

GitHub Actionsの待機中ジョブがキャンセルされるのは、concurrencyのデフォルトが完全な待ち行列ではなく、実行中1件と待機中1件だけを保持するためです。

すべての実行を順番に処理したい場合は、次の形を基本にします。

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  queue: max

そのうえで、次の点を確認します。

  • cancel-in-progress: trueを併用しない
  • 別ワークフローとのグループ名衝突を確認する
  • 大文字と小文字だけでグループを分けない
  • 待機上限は100件と理解する
  • FIFOをイベント発生時刻の絶対順序と考えない

まずは対象のYAMLでqueueが省略されていないかを確認し、複数の待機実行を残す必要がある場合はqueue: maxへ変更してください。
[1]: https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency “Control the concurrency of workflows and jobs – GitHub Docs”
[2]: https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax “Workflow syntax for GitHub Actions – GitHub Docs”

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次