⚙️ 業務自動化レシピ 2026.07.26 更新

GitHub Actionsが終わらないときの対策|timeoutと二重実行キャンセルの設定

GitHub Actionsが長時間終わらない原因を切り分け、timeout-minutesとconcurrencyを設定し、停止位置が分かるログと安全な後片付けを整える手順を解説します。

GitHub Actionsがいつまでも終わらないとき、画面から毎回キャンセルするだけでは再発を防げません。まず「処理が遅い」「入力待ちで止まった」「同じ処理が重なった」を分けます。そのうえで、上限時間の安全弁と、古い実行を自動で止める設定と、止まった場所が分かるログを一緒に用意します。

この記事では、ジョブへtimeout-minutesを付け、同じワークフローとブランチの古い実行をconcurrencyで止める最小構成を紹介します。あわせて、集めた証拠をAIへ渡して安全に直してもらう頼み方までまとめます。

長時間実行の原因を分ける

最初にActions画面で、止まっている単位を確認します。queuedのままなら実行枠やランナー(=処理を実行するコンピュータ)の割り当て、特定ステップから進まないなら、そのコマンド、通信、子プロセスが原因候補です。

次の順で見ると、推測だけで設定値を決めずに済みます。

  1. 過去の正常実行を開き、同じジョブとステップの所要時間を比べる。ジョブの実行時間はワークフロー実行の概要画面で確認できます
  2. 最後に出力されたログの直前で、外部通信、対話入力、監視プロセスを起動していないか確認する。各ジョブのログは表示、検索、ダウンロードができ、ステップごとの所要時間も見られます
  3. 同じブランチの実行が複数重なっていないか、Actionsの実行一覧で確認する
  4. キャッシュを使う処理なら、ヒット時とミス時を分けて比べる

見落としやすいのが「終了しないコマンド」です。たとえばNode.jsのテストランナーは、公式ドキュメントにあるとおりnode --test --watchで監視モードになり、プロセスが終了させられるまで動き続けます。CIでこれを呼ぶと、テストは成功しているのにジョブが永遠に終わりません。package.jsonscripts.testが何を呼んでいるかを確認し、使っているツールの「1回実行して終了する」コマンドへ直します。開発サーバーの起動コマンドも同じ理由で常駐します。

ここで集めた「止まったステップ名」「正常時と異常時の所要時間」「最後のログ数行」は、後の設定値決めと、AIへ修正を頼むときの材料になります。捨てずに控えておいてください。

状態主な対策設定場所注意点
ジョブ全体が終わらないtimeout-minutesjobs.<job_id>既定は360分。ジョブごとに指定する
特定ステップだけが終わらないtimeout-minutesjobs.<job_id>.steps[*]正の整数で指定し、最大360分
同じブランチの実行が重なるconcurrencyワークフロー直下またはジョブcancel-in-progress: trueで実行中もキャンセル
停止位置が分からないステップ分割とログsteps処理名、開始・終了、終了コードを残す
コマンドが常駐して終わらないコマンド自体を直すpackage.jsonなどwatchモードや開発サーバーは1回実行へ変える

timeout-minutesをジョブとステップへ設定する

timeout-minutesは、家のブレーカーのような安全弁です。原因を直す機能ではありませんが、これが無いと1回のハングで実行枠を何時間も無駄にします。

公式のワークフロー構文では、ジョブ単位のjobs.<job_id>.timeout-minutesで最大実行時間を分単位で指定できます。既定値は360分です。ただし、ランナー側の実行上限が先に来る場合は、指定時間より前に終了します。公式の上限一覧によると、GitHubホステッドランナーの1ジョブの実行上限は6時間です。これらの数値は変わることがあるため、公開時点の公式ページで確認してください。

次は、検査ジョブを15分で自動キャンセルする例です。

name: CI

on:
  push:
  pull_request:

jobs:
  check:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npm test
      - run: npm run build

timeout-minutesはワークフロー全体へ1個書けば全ジョブに効く設定ではありません。複数ジョブがある場合は、それぞれのジョブへ設定します。GitHub Actionsで記事サイトを自動検品する手順のように検査後にデプロイする構成では、両方のジョブに上限を付けます。

特定のステップだけに短い上限を付けたい場合は、jobs.<job_id>.steps[*].timeout-minutesも使えます。たとえば外部APIを呼ぶステップを5分で止めるなら、次のように書きます。

steps:
  - name: 外部APIを確認する
    timeout-minutes: 5
    run: npm run check-api

ステップ単位の値も正の整数で指定し、最大は360分です。小数は使えません。ジョブ側の上限が先に来れば、ステップの設定時間を待たずにジョブが終了します。

上限値は正常実行の履歴から決めます。キャッシュのヒットとミスなど、時間が変わる条件は分けて比べます。たとえば通常4分、遅い正常実行が7分なら、最初は10〜15分から試すという決め方があります。これはGitHubの公式推奨値ではなく、調整を始めるための編集上の目安です。上限へ近づく実行が増えたら、延長する前にステップの分割、不要処理の削除、キャッシュを見直します。依存関係の取得が遅い場合はGitHub Actionsのキャッシュ入門も確認してください。

concurrencyで同じブランチの古い実行を止める

短い間隔でpushすると、古いコミットの検査やデプロイが新しい実行と重なることがあります。新しい注文が来たら古い注文を取り消す、という動きを作るのがconcurrencyです。

timeout-minutesconcurrencyは書く階層が違います。次の統合例で位置を確認してください。concurrencyはワークフロー直下(nameonと同じ深さ)、timeout-minutesは各ジョブの中です。

name: CI

on:
  push:
  pull_request:

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

jobs:
  check:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

グループ名にgithub.workflowを含めるのは、別ワークフローとの衝突を避けるためです。公式のconcurrencyドキュメントによると、グループ名は大文字と小文字を区別せず、複数のワークフローで同じグループ名を使うと互いにキャンセルし合う可能性があります。github.refを含めるのは、別ブランチや別のプルリクエストまで巻き込んで止めないためです。

動きを2つに分けて覚えてください。第一に、同じグループで実行中にできるのは最大1件です。既定では待機中も1件で、さらに新しい実行が入ると、古い待機中の実行がキャンセルされて置き換わります。第二に、すでに実行中の処理まで止めるにはcancel-in-progress: trueが必要です。書かなければ、実行中の処理は最後まで走ります。

なお、公式ドキュメントには待機列を増やすqueue: maxという指定もあります。この記事のcancel-in-progress: true構成とは併用できない(検証エラーになる)ため、混同しないでください。仕様は変わることがあるため、公開時点の公式ページで確認してください。

本番デプロイを途中で止めると、外部環境が中途半端な状態になる場合があります。その場合は無条件にキャンセルせず、環境単位で直列化するなど方式を先に見直します。${{ github.workflow }}-${{ github.ref }}は公式例ですが、すべての運用の唯一の正解ではありません。

止まった場所が分かるログと、壊れない後片付け

タイムアウトだけ追加すると、「どこで止まったか分からないまま失敗する」状態が残ります。長いrunを小さなステップへ分け、ステップ名と開始・終了時刻を残します。

steps:
  - name: 依存関係を入れる
    run: |
      echo "npm ci start: $(date -Is)"
      npm ci
      echo "npm ci end: $(date -Is)"

  - name: ビルドする
    run: |
      echo "build start: $(date -Is)"
      npm run build
      echo "build end: $(date -Is)"

依存関係の取得にはnpm ciを使います。npm公式が、CIやデプロイなどの自動環境向けとして案内しているクリーンインストールです。npm installとの使い分けはCIでnpm ciを使う理由で詳しく説明しています。Windowsランナーでは、時刻表示をPowerShellのGet-Date -Format oへ置き換えます。環境変数の一括表示は避け、処理名、開始・終了、終了コードなど必要な情報だけを出します。秘密の値をログへ出さないためです。

通常ログで足りないときだけ、公式のデバッグログを使います。ACTIONS_STEP_DEBUGでステップの詳細ログ、ACTIONS_RUNNER_DEBUGでランナーの診断ログを有効にできます。常時オンにはせず、問題の再現時だけ有効にします。

キャンセル後の後片付けにも罠があります。公式のキャンセル仕様によると、キャンセル時は実行中のステップへ割り込みシグナルを送り、短い待ち時間で終了しなければプロセスツリーごと停止します。それでも残る対象は、サーバーが約5分後に強制終了します。つまり、通常の終了処理が完走するとは限りません。

if: ${{ always() }}を付けた後処理も万能ではありません。公式の式ドキュメントは、always()はキャンセル時にもtrueを返し、重大な失敗が起こり得る処理へ使うとタイムアウトまでハングする可能性があると警告しています。用途によってはif: ${{ !cancelled() }}が代替になります。

後片付けは次の方針で設計します。

  • 一時ファイルは次の実行と共有しない
  • 外部へ作る一時資源には実行IDと有効期限を付け、後から別の実行でも安全に回収できるようにする
  • 同じ削除処理を複数回実行しても壊れない形にする
  • 本番データの削除や切り戻しを、キャンセル時の無条件処理に入れない

AIへ修正を頼むときの渡し方と検証手順

ここまでの設定は、AIに書かせることもできます。ただし丸投げすると、関係ない場所まで書き換えられることがあります。次の材料をセットで渡すと安全です。

  1. 対象のワークフローファイル(.github/workflowsの中のYAML)の全文
  2. 止まったジョブ名とステップ名
  3. 通常時の所要時間と、止まったときの経過時間
  4. 最後に出力されたログの数行(トークン、URL内の資格情報、個人情報は伏せる)
  5. 同じブランチで実行が重なっていたかどうか
  6. 変更禁止の範囲(デプロイ手順や秘密の設定など、触らせない場所)
  7. 合格条件(次の検証がすべて通ること)

修正が返ってきたら、そのまま信用せず検証します。

  1. concurrencyがワークフロー直下、timeout-minutesが各ジョブの中にあるか、階層を目で確認する
  2. 一時的に待機コマンド(例: 上限より長いsleep)を入れたテスト実行で、時間どおりにタイムアウトすることを確認する
  3. 検証に使った待機コマンドを必ず削除する
  4. 同じブランチへ続けて2回pushし、古い実行がcancelledになり、新しい実行が続くことを実行一覧で確認する

本番デプロイのワークフローに入れる場合は、この検証に加えて、途中で止まっても再実行で復旧できることを先に確認してください。失敗をすぐ知りたい場合はGitHub Actionsの失敗通知を設定する方法も参照してください。

よくある質問

timeout-minutesはワークフロー全体に設定できますか?

ワークフロー全体へ一括適用する項目ではありません。ジョブごとにjobs.<job_id>.timeout-minutesを設定し、必要なら特定ステップにも短い上限を付けます。

timeout-minutesを省略すると何分で止まりますか?

ジョブの既定値は360分です。ただし、利用するランナー固有の実行上限が先に来る場合は、その時点で終了します。公式の上限一覧では、GitHubホステッドランナーは1ジョブ最大6時間です。数値は変わることがあるため、公開時点の公式案内で確認してください。

concurrencyを書くだけで、実行中の古い実行も止まりますか?

止まりません。実行中の処理までキャンセルするにはcancel-in-progress: trueが必要です。既定の動きは「新しい待機が古い待機を置き換える」ことで、実行中のキャンセルとは別です。

concurrencyで別ブランチや別ワークフローまで止まりませんか?

グループ名の作り方で決まります。公式例のgroup: ${{ github.workflow }}-${{ github.ref }}なら、同じワークフローかつ同じブランチ(またはPR)の実行だけが同じグループになるため、通常は別ブランチや別ワークフローを巻き込みません。

always()を使えば、キャンセル後の後片付けは必ず完了しますか?

保証されません。always()はキャンセル時にもtrueになりますが、ランナーの強制終了や通信断までは防げません。公式も、失敗し得る処理へalways()を使うとハングの可能性があると警告しています。外部資源には有効期限と、別実行から回収する手段を持たせてください。

まとめ

終わらないGitHub Actionsへの対策は、原因の切り分け、timeout-minutesの安全弁、concurrencyによる旧実行の停止、停止位置が分かるログ、キャンセル後の回復設計の5点セットです。次の順で手を動かしてください。

  • Actions画面で正常実行の所要時間を控え、遅い正常値を少し上回るtimeout-minutesを各ジョブへ付ける
  • ワークフロー直下へconcurrency(cancel-in-progress: true付き)を置き、同じブランチへ2回連続pushして、古い実行がcancelledになることを確認する
  • 止まったジョブ名・所要時間・最後のログを揃えてから、AIへ「変更してよい範囲」と「合格条件」を付けて修正を頼む

公式一次情報

確認日: 2026-07-26

続き(結論と実データ)はnoteに置いています

ここでは手順のところまで書きました。実際に出た数字、うまくいかなかった条件、そのまま使える設定ファイルは、 note の記事にまとめてあります。

noteで続きを読む Xをフォローする