Renovate Pull Request 机制详解:PR 发现、关闭即忽略与 Immortal PR 的来龙去脉
【免费下载链接】renovateHome of the Renovate CLI: Cross-platform Dependency Automation by Mend.io项目地址: https://gitcode.com/GitHub_Trending/re/renovate
Renovate 的 Pull Request(PR)是依赖自动化更新的核心交付物。本篇指南以官方文档 pull-requests.md 为主体,深入讲解 Renovate 如何在不维护任何数据库的前提下发现既有 PR、为何"关闭 PR 即表示忽略该更新"、以及令人困扰的 "Immortal PR"(永生 PR)背后的缓存键设计逻辑与修复方案。读完本文,你将理解 Renovate PR 的标题与分支命名约定,掌握recreateWhen等关键配置项,并能从源码层面解释为什么某些 PR 关闭后会反复出现。
Renovate 如何发现既有 PR
Renovate 采用无状态设计:它不需要维护任何关于打开或关闭 PR 的数据库/状态记录。这一点与许多自建依赖机器人不同——Renovate 不会在本地持久化"我历史上创建过哪些 PR",而是每次运行都直接调用代码平台(GitHub、GitLab、Gitea、Azure DevOps 等)的 API去搜索、查找这些 PR。
具体来说,Renovate 通过同时匹配两个条件来定位一个既有 PR(无论是打开的还是关闭的):
- 分支名(branch name),例如
renovate/lodash-4.x; - PR 标题(PR title),例如
Update lodash to v4.17.21。
在大多数场景下,分支名 + PR 标题的组合是"唯一"的,因此只会有一个既有 PR 与之匹配。但如果你使用了分组(group)配置,把多个更新聚合到一个 PR,并使用类似 "All non-major updates" 的通用标题,那么历史上有多个PR 都可能匹配这个标题——这正是后续 Immortal PR 问题的伏笔。
源码印证:findPr 的双重匹配逻辑
以 GitHub 平台实现为例,lib/modules/platform/github/index.ts 中的findPr函数精确地实现了上述匹配逻辑:
export async function findPr({ branchName, prTitle, state = 'all', includeOtherAuthors, }: FindPRConfig): Promise<GhPr | null> { logger.debug(`findPr(${branchName}, ${prTitle}, ${state})`); if (includeOtherAuthors) { // PR 可能由任何人创建,因此不走缓存的 Renovate PR 列表,直接查平台 API const { body: prList } = await githubApi.getJsonUnchecked<GhRestPr[]>( `repos/${repo}/pulls?head=${org}:${branchName}&state=open`, { cacheProvider: repoCacheProvider }, ); ... } const prList = await getPrList(); const pr = prList.find((p) => { if (p.sourceBranch !== branchName) { return false; } if (prTitle && prTitle.toUpperCase() !== p.title.toUpperCase()) { return false; } if (!matchesState(p.state, state)) { return false; } ... return true; }); ... }从源码可以看到三个关键细节:
- 分支名是强匹配:
p.sourceBranch !== branchName直接判不等即排除; - 标题匹配不区分大小写:通过
toUpperCase()比较,所以Update lodash to v4.17.21与update lodash to v4.17.21会被视为同一个 PR; - state 参数决定搜索范围:默认
state = 'all',即同时覆盖打开与关闭的 PR。关闭的 PR 之所以能被"记住并忽略",正是因为搜索范围覆盖了已关闭状态。
findPr是各平台通用的接口契约,在 lib/modules/platform/types.ts 中定义,GitHub、GitLab、Bitbucket、Gitea、Forgejo 等所有平台适配器都实现了该函数。这印证了文档所述:"它使用代码平台的 API 来搜索和查找 PR"——每个平台各有对应的实现,但语义一致。
普通 PR:关闭即忽略
如上文所述,普通 Renovate PR 的标题中通常带有与升级版本相关的"唯一性"信息(例如标题里的目标版本号)。当这些"唯一" PR 被关闭时,Renovate 会假设你不想再看到该更新,从而在未来跳过它。
以lodash为例,文档给出了完整的行为链条:
- 你通过关闭 Renovate 的 PR,忽略了
lodash@4.17.21; - Renovate 据此假设你不需要
4.17.21的任何更新; - 当"分支名 + 标题唯一性"重新出现时(例如
lodash@4.17.22发布),Renovate 会创建新 PR——因为这是一个新的版本、新的唯一性。
对于major大版本更新,行为类似但范围更大:
- 你通过关闭 PR,忽略了 Lodash 的 major 更新(PR 标题形如 "Update lodash to v4");
- Renovate 假设你不需要
v4的任何更新; - 即使
v4后续发布了更新版本,Renovate 也不会为v4创建任何更新 PR。
这就是"关闭 PR = 忽略该更新"的完整语义:标题中的版本信息越具体,忽略的范围就越精确;反之,标题越笼统,忽略的范围就越宽,越容易"误伤"后续更新。
Immortal PR:为什么关闭后还会反复出现
有时你会看到某些 Renovate PR 的描述里带有这样一段特殊声明:
👻Immortal:This PR will be recreated if closed unmerged. Get config help if that's undesired.
这段声明意味着:这是一个 "Immortal"(永生)PR——你关闭它之后,它会在下一次 Renovate 运行时再次出现。本文档明确澄清:这并不是 Renovate 出于"这个更新对你有益,别忽略它"之类的哲学考量,而是因为在现有机制下,Renovate 没有好的办法在 PR 关闭后安全地忽略它们。
根因:分支名和 PR 标题是缓存键
Renovate 把分支名 + PR 标题当作类似**缓存键(cache key)**的机制来使用:
- 如果同一个缓存键存在且对应的 PR 已被关闭,Renovate 就忽略这个 PR;
- 如果缓存键不存在(例如版本升级到了新的目标版本,标题随之变化),Renovate 就会创建一个新 PR。
这套机制对"普通 PR"工作得很好,因为普通 PR 的标题带有版本唯一性(如 "to v16.1.2"),关闭后可以被安全地忽略;但一旦标题失去唯一性,缓存键机制就会出问题。
缓存键引发的两个典型问题
问题一:过度宽泛的标题会永久屏蔽整个更新类别。假设你配置了一个 "All non-major updates" 的分组 PR。如果关闭这个 PR,而 Renovate 基于 PR 标题将其忽略,那么你将永远不再收到任何 non-major 更新——因为标题里没有任何版本信息可以区分"这次的 non-major 更新"和"下一次的 non-major 更新"。
问题二:只有"唯一版本" PR 才能被安全忽略。Renovate 只有在标题包含唯一版本(如 "to v16.1.2" 或 "to v16")时,才能安全地忽略被关闭的 PR。一旦标题退化成了没有具体版本的形式(见下文的分组场景),关闭它就不再是一个可靠的"忽略"信号。
分组更新且各依赖版本不同:Immortal 的典型来源
问题的高发场景是分组更新(grouped updates)中包含版本各不相同的多个依赖。此时:
- 如果组内各依赖升级到同一个版本,标题可以是 "Update react to v16",仍具有唯一性,可被安全忽略;
- 如果组内各依赖的目标版本各不相同,标题就会退化为 "Update react (major)" 这种不可安全忽略的形式(而不是 "Update react to v16")。
换句话说,Immortal PR 本质上是"无法用标题表达唯一版本"的分组 PR。你越倾向于把版本进度不同的依赖强行分组,就越容易制造 Immortal PR。
源码印证:Immortal 标记的生成条件
PR 描述中的 Immortal 声明并非随机出现,而是由 lib/workers/repository/update/pr/body/config-description.ts 依据recreateClosed标志渲染的:
if (config.recreateClosed) { prBody += emojify( `:ghost: **Immortal**: This PR will be recreated if closed unmerged. Get config help.help}) if that's undesired.\n\n`, ); } else { prBody += emojify( `:no_bell: **Ignore**: Close this PR and you won't be reminded about ${ config.upgrades.length === 1 ? 'this update' : 'these updates' } again.\n\n`, ); }这段代码揭示了两种截然相反的 PR 行为声明:
recreateClosed = true:渲染 👻Immortal声明,提示"关闭后会被重建";recreateClosed = false:渲染 🔕Ignore声明,提示"关闭后不会再提醒你"。
那么recreateClosed何时为true?答案在分支生成逻辑 lib/workers/repository/updates/generate.ts 中:
- 普通升级:
upg.recreateClosed = upg.recreateWhen === 'always'——只有显式配置recreateWhen: 'always'才会变成 Immortal; - lockfile 维护(lockFileMaintenance):
upg.recreateClosed = upg.recreateWhen !== 'never'——只要没配置never,默认(auto)即视为 Immortal; - 分组后目标版本不唯一(
toVersions.length > 1 && toValues.size > 1 && newValue.length > 1):upgrade.recreateClosed = upgrade.recreateWhen !== 'never'——这正是本文档描述的"分组更新不同版本导致不可忽略"场景,源码中对应地将其判定为 Immortal,除非显式配置never。
可见,文档中关于"分组后标题退化为 Update react (major) 因而不可安全忽略"的论述,与源码中"多版本分组 → 强制 recreateClosed"的判定逻辑完全一致。
recreateWhen配置项详解
控制 Immortal 行为的关键配置是recreateWhen,定义于 lib/config/options/index.ts:
| 属性 | 值 |
|---|---|
| 名称 | recreateWhen |
| 描述 | "Recreate PRs even if same ones were closed previously."(即使之前关闭过相同 PR 也重新创建) |
| 类型 | string |
| 默认值 | auto |
| 允许值 | auto、always、never |
三个取值的行为差异:
auto(默认):智能判定。普通唯一版本 PR 被关闭后正常忽略;但对于 lockfile 维护、目标版本不唯一的分组等"无法安全忽略"的场景,自动表现为 Immortal;always:无论何种情况,关闭后一律重建 PR(所有升级都会变成 Immortal);never:彻底关闭 Immortal 行为,关闭后的 PR 不再重建。
顺带说明,旧版配置recreateClosed已被迁移为recreateWhen,迁移逻辑见 lib/config/migrations/custom/recreate-closed-migration.ts。
未来规划
文档中提到的未来方向是:在 PR 中嵌入元数据,精确标识该 PR 包含的具体文件、包与版本。这样一来,Renovate 就可以安全地允许此类 PR 被关闭/忽略,同时只要其中的文件、包或版本有任何更新的可能,就"缓存失效"并创建新 PR——从"基于标题猜测"升级为"基于内容精确判断"。
此外,文档给出了一条实用建议:如果你经常需要关闭 Immortal PR,这本身就是一个信号,说明你的分组可能过于宽泛。频繁关闭 Immortal PR 不是长久之计,真正要做的是调整分组策略。
如何修复 Immortal PR
针对 Immortal PR,文档给出了三条可落地的修复建议:
1. 避免分组版本不同步的依赖。不要把版本各不相同、或者你很可能想要忽略的依赖强行分到一组。分组只应面向"升级节奏一致、版本可同步"的依赖集合。正如 lib/workers/repository/updates/generate.ts 所展示的,一旦组内出现多个目标版本,标题就失去唯一性,PR 就会被标记为 Immortal。
2. 对确实希望永久保持关闭的 Immortal PR,设置"recreateWhen": "never"。这是一个全局(或按 packageRules 作用域)配置:
{ "recreateWhen": "never", }设置后,关闭的 PR 将不再被重建(对应源码中upgrade.recreateClosed = upgrade.recreateWhen !== 'never'分支的判定结果)。
3. Major 更新走 Dependency Dashboard 审批。除非 major 升级的对象是相互关联的依赖,否则应避免把多个 major 升级分组在一起。更稳妥的做法是让 major 更新进入依赖仪表盘(Dependency Dashboard)人工审批:
{ "packageRules": [ { "matchUpdateTypes": ["major"], "dependencyDashboardApproval": true, } ], }通过"dependencyDashboardApproval": true,major 更新不会直接创建 PR,而是先出现在 Dependency Dashboard 上等待你批准,从而让你对"何时创建 major PR"拥有完全的控制权,从根源上避免产生需要反复关闭的 major 分组 PR。关于 Dashboard 的完整用法,可参考 dashboard.md。
忽略 PR 的正确姿势
关闭一个 Renovate PR,就是忽略它。这是 Renovate 最基础也最重要的交互约定:
- 对于普通 PR(标题含唯一版本):关闭后,该版本的更新不会再出现;
- 对于 Immortal PR:关闭后,它会在下次 Renovate 运行时再次出现,因此关闭无法真正忽略 Immortal PR。
要真正忽略 Immortal PR,请回到上文 如何修复 Immortal PR 一节:要么调整分组避免产生 Immortal PR,要么显式设置"recreateWhen": "never",要么为 major 分组开启dependencyDashboardApproval人工把关。正确理解"关闭即忽略"的边界,是驾驭 Renovate PR 工作流的关键。
【免费下载链接】renovateHome of the Renovate CLI: Cross-platform Dependency Automation by Mend.io项目地址: https://gitcode.com/GitHub_Trending/re/renovate
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考