DevOps

Renovateの緊急PRだけがCIで落ちる

  • pnpm
  • Renovate
  • Security
  • GitHub Actions

pnpm 11 は、公開から 24 時間未満のバージョンを既定で解決しません。Renovate は脆弱性修正の PR を検出と同時に作るようになっています。

この 2 つが組み合わさると、緊急の PR だけが CI で落ちます。当初は「Renovate 側でも 1 日待たせる」つもりでしたが、先行事例と一次情報を調べた結果、変えるのは pnpm-workspace.yaml の側になりそうです。

# pnpm-workspace.yaml
minimumReleaseAge: 1440

緊急 PR だけ CI が常に落ちていた

hono の ReDoS 修正(GHSA-8j4g-w8fx-2239)に対して Renovate が [security] 付きの PR を作りました。変更は pnpm-lock.yaml の 1 ファイルだけです。その PR の CI が、次のエラーで落ちました。

[ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION] 1 lockfile entries failed verification:
  hono@4.12.34 was published at 2026-08-03T02:30:36.000Z, within the minimumReleaseAge cutoff

修正版の公開が 8 月 3 日 02:30 UTC で、PR の CI が走ったのが 23:13 UTC です。24 時間に 3 時間ほど足りませんでした。翌日 04:18 UTC に CI を再実行すると通り、そのまま自動マージされています。

hono@4.12.34 は 8 月 3 日 02:30 UTC に公開され、pnpm は翌 8 月 4 日 02:30 UTC までこのバージョンを拒否する。PR の CI は 8 月 3 日 23:13 UTC に走り、拒否される期間の中だったので落ちた。8 月 4 日 04:18 UTC の再実行は期間の外なので通った

コードには何の問題もなく、待てば通ります。ただ「緊急の PR は毎回落ち、人が時刻を見て再実行する」という運用は、それ自体が穴です。落ちている PR を見慣れると、本当に壊れている PR を見分けられなくなります。

なぜ落ちるのか

両側の既定値を並べると、落ちるのは仕様です。

pnpm の minimumReleaseAge は v11 から既定が 1440 分です。「In most cases, malicious releases are discovered and removed from the registry within an hour.」という理由で、新しいバージョンの解決を遅らせます。v11.1.3 からは、ロックファイルを読み込んだ直後、tarball を取りに行く前に全エントリを再検証するようになりました。--frozen-lockfilepnpm fetch も対象です(PR #11583)。ロックファイルの解決は Renovate 側で済んでいても、CI の pnpm install が改めて公開時刻を見て弾きます。

Renovate の vulnerabilityAlerts の既定は、ソースの lib/config/options/index.ts を読むと minimumReleaseAge: nullprCreation: 'immediate' です。通常の更新には config:best-practices 経由で npm に 3 日の待機(security:minimumReleaseAgeNpm)が入りますが、脆弱性修正だけはその対象外で、検出したら即座に PR になります。この「npm にだけ 3 日」は、GitHub Actions の Digest 更新を automerge から外したときに確かめた設定と同じものです。

ブログRenovate の Auto-merge 対象から Digest を外したGitHub Actions の commit SHA Pin は、その SHA を書き換える Renovate の PR を人が見なければ守りになりません。SHA Pin とフル semver のコメントが揃っていれば Digest 更新が単独で来るのはリリース済みのタグが動いたときだけなので、ラビーの共有 Preset の `automerge` 対象から Digest を外しました。`minimumReleaseAge` では止められない理由、Pin のコメントの揃え方、先に試した例外ルール、タグ Ruleset と `sha_pinning_required` も記録します。

つまり、通常の更新は Renovate が 3 日待ってから PR を作るので pnpm の 24 時間に引っかからず、緊急の PR だけが「Renovate は待たない、pnpm は待つ」という組み合わせになります。

リポジトリの packageManager に pnpm 11 を書いていて、CI で pnpm install が走るなら、どのリポジトリでも同じように落ちます。

他ではどうしているかを調べた

最初に考えたのは、共有の Renovate 設定に vulnerabilityAlerts: { minimumReleaseAge: "1 day" } を足すことでした。pnpm が 24 時間止める以上、前倒しで PR を作っても実利がないから失うものはない、という理屈です。

手を付ける前に、同じ問題を扱った記事と Issue を集めました。

  • pnpm のブログに載った Seattle Times の運用は、Shai-Hulud(2025 年 11 月、796 パッケージ、検知から削除まで約 12 時間)を待機期間で防ぎつつ、緊急パッチは除外リストにバージョンを追加して通しています。
  • Christian Schneider 氏の記事は「Cooldowns should apply to routine updates, not emergency security patches.」と書き、未知の更新は既定で遅らせ、既知のセキュリティ修正は意図して早く通す、と整理しています。
  • Andrew Nesbitt 氏の記事は、William Woodruff 氏が調べた 10 件の攻撃のうち 8 件で悪用できる期間が 1 週間未満だったという集計を引いたうえで、Bundler には CVE の修正をすぐ入れるための --cooldown 0 という逃げ道がある、と挙げています。
  • Safeguard の記事は、chalk と debug の乗っ取り(2025 年 9 月)が約 2 時間で削除された一方、例外のない待機期間は公開済みの脆弱性に対する修正も同じ日数だけ止めてしまう、と指摘しています。

共通しているのは「待機は通常更新に、緊急修正には例外経路を」です。脆弱性修正を Renovate 側でも 1 日遅らせる案は、公開済みの脆弱性に対して自分から修正を遅らせる選択で、この線とは逆でした。

Renovate は例外経路を持つ

例外経路は、Renovate に実装済みでした。Renovate 42.89.0(2026 年 1 月)から、脆弱性修正の PR では pnpm-workspace.yamlminimumReleaseAgeExcludehono@4.12.34 の形で修正版を自動追記します(PR #40020)。8 月の PR にそれが起きなかった理由を、lib/modules/manager/npm/artifacts.ts で確かめました。

if (!doc.get('minimumReleaseAge')) {
  return null;
}

執筆時点での分析では、追記が走るのは、pnpm-workspace.yamlminimumReleaseAge キーが書かれているときだけでした。 pnpm 11 の組み込み既定に頼っていると、Renovate から見れば「待機期間は設定されていない」ので、何もしません。

通常の更新は Renovate が 3 日待ってから PR を作るので pnpm の 24 時間の検証を通る。脆弱性修正は Renovate が即座に PR を作るので、修正版の公開から 24 時間以内に検出されると pnpm 側の検証に引っかかる。pnpm-workspace.yaml に minimumReleaseAge が明示されていれば、Renovate が minimumReleaseAgeExclude に修正版を追記するので検証を通る

pnpm 側から見ても、明示する方が筋が通ります。minimumReleaseAgeStrictminimumReleaseAge を明示したときだけ既定で true になり、組み込みの 1440 分は後方互換のために非 strict です。非 strict では、範囲内に待機期間を過ぎたバージョンがないとき pnpm が待機期間を無視して解決してしまいます。明示して strict にすれば、CI では ERR_PNPM_NO_MATURE_MATCHING_VERSION で止まり、例外は minimumReleaseAgeExclude にバージョン指定で書く、という 1 本の経路に揃います。

今後の対応方法

対処は次の 2 つにする予定です。

  1. 各リポジトリの pnpm-workspace.yamlminimumReleaseAge: 1440 を明示します。値は既定と同じですが、Renovate がこのキーを読めるようになり、pnpm 側も minimumReleaseAgeStrict が既定で true に変わります。
  2. Renovate の vulnerabilityAlerts には手を入れません。脆弱性修正は即座に PR を作らせ、Renovate が同じ PR で minimumReleaseAgeExclude に修正版を書く経路に任せます。

1 を入れたあとの脆弱性修正 PR では、pnpm-workspace.yaml の差分がこの形になるはずです。

# pnpm-workspace.yaml
minimumReleaseAge: 1440
minimumReleaseAgeExclude:
  - hono@4.12.34

minimumReleaseAgeExcludepnpm audit --fix も同じ場所に書くので、Renovate を経由しない緊急修正も同じファイルの同じキーに集まります。

とはいえ、この対応が正しいか?と言われると甚だ怪しいかと思います。

Renovate 側に残っている Issue

自動追記が必要以上に広く動く不具合が Issue #42190 として残っています。修正版が既に十分古くても minimumReleaseAgeExclude に追記されるので、除外リストに古いエントリが溜まります。

溜まったエントリを消す対応は別途起票されていますが、まだ入っていないようです。

除外リストは pnpm 側で pkg@version の完全一致なので、古いエントリが残っていても新しい脆弱なバージョンを通すことはありませんが、溜まった分は定期的に手作業で消すことになります。

Renovate が pnpm の minimumReleaseAge を読んで自分の待機期間に反映する機能は、Issue #41661 として open のままなので、今は両側に同じ値を書くしかなさそうです。