
Dependabot 的最小發布經過天數與自動合併 — 私有儲存庫裡的陷阱
私有儲存庫的相依關係圖預設關閉,所以 Dependabot 一個 npm PR 都沒開,而自動合併的工作流程在 CI 完成前 45 秒就合併了。
本頁目錄
引言
我在看這個部落格儲存庫的 pull request 清單時,發現少了某樣東西:沒有任何相依套件的版本更新。不是有一個過期的,也不是有一個失敗的,而是一個都沒有。我幾週前才寫好 Dependabot 的政策,連最小發布經過天數與自動合併的工作流程都寫進去了,卻舉不出它實際做過的任何一件事。
結果有兩套機制是停擺的,而且每一套都被某個看起來健康的東西遮住了。npm 生態系從來沒有運作過,因為私有儲存庫的相依關係圖預設是關閉的,而 Dependabot 需要它才能解析宣告檔。自動合併的工作流程從來沒有等待過任何東西,因為 gh pr merge --auto 等的是必要檢查,而這個儲存庫的方案根本無法把任何檢查標成必要。
本文整理這兩件事,以及它們底下的政策問題:最小發布經過天數實際上濾掉什麼、濾不掉什麼,以及為什麼這兩個答案湊在一起,才讓人能夠不魯莽地省掉 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"]關於這段設定,有兩件事值得直接講清楚,因為兩件都很容易弄錯。
cooldown 不適用於安全性更新。 這是 GitHub 刻意的設計:針對現行資安通報的修補,不該在佇列裡等上一週。這同時也表示,上面那段從來沒有在保護安全性這條路徑,而這件事比我當時以為的更重要。
它下面還有一段 github-actions,那段只有 default-days: 3。 這個生態系會忽略 semver-*-days 這幾個鍵,所以寫在那裡會看起來像政策,實際上什麼都不做。
npm 的 PR 一個都沒開過
設定看起來是對的,所以我改從證據下手。
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-03兩個 PR,都合併了,也都來自 github-actions 生態系。來自 npm 的一個也沒有。而 pnpm outdated 說有事情要做:astro 停在 7.1.6,7.2.2 已經出了。
在這裡,兩個綠色的相依更新 PR 是最糟糕的證據,因為它們正好回答了你以為自己在問的那個問題。Dependabot 確實在運作。它開過 PR、PR 合併了、標籤也對。所有看得見的東西都說這套系統是好的。
相依關係圖是關的,而一個生態系把它遮住了
跟我一起工作的智能體去看的是儲存庫本身的狀態,而不是設定檔,答案藏在三個 API 呼叫的深處。
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 這兩個間接相依的資安通報,兩件都在同一天修掉。
私有儲存庫的陷阱一句話就講完了。沒有任何東西會警告你。 沒有橫幅、沒有失敗的檢查、也沒有一個空狀態告訴你「把圖打開這件事就會動」。設定檔是有效的,排程有觸發,而更新工作只是找不到任何可以更新的東西。
為什麼我沒有加上 semver 關卡
npm 的更新即將開始流動,接下來理所當然的問題就是要不要只自動合併修補版。我說不要,而理由正是這整段作業替我釐清的東西。
不良的發布有兩種類型,各自需要不同的工具。
對所有人都是壞的發布,會被時間抓到。 它在幾小時內被撤回、隔天早上出修正版,或者是一個遭入侵的發布在有人發現後被拉掉。等待本身就是機制,而 cooldown 正是那個工具。在偵測這一類的問題上,我的建置沒有任何理由比整個生態系更強。
只對我是壞的發布,時間看不到。 它在別處都能正常建置,它會留著,而第 30 天看起來跟第 1 天一模一樣。唯一看得到它的是我自己的建置,因為壞掉的地方位在那個版本與這份程式碼的交集上。
semver 關卡是第二類的替代品,用在 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 在必要檢查通過後合併。陷阱就在那個形容詞上。這個儲存庫用的是免費的個人方案,所以分支保護與 rulesets 都回一個錯誤:
Upgrade to GitHub Pro or make this repository public to enable this feature.沒有必要檢查,就等於沒有東西要等,所以 gh 一看到就合併了。 最近一次相依更新的時間戳把這件事講得很具體:
Check & build 工作到 16:36:54 才結束,也就是分支已經在 main 上之後 45 秒。有一個值得知道的二階效應,因為它改變了這件事實際上有多糟。用 GITHUB_TOKEN 做的合併不會觸發任何工作流程執行,證據就在提交本身:由機器人合併的 8fce42b 完全沒有任何檢查執行,而由人合併的 67c3d57 有一次成功的 CI push 執行。也就是說,相依更新的合併從來沒有觸發過正式環境的部署工作。一個壞更新造成的損害是一個壞掉的 main,會在某人下一次 push 時浮現,而不是一個壞掉的網站。
理所當然的修法會等到自己
任何人第一個伸手去拿的修法,都是讓工作流程去等:保留 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'在這個條件下會跑四個步驟:找出 PR、拒絕在已經失敗的檢查之上合併、核准、合併。
其中兩個步驟承擔了容易在細節上出錯的部分。
head SHA 必須與 CI 測過的那個提交一致。 Dependabot 會 rebase 它的分支,而 gh pr merge 合併的是指令執行當下分支所指向的東西,不是 workflow_run 事件描述的那個。在找出 PR 的步驟裡檢查是必要的,但並不充分,因為中間還隔著兩個 API 來回:
gh pr merge --squash --match-head-commit "$HEAD_SHA" "$NUMBER"待處理中的檢查是刻意忽略的。 預覽的冒煙測試跑在部署商的時間軸上而不是 CI 的,對某個分支甚至可能永遠不會出現,所以這個步驟拒絕 fail 與 cancel 兩種 bucket,但從不等待 pending。失敗的樣子會是「沒看到冒煙結果就合併了」,這是可以補救的;「卡到逾時」則不行。
那個會讓一切變成空轉的 permissions 區塊
合併前的程式碼審查找到了一個東西,本機的試跑與 CI 都抓不到。
工作流程宣告了它需要寫入的權限:
permissions: contents: write pull-requests: write只要宣告了任何一項權限,沒有列出來的每一個範圍都會變成 none。也就是說 checks、statuses 與 actions 全都是零,而 gh pr checks 並不是一個 REST 呼叫:它送出一支固定的 GraphQL rollup 查詢,永遠會選取狀態脈絡、檢查執行,以及每個檢查套組背後的工作流程。--json 只在用戶端過濾,無法縮減實際送出去的內容。這個呼叫每一次都會失敗:
GraphQL: Resource not accessible by integration它是失敗時擋下,所以不會有錯的東西被合併。對的東西也一樣不會被合併。
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 更新。確認只需要一道指令:
gh pr view <n> --json mergedAt,statusCheckRollupmergedAt 必須晚於 Check & build 那一項的 completedAt。這個先後顛倒正是當初讓缺陷現形的東西,所以拿它來對修正做斷言是對的。
總結
私有儲存庫的陷阱不在於東西比較難設定,而在於預設值不一樣,而且失敗是安靜的。相依關係圖是關的,所以以宣告檔為基礎的生態系從來不會運作。必要檢查根本無法存在,所以任何建立在 --auto 之上的東西,都會在看起來像在等待的同時立刻合併。
從這件事長出來的政策,比我以前會寫的那一套更單純:
| 問題 | 答案 |
|---|---|
| 對所有人都是壞的(撤回、修正版、遭入侵) | cooldown,依 semver 距離調整 |
| 只對這個儲存庫是壞的 | CI,建立在一道真的會擋的關卡上 |
| 修補版、次版本、主版本之分 | 不是過濾器,是薄弱 CI 的替代品 |
| 資安通報 | 絕不延後。cooldown 不適用 |
另外,對任何有 Dependabot 設定卻沒有 Dependabot 動靜的儲存庫,有兩個值得跑一次的確認:
gh api repos/{owner}/{repo}/dependency-graph/sbom --jq '.sbom.packages | length'gh pr view <n> --json mergedAt,statusCheckRollup第一個告訴你 Dependabot 看不看得到你的相依套件。第二個告訴你你的合併關卡到底是不是一道關卡。