CI/CD
PR

セルフホストランナーがIdleなのにジョブを拾わない原因と対処|Waiting for a runner to pick up this job

saratogax
記事内に商品プロモーションを含む場合があります

セルフホストランナーで動かしている GitHub Actions のジョブが、Waiting for a runner to pick up this job... のまま進まなくなっていませんか?

設定画面でランナーを見ると、緑の Idle(待機中)と表示されています。

ランナーは空いているのに、ジョブだけが拾われない状態です。

ラベルもランナーグループも正しく、ランナーのログにもエラーは出ません。

原因は、GitHub が 2026-09-29 から全面適用したセルフホストランナーの最低バージョンの強制でした。

私の組織では、自動更新を有効にしていたランナーが 2.328.0 で止まっていました。

この記事では、バージョンの確認方法と手動での更新手順、止まる前に期限を検知する REST API の使い方、起動時に自分で更新する仕組みをまとめます。

Idle のまま「Waiting for a runner to pick up this job」で止まる症状

私の環境で起きていたのは、次の状態です。

  • ジョブが Queued のまま、Waiting for a runner to pick up this job... から進まない
  • 組織の Settings → Actions → Runners では、対象のランナーは Idle(緑)
  • ジョブの runs-on のラベルも、ランナーグループの設定も正しい
  • ランナーのサービスは動いていて、_diag のログにもバージョンが古いことを示すメッセージは見当たらない

このエラー文で検索すると、ラベルの不一致やランナーのオフラインといった原因がたくさん出てきます。

ところが今回は、そのどれにも当てはまりませんでした。

画面上は正常に見えるので、設定ミスを疑って時間を使ってしまいやすいのが厄介なところです。

ラベルとオンライン状態を確かめても原因が見つからないなら、次に疑うのはランナーのバージョンです。

原因: 2026-09-29 から始まった最低バージョンの強制

GitHub は 2026-06-12 に、セルフホストランナーのバージョン要件を強制する計画を告知しました。

開始日は一度動いていて、github.com(GitHub Enterprise Cloud)では当初の 9 月 25 日から 2026-09-29(火)に変わったうえで全面適用されています。

  • GitHub Enterprise Cloud: 2026-09-29 から全面適用
  • データレジデンシー版の GitHub Enterprise Cloud: 2026-07-31 から全面適用
  • GitHub Enterprise Server: 告知の対象外

登録の条件と、ジョブを実行する条件は別

注意したいのは、「登録できる最低バージョン」と「ジョブを実行できる最低バージョン」が別だという点です。

対象条件満たさないと
登録・再登録2.329.0 以上ランナーを登録できない
ジョブの実行新しいリリースを公開から 30 日以内に入れ続けるジョブを拾わない
※表は横スクロールできます

告知には、次の一文があります。

A runner pinned to 2.329.0 that never updates again will not pick up jobs.

登録の条件を満たしていても、更新を止めたランナーはいずれジョブを拾わなくなるということです。

告知に書かれているのは「ジョブがキューに残ったままになるか、失敗する」までで、画面の表示がどうなるかは書かれていません。

私の環境では、それが「Idle のまま待たされる」という見た目で現れました。

ランナーのバージョンを確認する

ランナーのバージョンは、ランナーのディレクトリで本体のコマンドを実行すると確認できます。

cd ~/actions-runner
./bin/Runner.Listener --version
# 2.328.0

sudo ./svc.sh status でも、起動した直後ならログの中に Current runner version の行が見えます。

ただし、表示されるのは直近のログだけなので、起動から時間がたつと出てきません。

私のランナーは 2.328.0 で、登録の最低バージョンの 2.329.0 にすら届いていませんでした。

その時点で最新だった 2.337.0 へ手動で上げると、ログに Listening for Jobs が出た3 秒後に、待たされていたジョブを拾いました。

設定は何も変えていないので、原因はバージョンだったと判断できます。

自動更新が有効なのに、なぜ古いままだったのか

告知では、自動更新が有効なランナーは「更新サービスに届く限り」30 日の条件を自動で満たすとされています。

私のランナーも、自動更新を無効にはしていませんでした(--disableupdate は付けていない)。

ところが、ランナーのディレクトリに残っている更新の記録を見ると、最後の自動更新は 2025-10-05(2.320.0 → 2.328.0)でした。

2.328.0 を最後に、1 年近く自動更新されていなかったことになります。

次のリリースの 2.329.0 は 2025-10-14 の公開で、告知では「新しい基盤がランナーを認識して接続を許すための最低バージョン」と説明されています。

そのため最初は、この基盤の切り替えのところで、自動更新の連鎖が途切れたのではないかと考えていました。

止まっていたのは、夜間に停止しているランナーだった

ところが、手動で上げたあとも様子を見ていると、もっとはっきりした傾向が見えてきました。

止まっていたのは、日中だけ EC2 を起動しているランナーです。

毎日 9 時ごろに起動して 20 時ごろに止め、夜間は停止しています。

一方、同じ組織で常時起動しているランナーは、その後も自動更新されていました。

常時起動しているランナーで、自動更新でできる bin.<バージョン> ディレクトリの作成日時を見ると次のとおりです。

  • 2.337.0: 2026-09-03 01:39(日本時間)
  • 2.338.0: 2026-10-24 01:39(日本時間)

どちらも、深夜 1 時 39 分ちょうどです。

このことから、GitHub は決まった時刻に、そのとき接続しているランナーへ一斉に更新を配っているように見えます。

その時刻に止まっているランナーは更新を受け取れず、あとでジョブを実行しても、更新は始まりませんでした。

手動で上げる前に、ランナーを起動して 8〜11 分待ってみても同じでした。

日中だけ動かしているランナーは、この時刻にはいつも止まっています。

また、配られたのは公開から 8〜18 日後で、公開されてすぐではありませんでした。

このランナーは 2.328.0 までは同じ止め方でも自動更新されていたので、2.329.0 の基盤の切り替えで、更新の配り方も変わったのかもしれません。

ただし、どちらも観察からの推測で、GitHub の公式な説明は見つけられていません。

「自動更新を有効にしているから大丈夫」とは言い切れないので、設定ではなく実際に動いているバージョンを確認してください。

対処: ランナーを手動で更新する

Linux(x64)のランナーを、systemd のサービスとして動かしている場合の手順です。

バージョンとアーキテクチャは、actions/runner のリリースページで最新のものに読み替えてください。

cd ~/actions-runner

# 1. サービスを止める
sudo ./svc.sh stop

# 2. 登録情報をバックアップする(.runner_migrated は無い環境もある)
mkdir -p ~/runner-backup
cp -p .runner .credentials* .env .path .service ~/runner-backup/
cp -p .runner_migrated ~/runner-backup/ 2>/dev/null || true

# 3. 新しいバージョンを取得し、リリースページに載っている SHA-256 と照合する
VERSION=2.337.0
curl -o actions-runner-linux-x64-${VERSION}.tar.gz -L \
  https://github.com/actions/runner/releases/download/v${VERSION}/actions-runner-linux-x64-${VERSION}.tar.gz
echo "<リリースページの SHA-256>  actions-runner-linux-x64-${VERSION}.tar.gz" | sha256sum -c

# 4. 既存のディレクトリに上書きで展開する
tar xzf ./actions-runner-linux-x64-${VERSION}.tar.gz

# 5. サービス用のスクリプトに差分があれば、新しいバージョンで置き換える
diff runsvc.sh bin/runsvc.sh || cp -p bin/runsvc.sh runsvc.sh

# 6. サービスを起動する
sudo ./svc.sh start

手順の中で気を付けたい点は 3 つです。

  • 再登録は不要でした。登録情報(.runner や .credentials)は展開しても上書きされません。バックアップは万一に備えたものです
  • 私のランナーでは、bin と externals が bin.2.328.0 のような古いバージョンのディレクトリへのシンボリックリンクになっていました。GNU tar で展開すると、リンクが普通のディレクトリに置き換わります。bin.2.328.0 などは残るので、切り戻しにも使えます
  • サービスが実際に呼ぶ runsvc.sh は、展開しても更新されません。2.337.0 では差分はありませんでしたが、2.338.0 で中身が変わりました(ランナー内部の処理に Node.js 24 を使う変更)。差分があれば、bin/runsvc.sh で置き換えてください

起動後に ./bin/Runner.Listener --version が新しいバージョンを返し、sudo ./svc.sh status のログに Listening for Jobs が出ていれば完了です。

再発を防ぐには: Online の監視では気付けない

今回の状態は、ランナーが Online かどうかの監視では検知できません。

画面上は Idle で、REST API でも status は online なので、死活監視は正常と判断します。

しかも「30 日以内に更新」のルールがあるので、一度直しても、新しいリリースが出るたびに同じことが起きえます。

ただし、実際の期限は「公開から 30 日」ちょうどになるとは限りません。

たとえば 2.338.0 は 2026-10-06 に公開されましたが、2.337.0 に付いた期限は 2026-12-25 で、公開から 80 日後でした。

期限は日数から推測せず、GitHub が返す値で確かめるのが確実です。

REST API でバージョンと期限を確認する

GitHub には、ランナーのバージョンごとの期限を返す REST API があります。

まず、組織のランナーの一覧から、各ランナーのバージョンを取ります。

gh api --paginate /orgs/ORG/actions/runners \
  --jq '.runners[] | [.name, .status, .version] | @tsv'

次に、そのバージョンの期限を問い合わせます。

gh api /orgs/ORG/actions/runners/deprecations/2.337.0

リポジトリ単位で登録したランナーなら、/repos/OWNER/REPO/actions/runners/deprecations/VERSION で同じ情報を取れます。

返ってくる項目は次の 3 つです。

  • runner_version — 問い合わせたバージョン
  • registration_deprecates_at — このバージョンで登録できなくなる日時(未定なら null)
  • runtime_deprecates_at — このバージョンでジョブを実行できなくなる日時(未定なら null)

runtime_deprecates_at を監視すれば、止まる前に気付けます。

私は、毎日 GitHub ホストのランナー(ubuntu-latest)でこの 2 つの API を呼び、期限が 14 日以内に迫ったら Slack に通知する workflow を作りました。

監視対象のセルフホストランナーが止まっていても動くように、監視自体は GitHub ホストのランナーで動かすのがポイントです。

なお、新しいバージョンが出てしばらくは、1 つ前のバージョンの runtime_deprecates_at が null のままのことがあります。

私の環境では、2.338.0 の公開から 2 週間以上たっても 2.337.0 は null のままで、その後に 2026-12-25 という期限が付きました。

null を「期限なし」と扱わず、新しいリリースの公開日から 30 日を数えるチェックも併せておくと安心です。

GitHub App で呼ぶときの権限

この 2 つの API には、組織の管理者相当の権限が要ります(PAT(classic)なら admin:org スコープ)。

workflow から GitHub App のトークンで呼ぶ場合は、組織の権限の Self-hosted runners を Read(読み取り)だけ付ければ呼べます。

Write(書き込み)を付けるとランナーの登録用トークンも発行できてしまうので、読み取りに留めておくのが安全です。

actions/create-github-app-token を使うなら、発行するトークンの権限も絞れます。

- name: Generate App token
  id: app-token
  uses: actions/create-github-app-token@v3
  with:
    client-id: ${{ vars.APP_CLIENT_ID }}
    private-key: ${{ secrets.APP_PRIVATE_KEY }}
    owner: ${{ github.repository_owner }}
    permission-organization-self-hosted-runners: read

PAT から GitHub App トークンへ切り替える手順は、PAT を GitHub App トークンに置き換える手順と 3 つの落とし穴もあわせてどうぞ。

決まった時間だけ起動するランナーは、起動時に自分で更新する

コスト削減のために日中だけ EC2 を起動しているランナーは、更新が配られる時刻に止まっているので、自動更新を受け取れません。

上の API で期限には気付けますが、そのたびに手で上げるのは手間です。

そこで、起動時に自分で更新する仕組みを入れました。

仕組みは単純で、ランナーのサービスが起動する前に、更新スクリプトを動かします。

systemd の drop-in(既存のユニットに設定を足すファイル)で、ExecStartPre を追加します。

# /etc/systemd/system/actions.runner.<組織名>.<ランナー名>.service.d/update-runner.conf
[Unit]
Wants=network-online.target
After=network-online.target

[Service]
ExecStartPre=-/usr/local/sbin/update-runner.sh
TimeoutStartSec=600
Environment=MIN_RELEASE_AGE_DAYS=14

設定の中身は次の 3 つです。

  • ExecStartPre の先頭の - は、失敗しても起動を続ける指定です。更新に失敗しても、今のバージョンのままランナーは起動します
  • svc.sh install が作るユニットは After=network.target だけなので、ネットワークの準備ができるまで待つ指定を足しています
  • ダウンロード(約 230MB)と展開に時間がかかるので、起動のタイムアウトを既定の 90 秒から 10 分に延ばしています

drop-in のディレクトリ名は、サービス名と完全に一致させる必要があります。

サービス名は svc.sh install が actions.runner.<組織名>.<ランナー名>.service の形で付けていて、~/actions-runner/.service にも書かれています。

名前がずれていると、systemd はエラーも出さずに drop-in を無視するので注意してください。

ファイルを置いたら sudo systemctl daemon-reload を実行し、systemctl cat でサービスを表示すると、drop-in が読み込まれたかを確かめられます。

スクリプト本体は、ジョブを実行するユーザーが書き換えられないように、root の所有で /usr/local/sbin に置きました。

更新スクリプトがやること

スクリプトは、次の順に動きます。

  • ./bin/Runner.Listener --version で、今のバージョンを調べる
  • REST API で、actions/runner のリリース一覧を取る(認証なし)
  • プレリリースを除き、公開から 14 日以上たったリリースのうち、最新のものを更新先にする
  • 今のバージョンが更新先と同じなら、何もせずに終わる
  • 新しいバージョンを取得し、リリースの asset に付いている digest(SHA-256)と照合する
  • 空き容量を確かめてから上書きで展開し、runsvc.sh に差分があれば置き換える

どこで失敗しても、スクリプトは正常終了して、今のバージョンのままランナーを起動させます。

また、1 時間以内に確かめたばかりなら、何もせずに終わるようにしました。

サービスの再起動が続いたときに、認証なしの API の上限(1 時間に 60 回)を使い切らないためです。

更新先を選ぶ部分は、次のように jq で書いています。

cutoff=$(( $(date -u +%s) - 14 * 86400 ))

curl -fsSL -H 'Accept: application/vnd.github+json' \
  'https://api.github.com/repos/actions/runner/releases?per_page=30' |
  jq -r --argjson cutoff "$cutoff" '
    [ .[]
      | select(.draft == false and .prerelease == false)
      | select((.published_at | fromdateiso8601) <= $cutoff) ]
    | sort_by(.tag_name | ltrimstr("v") | split(".") | map(tonumber))
    | last
    | .tag_name'

最新バージョンにすぐ追従せず 14 日待つのは、常時起動のランナーと同じバージョンに揃えるためです。

自動更新が配られたのは公開から 8〜18 日後だったので、14 日待つと、おおむね同じバージョンになります。

実際に動かした結果

設定を入れた翌朝、ランナーの起動時のログには次の行が出ていました。

$ journalctl -u actions.runner.<組織名>.<ランナー名>.service -b | grep update-runner
update-runner: current 2.337.0, target 2.338.0 (published 14 days ago or earlier)
update-runner: downloading https://github.com/actions/runner/releases/download/v2.338.0/actions-runner-linux-x64-2.338.0.tar.gz
update-runner: runsvc.sh was replaced with bin/runsvc.sh
update-runner: updated to 2.338.0

取得から展開まで 13 秒ほどで終わり、そのままジョブを拾える状態になりました。

止める時間帯をずらして、更新が配られる時刻に起動しておく方法も考えました。

ですが、配られる時刻が決まっているという公式の説明はないので、この方法は選びませんでした。

まとめ

  • セルフホストランナーが Idle なのに Waiting for a runner to pick up this job... のままなら、まずランナーのバージョンを疑う
  • GitHub Enterprise Cloud では 2026-09-29 から、登録は 2.329.0 以上、ジョブの実行は新リリースを 30 日以内に入れ続けることが強制されている
  • 自動更新を有効にしていても、2.328.0 で更新が止まっていた実例がある。設定ではなく、実際のバージョンを確認する
  • 自動更新は決まった時刻に一斉に配られているように見え、その時刻に止まっているランナーは受け取れない
  • 手動更新は、サービスを止めて上書きで展開するだけで、再登録は不要だった。2.338.0 からは runsvc.sh の置き換えも必要
  • Online の監視では気付けない。/orgs/{org}/actions/runners/deprecations/{version} の runtime_deprecates_at で期限を監視する。期限は「公開から 30 日」とは限らない
  • 決まった時間だけ起動するランナーは、systemd の ExecStartPre で起動時に自分で更新させる

セルフホストランナーは、一度動き始めると存在を忘れがちです。

ですが、強制が始まった以上は「放置すると 30 日で止まりうるもの」として扱う必要があります。

同じ症状で困っている方は、まず ./bin/Runner.Listener --version でバージョンを確認してみてください。

参考(出典)

ABOUT ME
saratoga
saratoga
フリーランスエンジニア
仕事にも趣味にも IT を駆使するフリーランスエンジニア。技術的な TIPS や日々の生活の中で深堀りしてみたくなったことを備忘録として残していきます。
記事URLをコピーしました