# Dependabotで最小リリース経過日数と自動マージを設定する — プライベートリポジトリに潜む罠

> プライベートリポジトリでは依存関係グラフが既定でオフのため、DependabotはnpmのPRを1件も出さず、自動マージのワークフローはCI完了の45秒前にマージしていた。

- Source: https://oharu121.com/ja/blog/dependabot-minimum-release-age-auto-merge-private-repo/
- Published: 2026-08-17T22:20:29+09:00
- Tags: GitHub Actions, 自動化, 開発ツール

---
## はじめに

このブログのリポジトリでプルリクエストの一覧を眺めていて、あるべきものがないことに気づきました。依存関係の更新が 1 件もないのです。古いものも、失敗しているものも、そもそもありません。数週間前に最小リリース経過日数と自動マージのワークフローまで含めた Dependabot のポリシーを書いていたのに、それが実際に何かをした形跡をひとつも挙げられませんでした。

2 つの仕組みが動いていないことが分かり、そのどちらも健全に見える何かに隠されていました。**`npm` のエコシステムは一度も動いていませんでした**。プライベートリポジトリでは依存関係グラフが既定でオフで、Dependabot はマニフェストの解析にそれを必要とするからです。**自動マージのワークフローは何も待っていませんでした**。`gh pr merge --auto` が待つのは *必須* チェックであり、このリポジトリのプランではどのチェックも必須にできないからです。

本記事では、その両方と、その下にあるポリシーの問いを整理します。最小リリース経過日数が実際に濾し取るもの、濾し取れないもの、そしてその 2 つの答えが揃うことで、なぜ無謀にならずに semver ゲートを省けるのかという話です。

## すでに持っていたポリシー

`cooldown` のブロックは私が書いたもので、今回の一件より前からありました。リリースが公開されてから一定日数が経つまで、提案される更新を遅らせる設定です。日数はバージョンの跳び幅に応じて変えています。

```yaml title=".github/dependabot.yml"
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 件も作られていなかった

設定は正しく見えたので、代わりに証拠から手をつけました。

```bash
gh pr list --author "app/dependabot" --state all --limit 20
```

```text
39	build(deps): bump pnpm/action-setup from 6.0.9 to 6.0.10	MERGED	2026-08-10
2	build(deps): bump pnpm/action-setup from 6 to 6.0.9	    MERGED	2026-08-03
```

PR は 2 件、どちらもマージ済みで、どちらも `github-actions` のエコシステムからのものです。`npm` からのものは 1 件もありません。そして `pnpm outdated` は仕事があると言っていました。`astro` は 7.1.6 で、7.2.2 が出ています。

**ここでは、緑色の依存更新 PR が 2 件あることが最悪の証拠です。** 自分が問いたいと思っている問いに、それが答えを返してしまうからです。Dependabot は確かに動いていました。PR を出し、それがマージされ、ラベルも正しい。目に見えるものはすべて、この仕組みは動いていると言っていました。

## 依存関係グラフがオフで、片方のエコシステムがそれを隠していた

一緒に作業していたエージェントは、設定ではなくリポジトリ自身の状態を見に行き、答えは API 3 回分の深さにありました。

```bash
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` を解析し、グラフには一切触れないからです。ほかがすべて死んでいるあいだ、これだけが動き続けていた理由がまさにそこにあります。

*Figure — MaskingAsymmetry: 動いていた側が、動いていなかった側を覆い隠していました。マージ済みのアクション更新 2 件が「Dependabot は動いている」と読めてしまい、マニフェストを使うエコシステムはすべて止まっていました。*

アラートを有効にしました。これで副作用として依存関係グラフが有効になります。

```bash
gh api -X PUT repos/{owner}/{repo}/vulnerability-alerts
gh api -X PUT repos/{owner}/{repo}/automated-security-fixes
```

グラフのマニフェストは 0 件から 5 件になり、SBOM は `404` から 600 パッケージになりました。このリポジトリにはセキュリティ更新もそれまで存在しておらず、有効にした途端に実際の仕事が出てきました。`nanoid` と `path-to-regexp` の推移的依存に対する 2 件のセキュリティアドバイザリで、いずれも同日に修正しています。

プライベートリポジトリの罠は一言で言えばこれです。**何も警告してくれません。** バナーも、失敗するチェックも、「グラフを有効にすれば動きます」と告げる空状態もありません。設定ファイルは妥当で、スケジュールは発火し、更新ジョブは更新すべきものを何も見つけないだけです。

## なぜ semver ゲートを足さなかったか

`npm` の更新が流れ始めようとしていたので、次に来る当然の問いは、パッチだけ自動マージすべきかどうかでした。私は足さないと決めました。その理由こそ、今回の一連の作業が私にはっきりさせてくれたものです。

不良なリリースには 2 つのクラスがあり、それぞれ別の道具を必要とします。

*Figure — TwoFilters: 最小リリース経過日数とビルドは、同じフィルタの強弱ではありません。見ている失敗が違い、どちらも他方の代わりにはなりません。*

**誰にとっても不良なリリースは、時間が捕まえます。** 数時間で取り下げられるか、翌朝に修正版が出るか、侵害された公開物が誰かの指摘で引っ込められるかです。待つことそのものが仕組みであり、`cooldown` はまさにその道具です。このクラスの検知について、私のビルドがエコシステム全体より優れている理由は何もありません。

**自分にとってだけ不良なリリースは、時間には見えません。** 他所では問題なくビルドでき、公開されたまま残り、30 日目は 1 日目とまったく同じに見えます。それを見られるのは自分のビルドだけです。壊れているのは、そのリリースとこのコードベースの交わるところだからです。

**semver ゲートは 2 つ目のクラスの代用品であり**、CI の網羅が弱いか遅い場所で使うものです。「マイナーはパッチより危険だ」と言っているのであって、それは平均としては正しく、*この* マイナーが *この* リポジトリを壊すかどうかについては何も言っていません。CI が実際に動き、実際に止めるのであれば、この代用品は本物の計測がすでに与えているもの以上を何も足しません。

その「であれば」は重い仕事をしており、ここではそれが成り立っていませんでした。

## 自動マージのワークフローは一度も何も止めていなかった

ワークフローはゲートのように読めました。

```yaml title=".github/workflows/dependabot-auto-merge.yml"
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 に頼むものです。落とし穴はその形容詞にあります。このリポジトリは無料の個人プランなので、ブランチ保護もルールセットもエラーを返します。

```text
Upgrade to GitHub Pro or make this repository public to enable this feature.
```

**必須チェックがないということは待つ対象がないということで、`gh` は即座にマージしました。** 直近の依存更新のタイムスタンプがそれを具体的に示しています。

*Figure — MergedBeforeCI: PR #39 は 16:35:56 に作成され、16:36:09 にマージされました。その `Check & build` ジョブが終わったのは 16:36:54 で、ブランチがすでに `main` に載ってから 45 秒後です。*

知っておく価値のある二次的な効果があり、これが実害の大きさを変えます。`GITHUB_TOKEN` で行ったマージはワークフローを起動しません。証拠はコミット自体にあります。ボットがマージした `8fce42b` には **チェック実行が 1 件もなく**、人がマージした `67c3d57` には成功した CI の push 実行があります。つまり依存更新のマージでは本番デプロイのジョブが一度も動いていません。不良な更新による被害は、誰かの次の push で表面化する壊れた `main` であって、壊れたサイトではありませんでした。

## 当然の修正は自分自身を待ってしまう

誰もが最初に手を伸ばす修正は、ワークフローに待たせることです。`pull_request_target` はそのままに、`--auto` を `gh pr checks --watch` に置き換えて、そのあとマージする。エージェントはまさにそれを提案し、そのうえでなぜ成立しないかを見つけました。

```bash
gh pr checks 39
```

```text
Deploy to Vercel (production)	skipping	0
Check & build	pass	50s
Vercel	pass	0
auto-merge	pass	9s
```

`auto-merge` がその一覧に入っています。**`pull_request_target` のジョブ自体が PR 上のチェックである**ため、その内側で全チェックの完了を待つステップは、自分が動いているジョブを待つことになります。

*Figure — WatchDeadlock: 循環は出発した箱に閉じます。何も完了せず、実行はマージではなく 6 時間のジョブタイムアウトで終わります。*

`--required` も逃げ道にはなりません。このプランには必須チェックが存在せず、それは元の問題が別の顔をしているだけです。

## 成功した方法。CI 完了後の workflow_run

私は `workflow_run` の経路を採りました。CI の実行が終わったあとに発火し、ベースリポジトリの文脈で書き込みトークンを持って動き、PR にチェックを付けず、待機でランナーの時間を使いません。

```yaml title=".github/workflows/dependabot-auto-merge.yml"
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 往復入るからです。

```bash
gh pr merge --squash --match-head-commit "$HEAD_SHA" "$NUMBER"
```

**保留中のチェックは意図的に無視します。** プレビューのスモークテストは CI ではなくデプロイ先のタイムラインで動き、あるブランチでは永遠に来ないこともあります。そのためこのステップは `fail` と `cancel` のバケットを拒否し、`pending` を待つことはしません。失敗の仕方は「スモークの結果を見ずにマージした」であって、これは取り返しがつきます。「タイムアウトまで固まる」ほうは取り返しがつきません。

## すべてを無意味にしていたはずの permissions ブロック

マージ前のコードレビューが、ローカルの試し撃ちにも CI にも捕まえられないものを見つけました。

ワークフローは、書き込みに必要なものを宣言していました。

```yaml
permissions:
  contents: write
  pull-requests: write
```

*いずれか*の権限を宣言すると、書かなかったスコープはすべて `none` になります。つまり `checks`、`statuses`、`actions` はどれもゼロでした。そして `gh pr checks` は REST の呼び出しではありません。ステータスコンテキスト、チェック実行、各チェックスイートの背後にあるワークフローを常に選択する、固定の GraphQL ロールアップクエリを 1 本投げます。`--json` はクライアント側で絞るだけで、通信内容を狭めることはできません。この呼び出しは毎回失敗していたはずです。

```text
GraphQL: Resource not accessible by integration
```

**これはフェイルクローズなので、間違ったものがマージされることはありません。正しいものもマージされません。**

```yaml
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 本です。

```bash
gh pr view <n> --json mergedAt,statusCheckRollup
```

`mergedAt` は `Check & build` の項目の `completedAt` より後でなければなりません。その逆転こそが元の欠陥を露わにしたので、修正に対して主張すべきものとして正しいのはそれです。

## まとめ

プライベートリポジトリの罠は、設定が難しいことではありません。**既定値が違い、しかも失敗が静かなこと**です。依存関係グラフはオフなので、マニフェストを使うエコシステムは一度も動きません。必須チェックは存在し得ないので、`--auto` の上に建てたものは、待っているように見えながら即座にマージします。

そこから出てきたポリシーは、以前の私が書いたであろうものより単純です。

| 問い | 答え |
| --- | --- |
| 誰にとっても不良（取り下げ、修正版、侵害） | `cooldown`。semver の距離に応じて調整 |
| このリポジトリにとってだけ不良 | CI。実際に止めるゲートの上で |
| パッチ／マイナー／メジャーの別 | フィルタではない。弱い CI の代用品 |
| セキュリティアドバイザリ | 決して遅らせない。`cooldown` は適用されない |

そして、Dependabot の設定があるのに Dependabot の動きがないリポジトリに対して、走らせる価値のある確認が 2 つあります。

```bash
gh api repos/{owner}/{repo}/dependency-graph/sbom --jq '.sbom.packages | length'
gh pr view <n> --json mergedAt,statusCheckRollup
```

1 つ目は Dependabot が依存関係を見えているかを教えます。2 つ目はマージのゲートがゲートになっているかを教えます。

## 参考リンク

- [Dependabot options reference。cooldown ブロックと、それがセキュリティ更新には適用されないという注記を含む](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/dependabot-options-reference)
- [Configuring Dependabot version updates。依存関係グラフを有効にする必要があると述べている](https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/configure-version-updates)
- [Automatic token authentication。いずれかの権限を宣言すると残りが none になる件](https://docs.github.com/en/actions/security-for-github-actions/security-guides/automatic-token-authentication)
- [Events that trigger workflows: workflow_run](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#workflow_run)
