「そういえば最近、RenovateのPRを見ていない」。そう気づいたのは、最後のPRから3週間後だった。エラーの通知は1通も来ていない。
調べると、依存更新のジョブはメモリ不足(OOM)で強制終了され、次のジョブは1週間キューに積まれたまま動いていなかった。落ちたなら落ちたと言ってほしい。現実は、止まったこと自体がrepoのどこにも残っていない。
この記事は、Mendがホストする無料版のRenovate(Community Cloud)を、schedule で週1回に絞って使っている人向けだ。
Community Cloudは無料で、ジョブ1本あたり1 vCPU・メモリ3GB・30分まで、通常は4時間ごとに実行される(公式のOverview)。見張りに使うGitHub Actionsは、公開repoなら無料、非公開repoはFreeプランで月2,000分の枠から引かれる(料金)。週1回の短いジョブなので、枠はほぼ減らない見込みだ。
1回の失敗で週が丸ごと飛ぶ理由
原因は3つの仕様の組み合わせにある。
1つ目は、schedule で決める時間帯(以下、窓)が「実行する時刻」ではなく「ブランチを作ってよい時刻」だということ。書き方はcronなのに、いつ実行するかはMend側が決める。公式は窓を最低3〜4時間取るよう勧めている(scheduleの説明)。ただ、4時間ごとの実行なら4時間の窓は1回分の機会でしかない。
2つ目は、OOMで落ちたrepoが週1回の実行に格下げされること。kernel-out-of-memory になったrepoは週1回のスケジューラに回る(Job Scheduling)。
3つ目は、OOMがプロセスごと強制終了されること。落ちた更新のPRも、コメントも、Dashboardの更新も残らない。結果が見えるのはMendのDeveloper Portalのジョブ一覧だけだった(筆者の観察)。
筆者のrepo(pnpm 12のmonorepo、窓は月曜0〜6時)では、次の順で止まった。
| 日時(JST) | 起きたこと |
|---|---|
| 9/21 01:20 | 窓の中のジョブが kernel-out-of-memory。lockfileの更新中に3.0GBに達した |
| 9/27 09:30 | 次のジョブがキューに入り、そのまま1週間PENDING |
| 9/28 | 月曜の窓が空振り |
| 10/4 | 9/14以降、PRが0本だったと気づく |
1回のOOMと、その後動かなかったジョブで、月曜の窓が2回空振りした。最後のPR(9/13)からは3週間だ。PENDINGのまま動かなかった理由は、Developer Portalの表示からは分からなかった(筆者の観察)。
手順1: いま止まっていないか確かめる
あなたのrepoのRenovateは、いま本当に動いているだろうか。Developer Portal(https://developer.mend.io/)でジョブ一覧を開く。FAILED や、古いまま PENDING のジョブを探す。repoのディレクトリでは、Renovateの最新PRとDashboardの更新日時を gh で引ける。
gh pr list --author "app/renovate" --state all --limit 1 --json number,createdAt
gh issue list --author "app/renovate" --search "Dependency Dashboard in:title" --json number,updatedAt
セルフホストなら app/renovate を、動かしているアカウント名に替える。Dashboardの更新日時が新しくても、ジョブ成功の証拠にはならない(理由は手順4)。
手順2: メモリの使い方を絞る
OOMはメモリを絞れば減らせることがあるが、pnpmの版によっては効かない。
| 設定 | すること | 効く見込みがある環境 |
|---|---|---|
toolSettings.nodeMaxMemory(MiB) | NODE_OPTIONS のヒープ上限としてpnpmなどに渡す(ソース) | npm、yarn、Node実装のpnpm 11以下 |
env の PNPM_MAX_WORKERS | pnpmのワーカー数の上限(pnpm 11のソース) | Node実装のpnpm 11以下 |
公式のFAQは、OOMが続くなら nodeMaxMemory を1.5〜2.5GBにするよう勧めている(FAQ)。env に書けるのは許可された変数だけだ。Mendが許可する global:safeEnv の一覧に、PNPM_MAX_WORKERS はあるが NODE_OPTIONS は無い(環境変数の扱い)。3GBの上限そのものは、OSS向けプラン(6GB、申請制)かEnterprise(8GB、営業経由)に移らないと上がらない。
pnpm 12はRustで書かれたCLIになった(v12.0.0のリリースノート)。Nodeのヒープ上限は関わらず、ソースに PNPM_MAX_WORKERS を読む箇所も見当たらない。筆者のrepoはpnpm 12なので、どちらも効かない見込みだ。pnpm 11以下でも、1.0〜2.5GBの nodeMaxMemory や PNPM_MAX_WORKERS の1で落ちた報告がある(ディスカッション)。
手順3: 窓を広げる
窓を広げても、OOMの再発は防げない。落ちたrepoは週1回の実行に回るので、48時間の窓でも、4時間ごとの残りの機会は来ない。効くのは次の3つだ。
- Mendの実行時刻の揺れを吸収する
- OOM以外の失敗(状態が
failed)なら1日1回の実行に回るので、窓の前半で落ちればその再実行が窓に入る(前掲のJob Scheduling) - 格下げ後の週1回のジョブが窓に当たる確率が上がる
| schedule | 窓の長さ | 通常時に当たる回数 | OOM後の週1回のジョブが当たる確率 |
|---|---|---|---|
schedule:weekly(* 0-3 * * 1) | 4時間 | 1回前後 | 約2% |
* 0-6 * * 1 | 7時間 | 1〜2回 | 約4% |
* * * * 0,1(日・月) | 48時間 | 約12回 | 約29% |
回数は4時間ごとに走る前提、確率は時刻がばらばらとした計算だ。手順2と合わせると次のようになる。pnpm 12なら timezone と schedule だけでよい(メモリの2行は手順2の表の環境向け)。
{
"timezone": "Asia/Tokyo",
"schedule": ["* * * * 0,1"],
"toolSettings": { "nodeMaxMemory": 2048 },
"env": { "PNPM_MAX_WORKERS": "2" }
}
timezone が無いと窓はUTCで評価される。分の欄は * でなければならず、0 0-6 * * 1 は設定エラーになる。マージ前に、公式のバリデータをrepoのルートで動かしておく。
RENOVATE_ALLOWED_ENV='["PNPM_MAX_WORKERS"]' npx --yes --package renovate -- renovate-config-validator
RENOVATE_ALLOWED_ENV は、Mend側の許可を手元で真似るためのものだ。
手順4: 止まったらissueで知らせる
窓とメモリの設定では、OOMの再発も、PENDINGのまま動かないジョブも防げない。そこで、RenovateのPRが一定日数動いていなければissueを立てるworkflowを .github/workflows/ に置く。
name: Renovate watchdog
on:
schedule:
- cron: '17 0 * * 2' # 火曜 09:17 JST。毎時 0 分は混むので外す
workflow_dispatch:
permissions:
issues: write
pull-requests: read
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v9
with:
script: |
const BOT = 'renovate[bot]'; // セルフホストなら Renovate を動かすアカウント名
const STALE_DAYS = 3; // 静かな週がある repo は 10
const TITLE = 'Renovate の PR が止まっています';
const { owner, repo } = context.repo;
const { data: pulls } = await github.rest.pulls.list({
owner, repo, state: 'all', sort: 'created', direction: 'desc', per_page: 100,
});
// 新しい PR を作った日時と、開いている PR に push した日時を活動とみなす
const times = pulls
.filter((p) => p.user?.login === BOT)
.map((p) => Date.parse(p.state === 'open' ? p.updated_at : p.created_at));
const ageDays = times.length ? (Date.now() - Math.max(...times)) / 86400000 : Infinity;
core.info(`Renovate の最後の活動: ${ageDays.toFixed(1)} 日前`);
const issues = await github.paginate(github.rest.issues.listForRepo, {
owner, repo, state: 'open', per_page: 100,
});
const alert = issues.find((i) => i.user?.login === 'github-actions[bot]' && i.title === TITLE);
const dashboard = issues.find((i) => i.user?.login === BOT && i.title === 'Dependency Dashboard');
if (ageDays < STALE_DAYS) {
if (alert) {
await github.rest.issues.update({ owner, repo, issue_number: alert.number, state: 'closed' });
}
return;
}
const body = [
`Renovate の PR が ${Number.isFinite(ageDays) ? Math.floor(ageDays) : '?'} 日間動いていません。`,
`Dependency Dashboard: ${dashboard ? `#${dashboard.number}(更新 ${dashboard.updated_at})` : '見つかりません'}`,
'',
'1. Mend の Developer Portal でジョブ一覧を開き、FAILED(kernel-out-of-memory など)や PENDING のまま止まったジョブを探す',
'2. ジョブが成功していて、出す更新が無かっただけならこの issue を閉じる',
'3. 待っている更新を今すぐ出すなら、Dashboard の「Awaiting Schedule」の欄でチェックを入れる',
].join('\n');
if (alert) {
await github.rest.issues.createComment({ owner, repo, issue_number: alert.number, body });
} else {
await github.rest.issues.create({ owner, repo, title: TITLE, body });
}
core.warning(`Renovate inactive for ${ageDays.toFixed(1)} days`);
活動とみなすのは、新しいPRの作成と、開いているPRへのpushだ。Dashboardの更新日時は「動いている証拠」らしく見えるのに、実際は当てにならない。本文が変わらないと書き換わらず(ソース)、窓の外のジョブが成功していれば、窓の中で毎回落ちていても書き換わる見込みがある。
知らせるまでの日数(コードの STALE_DAYS)は、何回の空振りまで待つかで選ぶ。日・月の窓に対して火曜の朝に見る場合はこうなる。
| 知らせるまでの日数 | 知らせるタイミング | 向いているrepo |
|---|---|---|
| 3日 | 窓1回分、PRが動かなかった翌朝 | 毎週なにかしらPRが出る |
| 10日 | 2週続けて動かなかった翌朝 | 更新が出ない週がある |
手元では、この script を偽のGitHub APIで動かし、issueの作成、2回目はコメントだけ、PRが動いたら閉じる、を確かめた。actionlint も通る。入れたら手動で1回実行し、ログの日数を見ておく。
見張りが取りこぼすとき
| 状況 | 起きること | 対処 |
|---|---|---|
| 新しい版が出ない静かな週 | 止まっていないのに知らせる | ジョブが成功していればissueを閉じる |
同時に開けるPR数の上限(prConcurrentLimit)に達している | 新しいPRが作れず知らせる | PRをマージするか上限を上げる |
| 長く開いていたPRを直近でマージした | 閉じたPRは作成日で見るので、知らせる | ジョブが成功していればissueを閉じる |
| OOMになる更新とは別の更新で、PRの作成やpushがあった | 活動ありと見て黙る | Developer Portalでジョブの結果を見る |
updateNotScheduled が既定で有効なので、開いているPRへのpushは窓の外でも起きる(設定の説明)。コメントでも更新日時は進む。見張り自身も混雑で遅れたり落ちたりし、公開repoでは60日間活動が無いと止まる(GitHubのドキュメント)。
後始末
renovate.jsonも見張りも、default branchに入るまで効かない。RenovateはbaseBranchPatternsの指定があってもdefault branchの設定を読む。scheduleのworkflowもdefault branchからしか動かない- 溜まった更新をすぐ出すなら、Dashboardの「Awaiting Schedule」の欄でチェックを入れる
- 次の窓が明けたら、Developer Portalでジョブの結果を1度見る
まとめ
| 決めること | 選び方 |
|---|---|
| メモリ | npm・yarn・pnpm 11以下なら nodeMaxMemory。pnpm 12には効かない見込み |
| 窓 | 週1回なら48時間。OOMの再発は防げない |
| 見張り | 新しいPRとpushの日付で見る |
外部のボットに任せた定期処理は、一般に「失敗した」ではなく「何も起きない」の形で壊れる。動いた痕跡が途切れたら知らせる見張りは、Renovateに限らず置いておく価値がある。



