news 2026/9/13 3:31:30

Renovate same-major 版本策略:把“同一大版本内自动滚动“的依赖交给 rollForward,让机器人只管跨 major 升级

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Renovate same-major 版本策略:把“同一大版本内自动滚动“的依赖交给 rollForward,让机器人只管跨 major 升级

Renovate same-major 版本策略:把"同一大版本内自动滚动"的依赖交给 rollForward,让机器人只管跨 major 升级

【免费下载链接】renovateHome of the Renovate CLI: Cross-platform Dependency Automation by Mend.io项目地址: https://gitcode.com/GitHub_Trending/re/renovate

在 Renovate 中,大多数依赖都遵循语义化版本(Semver)的升级逻辑:patch、minor 各升一档,major 单独处理。但有一类特殊场景——比如 .NET SDK 的global.json配置了rollForward: "major",运行时会自动在同一 major 范围内选用最新可用版本。这时如果 Renovate 对>= 6.0.300 < 7.0.0范围内的每一个新版都开 PR,只会产生大量"改了也没实际效果"的噪音 PR。Renovate 为此提供了same-major版本策略:把一个具体版本X.Y.Z视为"大于等于X.Y.Z、小于X+1.0.0"的隐式区间,从而让 Renovate 只在跨 major 时提议更新。读完本文,你将理解该策略的底层实现(如何在 semver-coerced 之上做版本改写)、每个核心 API 的行为差异、以及如何用packageRules把它配置到 dotnet-sdk 这类场景上。

解决什么问题:版本本身就是一个"隐式区间"

same-major策略的设计目标是处理"某个版本值实际上代表一个兼容范围"的情况:当配置中写的是X.Y.Z,它真正表达的语义是>= X.Y.Z < X+1.0.0——即从该版本起、到下一个 major 之前的所有版本都兼容。

这一策略最典型的应用场景是 dotnet-sdk 的rollForward配置。例如一个项目的global.json中:

{ "sdk": { "version": "6.0.300", "rollForward": "major" } }

rollForward是 .NET SDK 选择 SDK 版本时的回退/向前策略:这里取值为major表示"选用同一 major 下的最新版本",等价于区间约束>= 6.0.300 < 7.0.0。既然管理器本身(dotnet SDK 解析器)已经会在6.x.x范围内自动滚动到最新版,Renovate 就不应该在6.0.3016.0.400这类同 major 更新上创建 PR——那不会改变项目实际行为;Renovate 真正有价值的更新提议,是7.x.x这种跨 major 的升级。

Renovate 的 NuGet 管理器确实会解析global.json中的这类结构,其 schema 定义见 nuget/schema.ts,其中rollForward字段即对应 SDK 版本滚动策略。

实现原理:在 semver-coerced 之上做"版本改写"

same-major的实现非常克制:它不是从零写一套比较逻辑,而是底层复用 semver-coerced 版本策略,只在入口处把单版本改写成区间。核心实现在 same-major/index.ts:

export const id = 'same-major'; export const displayName = 'Same Major Versioning'; export const supportsRanges = false; /** * Converts input to range if it's a version. eg. X.Y.Z -> '>=X.Y.Z <X+1' * If the input is already a range, it returns the input. */ function massageVersion(input: string): string { // istanbul ignore if: same-major versioning should not be used with ranges as it defeats the purpose if (!semverCoerced.isSingleVersion(input)) { logger.warn( { version: input }, 'Same major versioning expects a single version but got a range. Please switch to a different versioning as this may lead to unexpected behaviour.', ); return input; } // we are sure to get a major because of the isSingleVersion check const major = semverCoerced.getMajor(input)!; return `>=${input} <${major + 1}`; }

可以拆出三个关键点:

  1. supportsRanges = false声明。该策略声明自身不支持区间输入——它的语义预设就是"输入是一个具体版本号"。
  2. massageVersion是唯一的魔法入口。它先用 semver-coerced 的isSingleVersion判断输入是否为单版本(该判断要求字符串以v或数字开头且可被 semver 强转,实现在 semver-coerced/index.ts 的isSingleVersion);若是,则取出 major 并拼出>=X.Y.Z <X+1这样的 semver 区间,再委托给 semver-coerced 的对应方法。例如6.0.300会被改写为>=6.0.300 <7,交给semver.satisfies一类函数去计算。
  3. 误用即告警。如果传入的其实是一个区间(例如^6.0.0),massageVersion会原样透传并打印一条logger.warn,提示"same-major 期望单版本输入,请改用其他 versioning"——即源码注释里说的"用区间输入会失去该策略存在的意义"。

最终的导出 API 采用展开继承 + 定向覆盖的方式:

export const api: VersioningApi = { ...semverCoerced, matches, getSatisfyingVersion, minSatisfyingVersion, isLessThanRange, isGreaterThan, };

也就是说,isValidisVersiongetMajorsortVersionsgetNewValue等全部直接复用 semver-coerced 的行为(包括对非标准版本号如6.0.300v6.0的强转能力),只有与"区间语义"和"跨 major 判断"相关的 5 个方法被覆盖:

被覆盖的方法行为
matches(version, range)range改写为>=range <major+1后调用 semver-coerced 的matches,回答"version是否落在range的同 major 区间内"
getSatisfyingVersion(versions, range)返回给定版本列表中满足该同 major 区间的最高版本
minSatisfyingVersion(versions, range)返回满足该区间的最低版本
isLessThanRange(version, range)回答"version是否严格小于该同 major 区间"
isGreaterThan(version, other)重写为仅比较 major:major 更大才为true,同 major 一律视为"不大于"(相等)

其中isGreaterThan的重写值得单独说明——这是"same-major"语义最直接的体现:

// for same major versioning one version is greater than the other if its minor is greater function isGreaterThan(version: string, other: string): boolean { const versionMajor = semverCoerced.getMajor(version)!; const otherMajor = semverCoerced.getMajor(other)!; if (!versionMajor || !otherMajor) { return false; } return versionMajor > otherMajor; }

注意这与 semver-coerced 原始实现(比较完整的 semver 值)不同:在 same-major 视角下,3.1.0并不"大于"3.0.0,因为二者同 major,视为同一兼容域。任一方无法解析出 major 时保守返回false

用测试用例核对行为边界

same-major/index.spec.ts 用少量用例把上述语义钉死了,可以直接对照理解各方法的判定边界:

isGreaterThan——只看 major:

expect(sameMajor.isGreaterThan('4.0.0', '3.0.0')).toBeTrue(); // greater expect(sameMajor.isGreaterThan('2.0.2', '3.1.0')).toBeFalse(); // less expect(sameMajor.isGreaterThan('3.1.0', '3.0.0')).toBeFalse(); // same major -> equal expect(sameMajor.isGreaterThan('3.0.0', '3.0.0')).toBeFalse(); // equal expect(sameMajor.isGreaterThan('a', '3.0.0')).toBeFalse(); // invalid versions

matches——同 major 且不低于该版本才算命中:

expect(sameMajor.matches('1.0.1', '1.0.0')).toBeTrue(); // 同 major、更高 expect(sameMajor.matches('1.0.0', '1.0.0')).toBeTrue(); // 边界包含 expect(sameMajor.matches('2.0.1', '1.0.0')).toBeFalse(); // 跨 major expect(sameMajor.matches('1.2.3', '1.2.4')).toBeFalse(); // 同 major 但更低,落在 >= 下界之外 expect(sameMajor.matches('1.0.0', 'xxx')).toBeFalse(); // 非法 range

最后一条1.2.3vs1.2.4的用例最能说明区间改写的意义:它不是"同 major 即匹配",而是完整执行>=1.2.4 <2的语义,低于起点的版本同样被排除。

getSatisfyingVersion/minSatisfyingVersion——在版本池中取区间极值:

expect( sameMajor.getSatisfyingVersion(['1.0.0', '1.0.4', '1.3.0', '2.0.0'], '1.0.3'), ).toBe('1.3.0'); // 区间 [1.0.3, 2.0.0) 内最高:1.3.0(2.0.0 被 <2 排除) expect( sameMajor.minSatisfyingVersion(['1.0.0', '1.0.4', '1.3.0', '2.0.0'], '1.0.3'), ).toBe('1.0.4'); // 区间内最低:1.0.4

isLessThanRange——是否严格小于整个同 major 区间:

expect(sameMajor.isLessThanRange?.('2.0.2', '3.0.0')).toBeTrue(); // 低于 3.x 区间 expect(sameMajor.isLessThanRange?.('4.0.0', '3.0.0')).toBeFalse(); // 高于区间 expect(sameMajor.isLessThanRange?.('3.1.0', '3.0.0')).toBeFalse(); // 落在区间内部

这些测试共同刻画了同一套判定:把range视为[range, major+1)的左闭右开区间,一切比较都基于该区间展开。

实战配置:为 dotnet-sdk 的 global.json 启用 same-major

该策略已通过 versioning/api.ts 注册进 Renovate 的版本策略模块清单,因此可以在 Renovate 配置中通过versioning: "same-major"指定。以global.json场景为例,用packageRules针对该文件单独指定版本策略,使 Renovate 只在跨 major 时更新:

{ "packageRules": [ { "matchFiles": ["(^|/)global\\.json$"], "versioning": "same-major" } ] }

配合 Renovate 的matchCurrentVersion规则(其底层CurrentVersionMatcher正是调用各 versioning 的matches方法做判定,见 package-rules/current-version.ts,且测试中显式覆盖了versioning: 'same-major'的用例,见 current-version.spec.ts),还可以把规则收敛得更细,例如只在当前 SDK 处于 6.x 时匹配:

{ "packageRules": [ { "matchFiles": ["(^|/)global\\.json$"], "versioning": "same-major", "matchCurrentVersion": "6.0.300" } ] }

此时 Renovate 对global.jsonsdk.version的更新判定逻辑即为:6.0.4xx6.1.x等全部命中>=6.0.300 <7区间而被视为"当前版本已覆盖",不产生 PR;只有7.x.x这样的跨 major 版本才会触发更新。同理,若只想在特定 major 段内启用该策略(比如从 7 升 8 才允许),也可以结合matchCurrentVersion的正则形式按 major 过滤。

配置思路说明:以上 JSON 是依据packageRulesmatchFilesmatchCurrentVersion等 Renovate 标准配置项与same-majormatches语义组合给出的示例,适用于你自行维护的renovate.json;具体字段的完整取值请参考 Renovate 的配置文档。

使用注意:这是实验性支持

原文档(same-major/readme.md)在结尾特别强调:该 versioning 属于实验性支持,启用前建议在社区中先发起讨论,因为可能仍有未覆盖的边界情况。结合源码,可以归纳出三条实操注意事项:

  1. 输入必须是单版本supportsRanges = false,传入区间时massageVersion会原样透传并输出 warn 日志("Same major versioning expects a single version but got a range"),此时判定会退化为对原始区间的 semver 匹配,行为可能与预期不符。
  2. 依赖 semver 强转能力。底层是 semver-coerced,因此6.0v6.0.300这类非完整写法可以工作;完全无法强转的输入(如xxx)会被matches判为falseisGreaterThan同样保守返回false
  3. 同 major 即"相等"。由于isGreaterThan只比较 major,任何影响 Renovate 升级分级(release type 判定、PR 标题等)的链路在该策略下都只区分 major 边界,minor/patch 层面的差异对升级决策是"透明"的——这正是想要的降噪效果,但也意味着该策略不适合用于需要精细区分 minor 的依赖。

参考实现与测试位置

  • 策略主实现:lib/modules/versioning/same-major/index.ts(massageVersionisGreaterThan重写与 API 组装)
  • 单元测试:lib/modules/versioning/same-major/index.spec.ts
  • 底层依赖的 semver-coerced 实现:lib/modules/versioning/semver-coerced/index.ts(isSingleVersionmatchesgetSatisfyingVersion等被复用方法)
  • 版本策略注册入口:lib/modules/versioning/api.ts
  • NuGet 管理器对rollForward字段的 schema 定义:lib/modules/manager/nuget/schema.ts
  • matchCurrentVersion规则匹配器(调用 versioningmatches):lib/util/package-rules/current-version.ts
  • 官方模块说明文档:lib/modules/versioning/same-major/readme.md

【免费下载链接】renovateHome of the Renovate CLI: Cross-platform Dependency Automation by Mend.io项目地址: https://gitcode.com/GitHub_Trending/re/renovate

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/13 3:29:11

GESP四级真题B3870变长编码解析:从位运算到LEB128/varint

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/13 3:17:34

HDMI切换器选购指南:从带宽、EDID到RE辐射,避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华