「gh workflow run で投入した run が、何時間たっても queued のまま動かない」「gh run cancel を打ったら "Cannot cancel a workflow run that is completed" と返ってきた」「夜間の自動デプロイが朝まで止まっていて、何も起きなかったように見えた」——GitHub Actions を CLI から無人で回していると、いつか必ず踏む詰まりです。
原因のほとんどは自分のワークフロー設定ではなく、GitHub Actions 側の障害中に投入した run が「ゴースト化」してキューから二度と拾われないことにあります。この記事では、ゴースト run を1コマンドで見分ける方法、待たずに復旧する手順、そして無人スクリプトが朝まで固まらないための gh run watch のタイムアウト設計をまとめます。
-
gh run view <run-id> --json status,conclusion,updatedAt で queued のまま updatedAt が投入時刻から動かなければゴースト run。待っても解けない - 直近の run が数分で成功しているなら自分の設定の問題ではない。GitHub Status で Actions の障害を確認する
- 復旧は「同じコマンドで再ディスパッチ」。ゴースト run はキャンセルもできないので放置してよい
- 無人スクリプトの
gh run watch には必ず timeout を付ける。付けないと lock を握ったまま朝まで止まる - 証跡は「run を投入した」でなく「run が success した」を条件に書く
目次を開く
症状:queued のまま何時間も動かない run
まず「順番待ち」なのか「ゴースト」なのかを切り分けます。見るのは次の4点です。
| 見るもの | 正常な順番待ち | ゴースト run |
|---|---|---|
status | queued → 数分で in_progress | queued のまま数時間 |
updatedAt | ランナー割当のたびに更新される | 投入時刻(createdAt)から一度も動かない |
gh run cancel | キャンセルできる | Cannot cancel a workflow run that is completed と矛盾した応答が返る |
| 直近の run | — | 直前まで数分で success していた |
# 状態と最終更新時刻を見る
gh run view <run-id> --repo <owner>/<repo> --json status,conclusion,createdAt,updatedAt
# 直近の run が正常だったかを並べて見る
gh run list --repo <owner>/<repo> --workflow deploy.yml --limit 6 status が queued、conclusion が空、updatedAt が createdAt とほぼ同じ時刻のまま何時間も止まっている——この組み合わせが揃ったらゴースト run です。ランナー不足の順番待ちなら updatedAt は動きますし、self-hosted runner 側の問題なら直近の run も落ちているはずです。
原因:Actions の障害中に投入した run はキューから拾われない
実際に踏んだ例では、00:05 に gh workflow run で投入した直後の 00:11 に GitHub Actions の障害("degraded availability for Actions")が始まり、障害が解消した後も run は9時間半 queued のまま拾われませんでした。直近6回の run はすべて3分で成功していたので、ワークフローや権限の問題ではありません。
障害中にキューへ入った run は、復旧後に再スケジュールされないことがある——そう考えておくのが安全です。「もう少し待てば動くはず」は当たりません。障害の有無は GitHub Status(githubstatus.com)の Actions の項目で確認できます。
復旧手順:待たずに再ディスパッチする
- GitHub Status で Actions の障害が解消していることを確認する
- ゴースト run はキャンセルしない(そもそも
cancelが通らない)。そのまま放置してよい - 元と同じコマンドで再投入する
# 例:入力付きのワークフローを再投入する
gh workflow run deploy.yml --repo <owner>/<repo> -f build_mode=cached
# 数秒待ってから、新しい run の id と状態を取る
sleep 10
gh run list --repo <owner>/<repo> --workflow deploy.yml --limit 1 --json databaseId,status --jq '.[0]' 障害が解消していれば新しい run は即 in_progress になり、通常どおり完走します。取り残されたゴースト run は一覧に残り続けますが、実害はありません。
無人スクリプトの穴:gh run watch にタイムアウトが無い
手動なら「進まないな」と気づけますが、無人スクリプトでは事情が違います。gh run watch <run-id> --exit-status は run が終わるまで戻ってきません。ゴースト run を watch すると、スクリプトは flock の lock を握ったまま朝まで止まり、後続の証跡・通知・コミットが一切出ません。翌日の定時起動も「前回がまだ動いている」と判断して skip します。結果として「昨夜は何もなかった」ように見えるのに、実際には途中の工程(この例では CMS への書込み)まで完了している、という一番読みにくい状態になります。
直し方は1行です。timeout を前に付けます。
#!/bin/bash
set -u
REPO="<owner>/<repo>"
LOCK="/path/to/deploy.lock"
exec 9>"$LOCK"
flock -n 9 || { echo "skip: running"; exit 0; }
gh workflow run deploy.yml --repo "$REPO" -f build_mode=cached
sleep 10
RUN_ID=$(gh run list --repo "$REPO" --workflow deploy.yml --limit 1 --json databaseId --jq '.[0].databaseId')
# ここが要点:timeout を必ず付ける(通常3分の run なら 30m で十分)
timeout 30m gh run watch "$RUN_ID" --repo "$REPO" --exit-status --interval 30
RC=$?
case "$RC" in
0) echo "deploy success run=$RUN_ID" ;;
124) echo "deploy TIMEOUT run=$RUN_ID(未完了・朝に手動確認)" ;;
*) echo "deploy failed rc=$RC run=$RUN_ID" ;;
esac -
timeoutの終了コード124は「未完了」として証跡に残します。success と区別できないと、朝に見る人が誤読します。 - 対話セッションで待つときも同じ形にします。
timeout 8m gh run watch <run-id> --repo <owner>/<repo> --exit-status --interval 20のように短めの上限を付け、戻り値を確認する癖をつけると、端末を占有されずに済みます。
すでに止まってしまったスクリプトの救出
朝になって lock を握ったまま止まっているスクリプトを見つけたら、スクリプト本体ではなく watch のプロセスだけ を止めます。
# watch のプロセスを探して止める(スクリプト本体は殺さない)
pgrep -af "gh run watch"
kill <pid>
# lock が解放されたか確認する
flock -n /path/to/deploy.lock true && echo free watch だけを止めれば、スクリプトは「完了状態不明」として次の工程へ進み、遅れて完走します。そのあとで前述の再ディスパッチを行えば、本番反映も数分で終わります。
証跡は「投入した」でなく「success した」を条件に書く
投入直後の run id をそのまま証跡や台帳に書くと、ゴースト化したときに「取り残された方の id」が記録として残ります。あとから読む人は、その id を見ても何が反映されたのか分かりません。
無人ループの記録は `gh run watch` が 0 で戻った run の id を書く条件にします。timeout で戻ったときは「デプロイ未完了・朝に手動」と明記し、朝に再ディスパッチした run の id で上書きします。
まとめ
-
queuedのままupdatedAtが動かず、cancelが「completed」と返すなら、ゴースト run。待っても解けない - 直近の run が数分で成功していたなら自分の設定ではなく Actions 側の障害。GitHub Status で裏を取る
- 復旧は同じコマンドで再ディスパッチ。ゴースト run は放置でよい
- 無人スクリプトの
gh run watchにはtimeout 30mを付け、終了コード124を「未完了」として証跡に残す - 止まってしまったら watch のプロセスだけ
killして後続を進める
よくある質問(FAQ)
ゴースト run を放置して問題はありませんか?
実害はありません。一覧に queued のまま残りますが、ランナーを消費せず課金も発生しません。キャンセルしようとしても「completed」と返るので、そのままにしておきます。
timeout は何分にすればよいですか?
そのワークフローの通常の所要時間の数倍〜10倍が目安です。3分で終わる run なら 30m、20分かかるビルドなら 1h 程度。短すぎると正常な順番待ちを「未完了」と誤判定し、長すぎると障害時の復旧が遅れます。
順番待ちとゴーストの見分けが付きません
gh run view <run-id> --json status,createdAt,updatedAt を数分おきに2回打ってください。updatedAt が動いていれば順番待ち、createdAt と同じまま動かなければゴーストです。あわせて gh run list --limit 6 で直前の run が正常に success しているかを見ます。
gh run watch を使わずに完了を待つ方法はありますか?
gh run view --json status,conclusion を自分でポーリングすれば、待ち時間の上限や途中の分岐を自由に書けます。
for i in $(seq 1 60); do
S=$(gh run view "$RUN_ID" --repo "$REPO" --json status,conclusion --jq '.status + "/" + .conclusion')
echo "$S"
case "$S" in completed/*) break ;; esac
sleep 30
done この形なら 60 回(30分)で必ず抜けるため、timeout を付け忘れる事故がありません。手元の gh の版で --json に使えるフィールド名は gh run view --help で確認してください。
