# Dependabot 的最小發布經過天數與自動合併 — 私有儲存庫裡的陷阱

> 私有儲存庫的相依關係圖預設關閉，所以 Dependabot 一個 npm PR 都沒開，而自動合併的工作流程在 CI 完成前 45 秒就合併了。

- Source: https://oharu121.com/zh-tw/blog/dependabot-minimum-release-age-auto-merge-private-repo/
- Published: 2026-08-17T22:20:29+09:00
- Tags: GitHub Actions, 自動化, 開發工具

---
## 引言

我在看這個部落格儲存庫的 pull request 清單時，發現少了某樣東西：沒有任何相依套件的版本更新。不是有一個過期的，也不是有一個失敗的，而是一個都沒有。我幾週前才寫好 Dependabot 的政策，連最小發布經過天數與自動合併的工作流程都寫進去了，卻舉不出它實際做過的任何一件事。

結果有兩套機制是停擺的，而且每一套都被某個看起來健康的東西遮住了。**`npm` 生態系從來沒有運作過**，因為私有儲存庫的相依關係圖預設是關閉的，而 Dependabot 需要它才能解析宣告檔。**自動合併的工作流程從來沒有等待過任何東西**，因為 `gh pr merge --auto` 等的是*必要*檢查，而這個儲存庫的方案根本無法把任何檢查標成必要。

本文整理這兩件事，以及它們底下的政策問題：最小發布經過天數實際上濾掉什麼、濾不掉什麼，以及為什麼這兩個答案湊在一起，才讓人能夠不魯莽地省掉 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"]
```

關於這段設定，有兩件事值得直接講清楚，因為兩件都很容易弄錯。

**`cooldown` 不適用於安全性更新。** 這是 GitHub 刻意的設計：針對現行資安通報的修補，不該在佇列裡等上一週。這同時也表示，上面那段從來沒有在保護安全性這條路徑，而這件事比我當時以為的更重要。

**它下面還有一段 `github-actions`，那段只有 `default-days: 3`。** 這個生態系會忽略 `semver-*-days` 這幾個鍵，所以寫在那裡會看起來像政策，實際上什麼都不做。

## npm 的 PR 一個都沒開過

設定看起來是對的，所以我改從證據下手。

```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，都合併了，也都來自 `github-actions` 生態系。來自 `npm` 的一個也沒有。而 `pnpm outdated` 說有事情要做：`astro` 停在 7.1.6，7.2.2 已經出了。

**在這裡，兩個綠色的相依更新 PR 是最糟糕的證據**，因為它們正好回答了你以為自己在問的那個問題。Dependabot 確實在運作。它開過 PR、PR 合併了、標籤也對。所有看得見的東西都說這套系統是好的。

## 相依關係圖是關的，而一個生態系把它遮住了

跟我一起工作的智能體去看的是儲存庫本身的狀態，而不是設定檔，答案藏在三個 API 呼叫的深處。

```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: 能運作的那一半，掩蓋了不能運作的另一半。兩個已合併的 action 更新讀起來就是「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` 這兩個間接相依的資安通報，兩件都在同一天修掉。

私有儲存庫的陷阱一句話就講完了。**沒有任何東西會警告你。** 沒有橫幅、沒有失敗的檢查、也沒有一個空狀態告訴你「把圖打開這件事就會動」。設定檔是有效的，排程有觸發，而更新工作只是找不到任何可以更新的東西。

## 為什麼我沒有加上 semver 關卡

`npm` 的更新即將開始流動，接下來理所當然的問題就是要不要只自動合併修補版。我說不要，而理由正是這整段作業替我釐清的東西。

不良的發布有兩種類型，各自需要不同的工具。

*Figure — TwoFilters: 最小發布經過天數與建置，並不是同一道過濾器的兩種強度。它們看到的失敗不同，而且誰也取代不了誰。*

**對所有人都是壞的發布，會被時間抓到。** 它在幾小時內被撤回、隔天早上出修正版，或者是一個遭入侵的發布在有人發現後被拉掉。等待本身就是機制，而 `cooldown` 正是那個工具。在偵測這一類的問題上，我的建置沒有任何理由比整個生態系更強。

**只對我是壞的發布，時間看不到。** 它在別處都能正常建置，它會留著，而第 30 天看起來跟第 1 天一模一樣。唯一看得到它的是我自己的建置，因為壞掉的地方位在那個版本與這份程式碼的交集上。

**semver 關卡是第二類的替代品**，用在 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 在必要檢查通過後合併。陷阱就在那個形容詞上。這個儲存庫用的是免費的個人方案，所以分支保護與 rulesets 都回一個錯誤：

```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` **完全沒有任何檢查執行**，而由人合併的 `67c3d57` 有一次成功的 CI push 執行。也就是說，相依更新的合併從來沒有觸發過正式環境的部署工作。一個壞更新造成的損害是一個壞掉的 `main`，會在某人下一次 push 時浮現，而不是一個壞掉的網站。

## 理所當然的修法會等到自己

任何人第一個伸手去拿的修法，都是讓工作流程去等：保留 `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: 循環回到它出發的那個方框。沒有任何東西能完成，這次執行不是以合併作結，而是以六小時的工作逾時作結。*

`--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'
```

在這個條件下會跑四個步驟：找出 PR、拒絕在已經失敗的檢查之上合併、核准、合併。

其中兩個步驟承擔了容易在細節上出錯的部分。

**head SHA 必須與 CI 測過的那個提交一致。** Dependabot 會 rebase 它的分支，而 `gh pr merge` 合併的是指令執行當下分支所指向的東西，不是 `workflow_run` 事件描述的那個。在找出 PR 的步驟裡檢查是必要的，但並不充分，因為中間還隔著兩個 API 來回：

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

**待處理中的檢查是刻意忽略的。** 預覽的冒煙測試跑在部署商的時間軸上而不是 CI 的，對某個分支甚至可能永遠不會出現，所以這個步驟拒絕 `fail` 與 `cancel` 兩種 bucket，但從不等待 `pending`。失敗的樣子會是「沒看到冒煙結果就合併了」，這是可以補救的；「卡到逾時」則不行。

## 那個會讓一切變成空轉的 permissions 區塊

合併前的程式碼審查找到了一個東西，本機的試跑與 CI 都抓不到。

工作流程宣告了它需要寫入的權限：

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

只要宣告了*任何一項*權限，沒有列出來的每一個範圍都會變成 `none`。也就是說 `checks`、`statuses` 與 `actions` 全都是零，而 `gh pr checks` 並不是一個 REST 呼叫：它送出一支固定的 GraphQL rollup 查詢，永遠會選取狀態脈絡、檢查執行，以及每個檢查套組背後的工作流程。`--json` 只在用戶端過濾，無法縮減實際送出去的內容。這個呼叫每一次都會失敗：

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

**它是失敗時擋下，所以不會有錯的東西被合併。對的東西也一樣不會被合併。**

```yaml
permissions:
  contents: write
  pull-requests: write
  checks: read
  statuses: read
  actions: read
```

我的本機驗證為什麼漏掉這一點，值得記下來。我在一個持有全範圍權杖的 shell 裡跑了 `gh pr checks 39 --json name,bucket`，看著它回傳合理的 JSON。**那證明的只是輸出的形狀，對於工作流程自己的權杖則什麼都沒有證明。** 被限縮的憑證是另一份憑證。

跟著它一起出現的還有兩個比較小的發現。沒有 `shell:` 鍵的 `run:` 步驟會在沒有 `pipefail` 的 `bash -e {0}` 下執行，所以 `gh … | head -1` 拿到的是 `head` 的結束狀態，會把一個被限流的 API 呼叫，在一次綠色的執行裡報告成「找不到 Dependabot 的 PR」。另外，被取消的檢查落在 gh 的 `cancel` bucket 而不是 `fail`，所以拒絕用的過濾條件必須把兩者都點名。

## 綠燈實際證明了什麼

這個儲存庫的 CI 只跑 `pnpm check` 與 `pnpm build`，沒有別的。**這個故事裡的每一個缺陷，都是讀出來的，不是靠某個徽章變紅發現的。**

新工作流程第一次真正受到考驗，會是那個等次版本 cooldown 到期後才符合資格的 `astro` 更新。確認只需要一道指令：

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

`mergedAt` 必須晚於 `Check & build` 那一項的 `completedAt`。這個先後顛倒正是當初讓缺陷現形的東西，所以拿它來對修正做斷言是對的。

## 總結

私有儲存庫的陷阱不在於東西比較難設定，而在於**預設值不一樣，而且失敗是安靜的**。相依關係圖是關的，所以以宣告檔為基礎的生態系從來不會運作。必要檢查根本無法存在，所以任何建立在 `--auto` 之上的東西，都會在看起來像在等待的同時立刻合併。

從這件事長出來的政策，比我以前會寫的那一套更單純：

| 問題 | 答案 |
| --- | --- |
| 對所有人都是壞的（撤回、修正版、遭入侵） | `cooldown`，依 semver 距離調整 |
| 只對這個儲存庫是壞的 | CI，建立在一道真的會擋的關卡上 |
| 修補版、次版本、主版本之分 | 不是過濾器，是薄弱 CI 的替代品 |
| 資安通報 | 絕不延後。`cooldown` 不適用 |

另外，對任何有 Dependabot 設定卻沒有 Dependabot 動靜的儲存庫，有兩個值得跑一次的確認：

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

第一個告訴你 Dependabot 看不看得到你的相依套件。第二個告訴你你的合併關卡到底是不是一道關卡。

## 參考連結

- [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)
