
Dependabotで最小リリース経過日数と自動マージを設定する — プライベートリポジトリに潜む罠
プライベートリポジトリでは依存関係グラフが既定でオフのため、DependabotはnpmのPRを1件も出さず、自動マージのワークフローはCI完了の45秒前にマージしていた。
目次
はじめに
このブログのリポジトリでプルリクエストの一覧を眺めていて、あるべきものがないことに気づきました。依存関係の更新が 1 件もないのです。古いものも、失敗しているものも、そもそもありません。数週間前に最小リリース経過日数と自動マージのワークフローまで含めた Dependabot のポリシーを書いていたのに、それが実際に何かをした形跡をひとつも挙げられませんでした。
2 つの仕組みが動いていないことが分かり、そのどちらも健全に見える何かに隠されていました。npm のエコシステムは一度も動いていませんでした。プライベートリポジトリでは依存関係グラフが既定でオフで、Dependabot はマニフェストの解析にそれを必要とするからです。自動マージのワークフローは何も待っていませんでした。gh pr merge --auto が待つのは 必須 チェックであり、このリポジトリのプランではどのチェックも必須にできないからです。
本記事では、その両方と、その下にあるポリシーの問いを整理します。最小リリース経過日数が実際に濾し取るもの、濾し取れないもの、そしてその 2 つの答えが揃うことで、なぜ無謀にならずに semver ゲートを省けるのかという話です。
すでに持っていたポリシー
cooldown のブロックは私が書いたもので、今回の一件より前からありました。リリースが公開されてから一定日数が経つまで、提案される更新を遅らせる設定です。日数はバージョンの跳び幅に応じて変えています。
updates: - package-ecosystem: "npm" directory: "/" schedule: interval: "weekly" cooldown: default-days: 7 semver-major-days: 14 semver-minor-days: 7 semver-patch-days: 3 open-pull-requests-limit: 10 ignore: # TypeScript 7 (the native compiler) does not expose the programmatic API # that `astro check` relies on, so upgrading breaks `pnpm check`. - dependency-name: "typescript" update-types: ["version-update:semver-major"]この設定について、はっきり書いておくべきことが 2 つあります。どちらも取り違えやすい点です。
cooldown はセキュリティ更新には適用されません。 これは GitHub 側の意図的な設計です。現に出ている脆弱性への修正が 1 週間もキューで待たされるべきではありません。同時にそれは、上のブロックがセキュリティの経路を一度も守っていなかったということでもあり、当時の私が思っていたよりも重い意味を持っていました。
その下には github-actions のブロックがあり、そちらは default-days: 3 だけです。 このエコシステムでは semver-*-days のキーは無視されるので、そこに書けばポリシーのように見えて何もしないものになっていたはずです。
npm の PR は 1 件も作られていなかった
設定は正しく見えたので、代わりに証拠から手をつけました。
gh pr list --author "app/dependabot" --state all --limit 2039 build(deps): bump pnpm/action-setup from 6.0.9 to 6.0.10 MERGED 2026-08-102 build(deps): bump pnpm/action-setup from 6 to 6.0.9 MERGED 2026-08-03PR は 2 件、どちらもマージ済みで、どちらも github-actions のエコシステムからのものです。npm からのものは 1 件もありません。そして pnpm outdated は仕事があると言っていました。astro は 7.1.6 で、7.2.2 が出ています。
ここでは、緑色の依存更新 PR が 2 件あることが最悪の証拠です。 自分が問いたいと思っている問いに、それが答えを返してしまうからです。Dependabot は確かに動いていました。PR を出し、それがマージされ、ラベルも正しい。目に見えるものはすべて、この仕組みは動いていると言っていました。
依存関係グラフがオフで、片方のエコシステムがそれを隠していた
一緒に作業していたエージェントは、設定ではなくリポジトリ自身の状態を見に行き、答えは API 3 回分の深さにありました。
gh api repos/{owner}/{repo}/dependency-graph/sbom --jq '.sbom.packages | length'# 404
gh api repos/{owner}/{repo}/vulnerability-alerts -i | head -1# HTTP/2.0 404 Not Found
gh api graphql -f query='{ repository(owner:"…", name:"…") { dependencyGraphManifests { totalCount } } }'# {"dependencyGraphManifests":{"totalCount":0}}マニフェストは 0 件。依存関係グラフが無効になっており、これはプライベートリポジトリの既定です。 Dependabot は package.json と pnpm-lock.yaml を読むためにそれを必要とします。github-actions のエコシステムは必要としません。更新ジョブの内側で .github/workflows/*.yml を解析し、グラフには一切触れないからです。ほかがすべて死んでいるあいだ、これだけが動き続けていた理由がまさにそこにあります。
アラートを有効にしました。これで副作用として依存関係グラフが有効になります。
gh api -X PUT repos/{owner}/{repo}/vulnerability-alertsgh api -X PUT repos/{owner}/{repo}/automated-security-fixesグラフのマニフェストは 0 件から 5 件になり、SBOM は 404 から 600 パッケージになりました。このリポジトリにはセキュリティ更新もそれまで存在しておらず、有効にした途端に実際の仕事が出てきました。nanoid と path-to-regexp の推移的依存に対する 2 件のセキュリティアドバイザリで、いずれも同日に修正しています。
プライベートリポジトリの罠は一言で言えばこれです。何も警告してくれません。 バナーも、失敗するチェックも、「グラフを有効にすれば動きます」と告げる空状態もありません。設定ファイルは妥当で、スケジュールは発火し、更新ジョブは更新すべきものを何も見つけないだけです。
なぜ semver ゲートを足さなかったか
npm の更新が流れ始めようとしていたので、次に来る当然の問いは、パッチだけ自動マージすべきかどうかでした。私は足さないと決めました。その理由こそ、今回の一連の作業が私にはっきりさせてくれたものです。
不良なリリースには 2 つのクラスがあり、それぞれ別の道具を必要とします。
誰にとっても不良なリリースは、時間が捕まえます。 数時間で取り下げられるか、翌朝に修正版が出るか、侵害された公開物が誰かの指摘で引っ込められるかです。待つことそのものが仕組みであり、cooldown はまさにその道具です。このクラスの検知について、私のビルドがエコシステム全体より優れている理由は何もありません。
自分にとってだけ不良なリリースは、時間には見えません。 他所では問題なくビルドでき、公開されたまま残り、30 日目は 1 日目とまったく同じに見えます。それを見られるのは自分のビルドだけです。壊れているのは、そのリリースとこのコードベースの交わるところだからです。
semver ゲートは 2 つ目のクラスの代用品であり、CI の網羅が弱いか遅い場所で使うものです。「マイナーはパッチより危険だ」と言っているのであって、それは平均としては正しく、この マイナーが この リポジトリを壊すかどうかについては何も言っていません。CI が実際に動き、実際に止めるのであれば、この代用品は本物の計測がすでに与えているもの以上を何も足しません。
その「であれば」は重い仕事をしており、ここではそれが成り立っていませんでした。
自動マージのワークフローは一度も何も止めていなかった
ワークフローはゲートのように読めました。
on: pull_request_target: types: [opened, synchronize, reopened]
jobs: auto-merge: if: github.actor == 'dependabot[bot]' steps: # … - name: Enable auto-merge for Dependabot PR run: gh pr merge --auto --squash "$PR_URL"--auto は「必須チェックが通ったらマージしてほしい」と GitHub に頼むものです。落とし穴はその形容詞にあります。このリポジトリは無料の個人プランなので、ブランチ保護もルールセットもエラーを返します。
Upgrade to GitHub Pro or make this repository public to enable this feature.必須チェックがないということは待つ対象がないということで、gh は即座にマージしました。 直近の依存更新のタイムスタンプがそれを具体的に示しています。
Check & build ジョブが終わったのは 16:36:54 で、ブランチがすでに main に載ってから 45 秒後です。知っておく価値のある二次的な効果があり、これが実害の大きさを変えます。GITHUB_TOKEN で行ったマージはワークフローを起動しません。証拠はコミット自体にあります。ボットがマージした 8fce42b には チェック実行が 1 件もなく、人がマージした 67c3d57 には成功した CI の push 実行があります。つまり依存更新のマージでは本番デプロイのジョブが一度も動いていません。不良な更新による被害は、誰かの次の push で表面化する壊れた main であって、壊れたサイトではありませんでした。
当然の修正は自分自身を待ってしまう
誰もが最初に手を伸ばす修正は、ワークフローに待たせることです。pull_request_target はそのままに、--auto を gh pr checks --watch に置き換えて、そのあとマージする。エージェントはまさにそれを提案し、そのうえでなぜ成立しないかを見つけました。
gh pr checks 39Deploy to Vercel (production) skipping 0Check & build pass 50sVercel pass 0auto-merge pass 9sauto-merge がその一覧に入っています。pull_request_target のジョブ自体が PR 上のチェックであるため、その内側で全チェックの完了を待つステップは、自分が動いているジョブを待つことになります。
--required も逃げ道にはなりません。このプランには必須チェックが存在せず、それは元の問題が別の顔をしているだけです。
成功した方法。CI 完了後の workflow_run
私は workflow_run の経路を採りました。CI の実行が終わったあとに発火し、ベースリポジトリの文脈で書き込みトークンを持って動き、PR にチェックを付けず、待機でランナーの時間を使いません。
on: workflow_run: workflows: ["CI"] types: [completed]
concurrency: group: dependabot-auto-merge-${{ github.event.workflow_run.head_branch }} cancel-in-progress: false
jobs: auto-merge: if: >- github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'この条件の下で 4 つのステップが動きます。PR の特定、すでに失敗しているチェックの上でのマージ拒否、承認、マージです。
そのうち 2 つが、微妙に間違えやすい部分を担っています。
ヘッド SHA は CI がテストしたコミットと一致していなければなりません。 Dependabot はブランチをリベースしますし、gh pr merge はコマンドを実行した時点でブランチが指しているものをマージするのであって、workflow_run のイベントが記述したものをマージするわけではありません。特定のステップで確認することは必要ですが十分ではありません。そのあいだに API のやり取りが 2 往復入るからです。
gh pr merge --squash --match-head-commit "$HEAD_SHA" "$NUMBER"保留中のチェックは意図的に無視します。 プレビューのスモークテストは CI ではなくデプロイ先のタイムラインで動き、あるブランチでは永遠に来ないこともあります。そのためこのステップは fail と cancel のバケットを拒否し、pending を待つことはしません。失敗の仕方は「スモークの結果を見ずにマージした」であって、これは取り返しがつきます。「タイムアウトまで固まる」ほうは取り返しがつきません。
すべてを無意味にしていたはずの permissions ブロック
マージ前のコードレビューが、ローカルの試し撃ちにも CI にも捕まえられないものを見つけました。
ワークフローは、書き込みに必要なものを宣言していました。
permissions: contents: write pull-requests: writeいずれかの権限を宣言すると、書かなかったスコープはすべて none になります。つまり checks、statuses、actions はどれもゼロでした。そして gh pr checks は REST の呼び出しではありません。ステータスコンテキスト、チェック実行、各チェックスイートの背後にあるワークフローを常に選択する、固定の GraphQL ロールアップクエリを 1 本投げます。--json はクライアント側で絞るだけで、通信内容を狭めることはできません。この呼び出しは毎回失敗していたはずです。
GraphQL: Resource not accessible by integrationこれはフェイルクローズなので、間違ったものがマージされることはありません。正しいものもマージされません。
permissions: contents: write pull-requests: write checks: read statuses: read actions: read私のローカル検証がこれを取り逃がした理由は、覚えておく価値があります。私は gh pr checks 39 --json name,bucket を、全スコープのトークンを持つシェルから実行し、まともな JSON が返るのを見ていました。それが証明するのは出力の形だけで、ワークフロー自身のトークンについては何ひとつ証明していません。 絞られた資格情報は別の資格情報です。
これと一緒に、小さめの指摘が 2 つ出ました。shell: キーのない run: ステップは pipefail なしの bash -e {0} で動くため、gh … | head -1 は head の終了ステータスを拾っており、レート制限にかかった API 呼び出しを「Dependabot の PR はない」として緑の実行のまま報告していたはずです。もうひとつ、キャンセルされたチェックは gh の fail ではなく cancel バケットに入るので、拒否のフィルタは両方を名指しする必要がありました。
緑が実際に証明していること
このリポジトリの CI が動かすのは pnpm check と pnpm build だけです。この話に出てきた欠陥はすべて、バッジが赤くなったからではなく、読んだから見つかりました。
新しいワークフローが初めて本当に試されるのは、マイナーの cooldown が切れて対象になる astro の更新です。確認はコマンド 1 本です。
gh pr view <n> --json mergedAt,statusCheckRollupmergedAt は Check & build の項目の completedAt より後でなければなりません。その逆転こそが元の欠陥を露わにしたので、修正に対して主張すべきものとして正しいのはそれです。
まとめ
プライベートリポジトリの罠は、設定が難しいことではありません。既定値が違い、しかも失敗が静かなことです。依存関係グラフはオフなので、マニフェストを使うエコシステムは一度も動きません。必須チェックは存在し得ないので、--auto の上に建てたものは、待っているように見えながら即座にマージします。
そこから出てきたポリシーは、以前の私が書いたであろうものより単純です。
| 問い | 答え |
|---|---|
| 誰にとっても不良(取り下げ、修正版、侵害) | cooldown。semver の距離に応じて調整 |
| このリポジトリにとってだけ不良 | CI。実際に止めるゲートの上で |
| パッチ/マイナー/メジャーの別 | フィルタではない。弱い CI の代用品 |
| セキュリティアドバイザリ | 決して遅らせない。cooldown は適用されない |
そして、Dependabot の設定があるのに Dependabot の動きがないリポジトリに対して、走らせる価値のある確認が 2 つあります。
gh api repos/{owner}/{repo}/dependency-graph/sbom --jq '.sbom.packages | length'gh pr view <n> --json mergedAt,statusCheckRollup1 つ目は Dependabot が依存関係を見えているかを教えます。2 つ目はマージのゲートがゲートになっているかを教えます。