目的
Dependency Unblock Check の full モードが、probe 実行後の GitHub API 呼び出しで fetch failed になり、
MECHANISM: Unexpected evaluator error: fetch failed(exit 1)で失敗する不具合を直す。
あわせて、同種の失敗が起きたときに原因(error.cause)をログから特定できるようにする。
失敗した run
| run ID |
日時 (UTC) |
Node.js |
結果 |
| 34812644584 |
2026-09-14 06:14 |
24.20.0 |
success |
| 36388464626 |
2026-09-28 06:50 |
24.21.0 |
fetch failed |
| 37275307924 |
2026-10-05 07:00 |
24.21.0 |
fetch failed |
- いずれも
schedule(main)、step Run dependency unblock check
- 9/14 と 9/28 は同一コミット(1ae885f)。差分はランナーイメージ(20260907.300 → 20260920.314)に伴う Node.js 24.20.0 → 24.21.0 のみ
- 2026-09-21(run 35567855585)の失敗は
UNBLOCKED: jsdom@30.1.0(exit 10)で、本不具合とは別(設計どおりの赤)
根本原因
runFull は runProbes で spawnSync を使うため、probe 中(約 90 秒)イベントループが止まる
- probe 前の
getIssue / listLabeledOpenIssues で使った HTTP keep-alive の接続が、その間にサーバー側で閉じられる
- Node.js 24.21.0 同梱の undici 7.29.1 は idle socket の検証を ref'd
setImmediate に遅延する変更
(nodejs/undici#5769、#5707 の backport)を含み、
イベントループ再開直後の同期的な再利用では切断を検知できず、閉じた socket に書き込む
- 失敗したのは probe 後の
postUnblockComments → listIssueCommentBodies(GET /repos/{owner}/{repo}/issues/{n}/comments)
main() の catch が想定外の例外を Unexpected evaluator error: ${error.message} に丸め、error.cause を捨てているため、ログには fetch failed しか出ていなかった
ローカル再現
fetch(GET)→ spawnSync("sleep", [N]) → fetch(GET)の順で実行した結果。
| Node.js |
待ち時間 |
2 回目の fetch |
| 24.21.0 |
90 秒 |
EPIPE |
| 24.21.0 |
30 秒 |
UND_ERR_SOCKET |
| 24.21.0 |
10 秒 |
成功 |
| 24.20.0 / 24.18.0 |
90 秒 |
成功 |
- 24.21.0 でも、2 回目の前に
setImmediate / setTimeout(0) を 1 回待つ、または非同期 spawn にすると成功する
- 1 回目の失敗後に同じ GET を再試行すると、新しい接続で成功する
影響範囲
対象
scripts/ci/dependabot-unblock-check.mjs
createGitHubClient の request: GET に限り、TypeError: fetch failed かつ error.cause.code が
UND_ERR_SOCKET / EPIPE / ECONNRESET のとき 1 回だけ再試行する
main() の catch: Unexpected evaluator error のメッセージに error.cause の code / message を含める
scripts/ci/dependabot-unblock-check.test.mjs: 回帰テストを追加する
docs/operations/dependency-unblock-check.md: 「赤(MECHANISM)の理由と対処」表への行追加と、再試行の仕様の追記
受け入れ条件
- GET が上記 cause の
fetch failed で 1 回失敗しても、再試行で成功すれば full モードが正常に判定を返す
- 再試行は 1 回だけで、2 回目も失敗した場合や対象外の cause・HTTP エラー(
!response.ok)は再試行せず MECHANISM になる
- POST(
createIssueComment)は再試行しない(コメントの重複投稿を避ける)
- MECHANISM の Summary /
::error:: に cause の code と message が出る(例: fetch failed (cause: EPIPE write EPIPE))。トークン値は含まない
dependabot-unblock-check.test.mjs に、注入した fetchImpl で次を確かめるテストがある
- 対象 cause で 1 回失敗 → 再試行で成功する
- 2 回連続で失敗 → 例外になる(呼び出しは 2 回)
- 対象外の cause と POST は再試行しない
- 例外メッセージに cause が含まれる
node --test scripts/ci/dependabot-unblock-check.test.mjs と npm run lint:md が通る
- Node.js 24.21.0 で「GET →
spawnSync で 90 秒待機 → 修正後クライアントで GET」の再現スクリプトが成功する(PR に結果を記録)
docs/operations/dependency-unblock-check.md の「赤(MECHANISM)の理由と対処」表に Unexpected evaluator error: ... の行(意味と対処)があり、再試行の仕様が書かれている
- マージ後、ADR-0008 の「リリース手順(タグ運用)」に従い
v1.7.2(patch)を作成し、v1 タグを付け替える(force push を伴うため、実行前にユーザー確認を取る)
補足
- 修正方針は「GET の 1 回再試行」を採る。次の 2 案は採らない
- probe 後に
setImmediate を 1 回待つ案: 再現では有効だが undici 内部の検証タイミングに依存し、将来の undici で再び壊れうる。注入した fetchImpl での単体テストもできない
- probe を非同期
spawn にする案: 根本的だが createSpawnRunner / runProbes とテストの変更が大きく、今回の不具合に対して過剰
- 再試行案は undici の実装に依存せず、keep-alive 接続の切断全般(NAT の idle timeout 等)にも効く。既存の
fetchImpl 注入点でテストできる
- 再試行を GET に限るのは、GET が冪等なため(RFC 9110 の idempotent method)。今回失敗したのも probe 後最初の GET で、再試行で新しい接続に切り替われば直後の POST は成功する
- ticket-c2c-platform は
@v1.7.1 に固定しているため、v1.7.2 を参照させる caller の更新は ticket-c2c-platform 側の Issue / PR(または Dependabot の github-actions 更新 PR)で別途行う
- 「定期チェックの連続失敗に気づけない」(GitHub Actions の失敗通知メール以外に仕組みがない)問題は本 Issue の対象外とし、別途検討する
目的
Dependency Unblock Check の
fullモードが、probe 実行後の GitHub API 呼び出しでfetch failedになり、MECHANISM: Unexpected evaluator error: fetch failed(exit 1)で失敗する不具合を直す。あわせて、同種の失敗が起きたときに原因(
error.cause)をログから特定できるようにする。失敗した run
schedule(main)、stepRun dependency unblock checkUNBLOCKED: jsdom@30.1.0(exit 10)で、本不具合とは別(設計どおりの赤)根本原因
runFullはrunProbesでspawnSyncを使うため、probe 中(約 90 秒)イベントループが止まるgetIssue/listLabeledOpenIssuesで使った HTTP keep-alive の接続が、その間にサーバー側で閉じられるsetImmediateに遅延する変更(nodejs/undici#5769、#5707 の backport)を含み、
イベントループ再開直後の同期的な再利用では切断を検知できず、閉じた socket に書き込む
postUnblockComments→listIssueCommentBodies(GET /repos/{owner}/{repo}/issues/{n}/comments)main()の catch が想定外の例外をUnexpected evaluator error: ${error.message}に丸め、error.causeを捨てているため、ログにはfetch failedしか出ていなかったローカル再現
fetch(GET)→
spawnSync("sleep", [N])→ fetch(GET)の順で実行した結果。EPIPEUND_ERR_SOCKETsetImmediate/setTimeout(0)を 1 回待つ、または非同期spawnにすると成功する影響範囲
still blocked では probe 後に API を呼ばない。2026-10-12 の
scheduleは緑(OK: still blocked)の見込みworkflow_callの呼び出し側(IDP_UNBLOCK_REMOVAL_DIRが空)が UNBLOCKED になったとき、追跡 Issue へのコメント投稿経路で必ず失敗する。リリース済みの
v1.7.1(v1タグ = 8c179a4)も同じコードを含む@v1.7.1を参照、現状 still blocked で緑)対象
scripts/ci/dependabot-unblock-check.mjscreateGitHubClientのrequest: GET に限り、TypeError: fetch failedかつerror.cause.codeがUND_ERR_SOCKET/EPIPE/ECONNRESETのとき 1 回だけ再試行するmain()の catch:Unexpected evaluator errorのメッセージにerror.causeの code / message を含めるscripts/ci/dependabot-unblock-check.test.mjs: 回帰テストを追加するdocs/operations/dependency-unblock-check.md: 「赤(MECHANISM)の理由と対処」表への行追加と、再試行の仕様の追記受け入れ条件
fetch failedで 1 回失敗しても、再試行で成功すればfullモードが正常に判定を返す!response.ok)は再試行せず MECHANISM になるcreateIssueComment)は再試行しない(コメントの重複投稿を避ける)::error::に cause の code と message が出る(例:fetch failed (cause: EPIPE write EPIPE))。トークン値は含まないdependabot-unblock-check.test.mjsに、注入したfetchImplで次を確かめるテストがあるnode --test scripts/ci/dependabot-unblock-check.test.mjsとnpm run lint:mdが通るspawnSyncで 90 秒待機 → 修正後クライアントで GET」の再現スクリプトが成功する(PR に結果を記録)docs/operations/dependency-unblock-check.mdの「赤(MECHANISM)の理由と対処」表にUnexpected evaluator error: ...の行(意味と対処)があり、再試行の仕様が書かれているv1.7.2(patch)を作成し、v1タグを付け替える(force push を伴うため、実行前にユーザー確認を取る)補足
setImmediateを 1 回待つ案: 再現では有効だが undici 内部の検証タイミングに依存し、将来の undici で再び壊れうる。注入したfetchImplでの単体テストもできないspawnにする案: 根本的だがcreateSpawnRunner/runProbesとテストの変更が大きく、今回の不具合に対して過剰fetchImpl注入点でテストできる@v1.7.1に固定しているため、v1.7.2を参照させる caller の更新は ticket-c2c-platform 側の Issue / PR(または Dependabot の github-actions 更新 PR)で別途行う