Renovate 的 semver-coerced 版本方案:用宽松语义化版本识别非标准版本号
【免费下载链接】renovateHome of the Renovate CLI: Cross-platform Dependency Automation by Mend.io项目地址: https://gitcode.com/GitHub_Trending/re/renovate
Renovate 在识别依赖版本时默认使用semver-coerced(Coerced Semantic Versioning)版本方案,它是 Semantic Versioning 2.0 的一个"宽容变体":能够把v1、2.1、~3.1、1.1-foo这类非严格 SemVer 的输入强制类型转换(coerce)为严格的 SemVer 版本。本文以仓库中的方案说明文档 readme.md 为主体,结合其完整实现 lib/modules/versioning/semver-coerced/index.ts 与测试 lib/modules/versioning/semver-coerced/index.spec.ts,讲清楚它的转换规则、每个 API 函数的实际行为、它作为 Renovate 默认版本方案的使用场景,以及与严格semver方案的差异边界。
一、什么是 semver-coerced:宽松转换规则
根据方案文档的定义,Renovate 的 Coerced Semantic Versioning 是一个启用了版本转换(coercion)的 SemVer 2.0 宽容变体。它把非严格 SemVer 格式的版本输入非常宽容地翻译为严格 SemVer,官方文档给出的典型转换示例如下:
| 原始输入 | 转换后(coerced) | 说明 |
|---|---|---|
v1 | 1.0.0 | 前缀v+ 仅主版本号 |
2.1 | 2.1.0 | 缺少补丁号(patch) |
~3.1 | 3.1.0 | 携带范围前缀~ |
1.1-foo | 1.1.0 | 预发布后缀 + 缺少补丁号 |
转换规则的具体语义来自 npmsemver包的 Coercion 机制:从字符串中提取数字序列,补齐缺失的 minor/patch 段为0,忽略前缀字符与常见范围符。
文档同时给出了一条重要边界:由于该方案非常宽容,它并不会对版本范围(range)本身做转换。范围相关的函数只接受严格的 SemVer 作为输入,其范围行为与 Renovate 的semver版本方案完全等价。也就是说,"宽容"只作用于版本号一侧,而^1.0.0、~1.2.3这样的 range 表达必须严格合法。这一边界在源码中体现为模块导出的supportsRanges = false常量(index.ts),并向下游声明它不提供独立的范围计算能力。
二、与严格 semver 方案的对比
Renovate 仓库内置了严格版semver方案(lib/modules/versioning/semver/index.ts),两者 API 结构相同,区别集中在"严格输入"与"coerce 输入":
- 严格
semver方案直接复用semver包的valid、compare、maxSatisfying等函数,输入必须是标准 SemVer 字符串,17.04.0这类非常规写法会直接判定无效; semver-coerced方案的每个函数在调用semver包之前,都会先执行semver.coerce()把输入转换一遍。
从 semver/index.ts 中可以看到一条值得注意的注释:"If this is left as an alias, inputs like '17.04.0' throw errors"(如果直接作为别名使用,17.04.0这类输入会抛错)。semver-coerced沿用了同样的处理方式——isVersion不是简单转引isValid,而是显式定义为isValid(input)(即"coerce 之后能否得到合法 SemVer"),从而避免非常规版本在解析阶段直接抛异常,而是优雅地返回布尔值。
semver-coerced的isBreaking也复用了严格方案的实现:先把两个版本各自 coerce 为字符串,再委托给 semver 方案的 isBreaking。这意味着破坏性判断遵循 SemVer 的通行约定:主版本号变化算破坏性更新,而 v0.x 阶段的所有 minor 变化(如0.1→0.2.1)以及任何包含不稳定版本号的升级,都被视为潜在破坏性。测试用例 index.spec.ts 印证了这些行为:isBreaking('1.0', '1.0.1')为false,isBreaking('1.0.0', '2')为true,isBreaking('0.1', '0.2.1')为true。
三、源码剖析:各 API 函数如何"宽容"
semver-coerced模块最终导出一个完整的VersioningApi对象(index.ts#L153-L171),覆盖了 types.ts 中定义的版本方案接口。下面按功能分组说明关键实现。
1. 校验类:isValid / isVersion / isSingleVersion
function isValid(version: string): boolean { return !!semver.valid(semver.coerce(version)); }isValid的逻辑是"coerce 之后再valid",因此~1.2.3、^1.2.3、>1.2.3这类范围串会被认为"包含一个合法版本",测试 isValid 用例 确认了这一点;同时它也会正确拒绝 GitHub 仓库引用(如renovatebot/renovate#master)。
isSingleVersion则解决了一个更微妙的问题:coercion 既接受版本也接受范围,所以必须人工判断字符串是否以v或数字开头(index.ts#L109-L119):
const startsWithNumberRegex = regEx(`^\\d`); function isSingleVersion(version: string): boolean { // Since coercion accepts ranges as well as versions, we have to manually // check that the version string starts with either 'v' or a digit. if (!version.startsWith('v') && !startsWithNumberRegex.exec(version)) { return false; } return !!semver.valid(semver.coerce(version)); }测试 isSingleVersion 用例 表明:裸版本1.2.3、带预发布号的1.2.3-alpha.1返回true;而=1.2.3、= 1.2.3、~1.0返回false。该函数供 Renovate 的依赖固定(pinning)功能区分"单版本"与"范围"。
2. 版本拆解类:getMajor / getMinor / getPatch
三个函数实现完全同构,先 coerce 再取对应段位,无法转换时返回null(index.ts#L41-L54):
function getPatch(a: string | SemVer): number | null { const aCoerced = semver.coerce(a); return aCoerced ? semver.patch(aCoerced) : null; }测试表 getPatch 用例 展示了宽容度的具体边界:
| 输入 | getPatch 结果 |
|---|---|
v2.1 | 0(缺失的 patch 补 0) |
v1.0.4-alpha | 4(忽略预发布段取 patch) |
ver1.2.3 | 3(容忍非标准ver前缀) |
two1.0 | 0 |
abc | null(完全无法 coerce) |
3. 比较类:equals / isGreaterThan / sortVersions
这三个函数都是"先 coerce 双方,再交给semver包比较":
equals('v1.0', '1.0.0')返回true,equals('xxx', '1.2.3')返回false(equals 测试);isGreaterThan在任一版本无法 coerce 时保守地返回false,例如isGreaterThan('e.e.e', '4.1.0')为false;sortVersions使用semver.compare,对v1.0与1.x这样的等价非严格版本返回0,对无法转换的版本也返回0而非报错(sortVersions 测试)。
4. 稳定性判断:isStable
isStable是这个方案中唯一没有直接依赖semver.coerce的函数,它先用一条正则提取版本骨架,再用semver-stable判断(index.ts#L14-L32):
function isStable(version: string): boolean { // matching a version with the semver prefix // v1.2.3, 1.2.3, v1.2, 1.2, v1, 1 const regx = regEx( /^v?(?<major>\d+)(?<minor>\.\d+)?(?<patch>\.\d+)?(?<others>.+)?/, ); const m = regx.exec(version); if (!m?.groups) { return false; } const major = m.groups.major; const newMinor = m.groups.minor ?? '.0'; const newPatch = m.groups.patch ?? '.0'; const others = m.groups.others ?? ''; const fixed = major + newMinor + newPatch + others; return stable.is(fixed); }正则^v?(\d+)(\.\d+)?(\.\d+)?(.+)?允许的版本形如v1.2.3、1.2.3、v1.2、1.2、v1、1,随后把缺失的 minor/patch 补.0,把剩余部分(预发布后缀等)拼回,交给semver-stable判断是否为稳定版。测试表 isStable 用例 说明:1.0.0、v1.3.5、v2.1、3.4、v2、2均为稳定;而1.0.0-alpha、1.0.0-rc2、v1.0-alpha等带预发布标识的不是稳定版;two1.0、ver1.2.3、r3.0这类无法匹配该正则前缀的输入直接返回false——注意这里比getPatch更严格,说明"稳定判断"和"版本拆解"使用了不同的宽容尺度。
5. 范围匹配类:matches / getSatisfyingVersion / minSatisfyingVersion / isLessThanRange
这一组函数再次印证了文档中"range 不转换、只接受严格 SemVer"的边界——被转换的永远是 version 一侧,range 保持原样交给semver包:
function matches(version: string, range: string): boolean { const coercedVersion = semver.coerce(version); return coercedVersion ? semver.satisfies(coercedVersion, range) : false; }getSatisfyingVersion与minSatisfyingVersion处理版本列表时会稍作区分:前者对已经是合法 SemVer 的版本保持原样、只对非法版本 coerce(index.ts#L71-L82),后者则统一 coerce。测试 用例 展示:getSatisfyingVersion(['v1.0', '1.0.4-foo'], '^1.0')返回1.0.0——v1.0被 coerce 成1.0.0参与匹配。
6. 值生成:getNewValue
getNewValue处理更新时新值的书写格式,逻辑与严格semver方案一致(index.ts#L128-L137):
function getNewValue({ currentValue, currentVersion, newVersion, }: NewValueConfig): string { if (currentVersion === `v${currentValue}`) { return newVersion.replace(regEx(/^v/), ''); } return newVersion; }即当原始文件里写的是v1.0.0(currentVersion为v1.0.0,currentValue为1.0.0)时,生成的新值会去掉v前缀,与文件既有风格保持一致。测试 getNewValue 用例 中,currentValue: '1.0.0'、currentVersion: 'v1.0.0'、newVersion: 'v1.1.0'时返回1.1.0。
四、semver-coerced 在 Renovate 中的实际地位
1. 全局默认版本方案
从源码结构看,semver-coerced是 Renovate 的默认版本方案:版本方案注册入口 lib/modules/versioning/index.ts 直接将其导出为defaultVersioning:
export const defaultVersioning = semverCoerced;且 lib/modules/datasource/index.spec.ts 中的测试确认getDefaultVersioning(undefined)返回'semver-coerced'。
2. 自定义管理器(customManagers)的兜底方案
官方配置文档 configuration-options.md 说明:当自定义管理器配置中没有versioning字段,且所用数据源没有自带默认版本方案时,Renovate 默认回落到semver-coerced。这也是 customManagers 文档 建议"显式设置 versioning"的原因。regex与jsonata两个自定义管理器的文档同样标注了该默认值,例如 custom/regex 管理器文档:
A
versioningcapture group, or aversioningTemplateconfig field. If neither are present, Renovate defaults tosemver-coerced
3. 指定数据源的默认方案
- gitlab-tags / gitlab-releases:两个数据源文档均声明默认使用
semver-coerced(gitlab-releases/readme.md、gitlab-tags/readme.md),因为 Git tag 经常写成v1、1.0等非标准形式,宽松转换正好适配; - gomod 管理器:lib/modules/manager/gomod/extract.ts 对
gopkg.in风格的依赖显式指定versioning: 'semver-coerced',其对应测试 gomod/extract.spec.ts 中多处以'%goMod': 'semver-coerced'断言提取结果; - typst 数据源:lib/modules/datasource/typst/index.ts 直接导入 semver-coerced 作为其版本方案;
- repology 数据源的文档则给出了一个反例提醒(repology/readme.md):因为发行包版本号常常不符合
semver-coerced的规范,需要根据包类型手动指定版本方案——说明该方案的"宽松"也有边界,并非万能。
4. 作为配置值显式使用
semver-coerced也是一个可以直接写进配置的合法方案 ID。例如在 constraintsVersioning 配置示例 中,用 SemVer 风格(而非 Ruby 风格)的范围来定义约束时:
{ "constraints": { "rubygems": "^1.3" }, "constraintsVersioning": { "rubygems": "semver-coerced" } }此外,same-major 版本方案 也在内部复用 semver-coerced 的 API,可见它是 Renovate 版本体系里的一个基础构件。
五、行为边界与使用建议
综合文档与源码测试,使用semver-coerced时需要注意以下边界:
- 宽容只在版本一侧:
v1、2.1、~3.1等版本号会被转换;但 range(^1.0、>=2等)必须严格合法,模块的supportsRanges = false声明了它不提供范围计算能力,范围相关函数的行为与严格semver方案等价。 - coercion 对范围串也算"有效版本":
isValid('~1.2.3')返回true,因此区分"单版本"与"范围"要依赖isSingleVersion(必须v开头或数字开头)。 - 不同函数宽容尺度不一:
getPatch('ver1.2.3')返回3,而isStable('ver1.2.3')返回false——稳定性判断使用的正则更严格,不能以某一个函数的宽容度推断整体行为。 - 无法转换时优雅降级:
equals、isGreaterThan、matches等在输入无法 coerce 时返回false,getMajor/getMinor/getPatch返回null,sortVersions返回0,整体设计偏向"保守不误判"。 - 破坏性判断复用 SemVer 约定:major 变化算破坏性;v0.x 的所有 minor 变化都算破坏性;不稳定版本参与的升级一律视为潜在破坏性(源自 semver 方案 isBreaking)。
六、小结
semver-coerced是 Renovate 默认启用的版本方案,其本质是在semver包的每个版本判断入口前置一次semver.coerce(),从而让v1、2.1、~3.1这类在 Git tag、发布页、自定义文件中极为常见的非标准版本号进入 Renovate 的比较、匹配与更新流水线。它的实现集中在 lib/modules/versioning/semver-coerced/index.ts 一个文件内(约 170 行),行为边界则由 同目录测试 逐函数锁定。理解它的"版本宽容、范围严格"这一核心不对称性,是正确配置versioning选项、以及排查"为什么我的版本号没被识别"这类问题的关键。
【免费下载链接】renovateHome of the Renovate CLI: Cross-platform Dependency Automation by Mend.io项目地址: https://gitcode.com/GitHub_Trending/re/renovate
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考