依赖升级有两个极端:一个是「从不升级」,依赖版本停在两年前,等某个 CVE 曝光时被迫做一次大爆炸式升级,风险高、回归面广;另一个是「有更新就升」,每天几十个升级 PR 淹没人,最后没人看,CI 里全是红的。两者都不对。
正确的做法是把升级自动化、把判断留在门禁上:机器负责发现新版本、开 PR、跑测试;人只负责定义策略(哪些能自动合、哪些要审、什么时候升)。本文对比 Dependabot 与 Renovate 的配置模型,并给出一套可落地的大规模依赖治理方案。
1. 依赖升级的两难
1.1 不升级的代价
依赖腐化的三个阶段:
阶段一:落后 1~2 个 minor,升级无痛
阶段二:落后 1~2 个 major,API 有破坏性变更,升级要改代码
阶段三:多个 major 落后 + 依赖链冲突,升级变成「重写模块」
越晚升级,单次升级的回归面越大,风险越集中。
1.2 全自动升级的代价
每天 40 个升级 PR 的真实成本:
- PR 数量 > 人的处理能力 → 全部积压
- 积压的 PR 基于旧 main → 大量冲突,需要反复 rebase
- 反复 rebase 触发重复 CI → runner 成本上升
- 最终没人信任这些 PR → 全部关闭 → 回到「从不升级」
1.3 目标:把噪音压到人可处理的量级
健康的目标形态:
- 每周人工需要处理的升级 PR:个位数
- 其中绝大多数(patch/minor、有测试覆盖)自动合并
- major 升级与安全更新单独走审核通道
- 所有升级都有可追溯的记录(谁、何时、从什么版本到什么版本)
2. Dependabot 实践
2.1 基础配置
Dependabot 是 GitHub 原生能力,配置放在 .github/dependabot.yml,无需额外服务。
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "09:00"
timezone: "Asia/Shanghai"
open-pull-requests-limit: 5
versioning-strategy: "increase"
labels: ["dependencies", "npm"]
commit-message:
prefix: "chore(deps)"
include: "scope"
open-pull-requests-limit 是关键旋钮:默认 5,意味着即使有新版本也会被限流,避免一次性开几十个 PR。
2.2 分组(Groups)
Dependabot 从 2023 年起支持分组,把多个依赖的升级合并到一个 PR,显著降低 PR 数量。
groups:
# 所有 patch 更新合成一个 PR
patch-updates:
applies-to: version-updates
update-types: ["patch"]
# 开发依赖(devDependencies)单独一组
dev-dependencies:
dependency-type: "development"
# 框架相关的 minor 更新合成一组
framework-minor:
patterns: ["next", "react", "react-dom", "@types/react*"]
update-types: ["minor"]
分组的收益:
PR 数量从「每个依赖一个」降到「每类一个」
但注意:一组里任何一个依赖导致测试失败,整组都要排查
→ 组不宜过大,按「同类、同风险」划分
2.3 安全更新
安全更新(Security Updates)是 Dependabot 的另一条通道,由 GitHub Advisory Database 触发,独立于版本更新配置,优先级更高。
安全更新的特点:
- 触发源是 CVE/GHSA,而非「有新版本」
- 即使版本更新被限流,安全更新也会开 PR
- 可以在仓库 Settings → Code security 中单独开关
- 建议与代码扫描(CodeQL)联动,形成「发现→修复」闭环
与 CI 的集成细节可参考 Dependabot 与依赖安全实践 。
3. Renovate 实践
3.1 为什么很多团队转向 Renovate
Renovate 是开源工具(可自托管,也可用 Mend 托管版),相比 Dependabot 的优势在于:
| 能力 | Dependabot | Renovate |
|---|---|---|
| 配置粒度 | 较粗(每个 ecosystem 一段) | 极细(继承、正则匹配、每包覆盖) |
| 分组 | 支持 | 支持,且更灵活(正则、依赖类型) |
| 调度 | 按周/日 | 可精确到时间窗口、时区、频率 |
| 自动合并 | 不支持(靠 CI + 人工) | 原生支持(按条件自动 merge) |
| lockfile 维护 | 自动 | 自动,且可控 |
| 自定义 manager | 有限 | 丰富(regex manager 可管任意文件) |
| 自托管 | 不支持 | 支持 |
3.2 基础配置
// renovate.json
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
":dependencyDashboard",
":semanticCommits",
"schedule:weekly"
],
"timezone": "Asia/Shanghai",
"prConcurrentLimit": 5,
"prHourlyLimit": 2,
"rangeStrategy": "bump",
"labels": ["dependencies"]
}
extends 里的 config:recommended 提供了合理的默认值,prConcurrentLimit 与 prHourlyLimit 是限流的核心参数,直接决定了「每周要处理多少 PR」。
3.3 分组与包规则
{
"packageRules": [
{
"description": "所有 patch 更新自动合并",
"matchUpdateTypes": ["patch"],
"automerge": true,
"automergeType": "pr",
"platformAutomerge": true
},
{
"description": "开发依赖的 minor 更新自动合并",
"matchDepTypes": ["devDependencies"],
"matchUpdateTypes": ["minor"],
"automerge": true
},
{
"description": "React 全家桶同组,避免版本错配",
"groupName": "react",
"matchPackageNames": ["react", "react-dom", "@types/react", "@types/react-dom"],
"automerge": false
},
{
"description": "major 更新需人工审核",
"matchUpdateTypes": ["major"],
"automerge": false,
"addLabels": ["major-update", "needs-review"]
},
{
"description": "内部镜像源上的包不走公网升级",
"matchDatasources": ["npm"],
"matchPackagePrefixes": ["@internal/"],
"enabled": false
}
]
}
规则是按顺序匹配、后面的覆盖前面的,因此把「更具体的规则」放在后面。
3.4 调度与 Dashboard
{
"schedule": ["after 9am and before 5pm every weekday"],
"dependencyDashboard": true,
"dependencyDashboardTitle": "依赖升级看板"
}
Dependency Dashboard 是一个常驻 issue,列出所有待升级、待审核、被忽略的依赖。它把「散落的 PR」收拢成「一张可扫视的清单」,是 Renovate 相比 Dependabot 最实用的差异点之一。
Dashboard 上的操作:
☐ 勾选某行 → Renovate 立即为该依赖开 PR
☐ 取消勾选 → 暂停该依赖的升级
☐ 勾选 "Create all rate-limited PRs" → 一次性放开限流
4. 升级策略:语义化版本与范围
4.1 语义化版本的含义
语义化版本(Semantic Versioning, SemVer)约定 MAJOR.MINOR.PATCH:
MAJOR:不兼容的 API 变更(破坏性)
MINOR:向后兼容地新增功能
PATCH:向后兼容地修复缺陷
但现实中「MINOR 引入破坏性变更」并不罕见,
所以自动合并策略要建立在「测试能覆盖」的前提上,而不是盲信版本号。
关于依赖解析与版本范围的细节,可参考 语义化版本与依赖解析 。
4.2 rangeStrategy 的选择
rangeStrategy 决定「升级时如何改写版本范围」:
bump :只把下界提到新版本,如 ^1.2.0 → ^1.3.0
replace :直接替换为精确版本,如 1.2.0 → 1.3.0
widen :扩大范围而不移动下界,如 ^1.2.0 → >=1.2.0 <2.0.0
pin :固定为精确版本
应用库(被其他项目依赖)→ 倾向 widen/bump,给使用者空间
应用服务(直接部署) → 倾向 pin,保证可复现
4.3 lockfile 的角色
lockfile(package-lock.json / pnpm-lock.yaml / go.sum):
- 记录「实际解析到的精确版本」,保证安装可复现
- 升级 PR 必须同时更新 lockfile,否则 CI 装的是旧版本
- lockfile 冲突是升级 PR 积压的主要原因 → 减少并发 PR 数可缓解
5. 合并门禁与自动化测试
5.1 自动合并的前提
自动合并(automerge)不是「不看就合」,而是「把审核标准编码成门禁」。必须满足:
自动合并的硬前提:
1. 有测试覆盖:改动能被执行到的测试捕获
2. CI 必需检查全绿:单元测试、类型检查、构建
3. 版本类型在白名单内(通常是 patch、devDependencies 的 minor)
4. 非安全敏感包(加密、鉴权、序列化等要人工看)
5. 有回滚手段(能快速 revert 或回退版本)
5.2 门禁配置示例
# GitHub Actions:为自动合并提供必需检查
name: ci
on:
pull_request:
jobs:
verify:
if: github.actor == 'renovate[bot]' || github.actor == 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm test -- --ci
- run: npm run build
Renovate 的 platformAutomerge: true 会调用平台原生的自动合并(GitHub 的 auto-merge),只在所有必需检查通过后才真正合并,比 Renovate 自己判断更可靠。
5.3 灰度与回滚
升级的灰度思路:
- 先合并到开发环境,观察一段时间再进生产
- 关键依赖(数据库驱动、HTTP 客户端)单独升级并做压测
- 保留一键回滚:把版本改回去 + 重新部署
6. 大规模依赖治理
6.1 Monorepo 中的依赖
Monorepo 依赖治理要点:
1. 统一版本:同一依赖在多个包中出现时,尽量收敛到同一版本(pnpm overrides / npm overrides)
2. 分组升级:把「同源」依赖(如 React 全家桶)合成一组,避免版本错配
3. lockfile 单一:整个仓库一个 lockfile,减少冲突
4. 提升升级到 workspace 根:在根统一升级,避免每个包各自升
// pnpm workspace 根:强制统一某些依赖版本
{
"pnpm": {
"overrides": {
"typescript": "5.4.5",
"eslint": "8.57.0"
}
}
}
6.2 供应链安全联动
依赖升级是供应链安全的第一道防线。把升级流程与安全扫描打通:
联动链路:
1. 依赖清单(SBOM):知道用了什么、什么版本
2. 漏洞数据库比对(GitHub Advisory / OSV):知道哪个版本有洞
3. 升级 PR 自动生成:从「发现」到「修复 PR」自动衔接
4. 签名与来源校验:确保升级到的是官方制品
SBOM 生成与制品溯源可参考 制品管理与供应链溯源 ,更完整的供应链防护体系见 供应链安全 。
6.3 忽略与冻结
不是所有升级都值得跟:
{
"packageRules": [
{ "matchPackageNames": ["node"], "enabled": false },
{
"matchPackageNames": ["some-frozen-lib"],
"enabled": false,
"description": "该库已停止维护,锁定版本直到迁移"
}
],
"ignorePaths": ["**/fixtures/**", "**/testdata/**"]
}
忽略必须有记录和到期日,否则「临时忽略」会变成永久腐化。
7. 度量与常见坑
7.1 度量指标
依赖治理的核心指标:
- 升级 PR 的平均存活时间(从开到合/关)
- 每周需人工处理的 PR 数(目标:个位数)
- 自动合并占比(目标:patch 与 devDeps 中 > 70%)
- 已知漏洞的平均修复时长(MTTR for vulns)
- 依赖滞后度(当前版本与最新稳定版的差距分布)
7.2 常见坑
| 坑 | 现象 | 对策 |
|---|---|---|
| 无限流 | PR 洪水,全部积压 | prConcurrentLimit / open-pull-requests-limit |
| 无分组 | PR 数量爆炸 | 按类型/来源分组 |
| lockfile 冲突 | 反复 rebase | 降低并发、及时合并 |
| 盲信 semver | minor 引入破坏性变更 | 自动合并前必须有测试覆盖 |
| 自动合并无门禁 | 合了坏版本 | 用平台原生 auto-merge + 必需检查 |
| 忽略无期限 | 依赖永久冻结 | 忽略项登记到期日 |
| 安全更新被限流 | 漏洞修复延迟 | 安全通道独立、不限流 |
| 无度量 | 不知道是否退化 | 上报存活时间与自动合并占比 |
7.3 一句话原则
依赖升级自动化 = 机器负责发现与开 PR + 门禁负责判断 + 人负责定义策略。
人的精力只花在「策略」和「major/安全」上,其余交给自动化。
小结
依赖升级的目标不是「把所有依赖都升到最新」,而是把升级变成一件低风险、低成本、可预期的日常动作。Dependabot 适合快速起步、零运维成本的场景;Renovate 适合需要精细控制分组、调度与自动合并的团队。无论选哪个,真正决定成败的是三件事:限流把 PR 数量压到人可处理的量级、自动合并建立在测试覆盖之上、安全更新走独立通道。
把这三件事做对,依赖库就不会再是「技术债的黑洞」,而是一份持续被维护、随时可审计的资产清单。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。