
Renovate loose 版本化机制详解无严格版本规范时的兜底解析与排序策略【免费下载链接】renovateHome of the Renovate CLI: Cross-platform Dependency Automation by Mend.io项目地址: https://gitcode.com/GitHub_Trending/re/renovateRenovate 为“没有严格版本规范”的场景专门提供了一套名为looseLoose的版本化versioning模块。当依赖的发布版本不符合 SemVer 或任何已知规范——例如 Docker 镜像 tag、Go 模块的旧式伪版本、日期戳命名等——loose会以“尽力而为”best effort的方式解析和排序版本遇到 SemVer 兼容的版本就按 SemVer 思路处理遇到无法排序的版本则直接忽略。本文基于 loose 模块源码、单元测试 与仓库文档完整拆解它的解析规则、比较算法和实际使用方式帮助你理解何时、如何把它配置为自己的依赖更新策略。一、loose 版本化解决什么问题Renovate 的核心工作流是Manager 模块提取依赖、Datasource 获取版本列表、Versioning 模块对结果进行排序与过滤参见 Versioning 模块总览。对于 npm、Maven、PEP440 这类有统一版本规范生态Renovate 都有对应的专用版本化实现但很多生态根本不存在一致约定——官方文档指出一些 Docker 镜像可能用 SemVer一些用 PEP440一些干脆用 Calendar VersioningVersioning 文档。此时就需要一个“兜底”方案。官方配置文档 对 loose 的定位是Renovate also uses custom versioning, likedockerto address the most common way people tag versions using Docker, andlooseas a fallback that tries SemVer first.即loose是“先尝试 SemVer失败则尽力而为”的兜底版本化。典型使用场景在 Docker 更新指南 中有推荐路径先用默认的docker版本化如果结果不对就改用内置的loose版本化如果loose仍不满足需求再考虑regex自定义版本化。二、模块注册与核心特征loose模块的入口文件 lib/modules/versioning/loose/index.ts 只有 75 行结构非常清晰export const id loose; export const displayName Loose; export const urls []; export const supportsRanges false;四个关键特征值得注意id loose这是配置中versioning: loose所使用的标识符通过 versioning 统一注册表 的get()方法按 id 查表获取实例displayName Loose用于文档与界面展示urls []与 semver、pep440 等模块不同loose 不关联任何外部规范文档链接呼应其“无严格规范”的定位supportsRanges falseloose不支持版本范围如^1.2.0、1.0。它只负责比较两个具体版本所有范围匹配最终退化为“精确相等”判断。整个类继承自 GenericVersioningApi。抽象基类实现了VersioningApi接口的通用逻辑isVersion、isCompatible、equals、isGreaterThan、getMajor/Minor/Patch等均由_parse派生子类只需实现两个核心_parse(version)把版本字符串解析为结构化的GenericVersion解析失败返回null_compare(version, other)返回两个版本的大小比较结果。三、解析规则逐行拆解_parseloose 的解析逻辑集中在 index.ts 第 10-29 行const versionPattern regEx(/^[vV]?(?prefix\d(?:\.\d)*)(?suffix.*)$/); const commitHashPattern regEx(/^[a-f0-9]{7,40}$/); const numericPattern regEx(/^[0-9]$/); class LooseVersioningApi extends GenericVersioningApi { protected _parse(version: string): GenericVersion | null { if (commitHashPattern.test(version) !numericPattern.test(version)) { return null; } const matches versionPattern.exec(version); if (!matches) { return null; } const { prefix, suffix } matches.groups!; const release prefix.split(.).map(Number); if (release.length 6) { return null; } return { release, suffix: suffix || }; }规则可以归纳为四条commit hash 排除形如0a1b2c3这类 740 位纯十六进制字符串会被识别为 git commit hash 并拒绝解析返回null即“unsortable”。唯一例外是全数字的 hash因为20100527这类日期数字串需要保留。测试用例中0a1b2c3、0a1b2c3d4e5f6a7b8c9d0a1b2c3d4e5f6a7b8c9d0均为非法而纯数字的123098140293合法测试第 29-36 行主版本号以数字开头、允许 v/V 前缀versionPattern要求版本以可选的v/V加上一段点分数字开头v1.4、V0.5、3.5.0都能通过完全不以数字开头的字符串如foo直接判非法点分段数不超过 61.2.3.4.5.6.77 段被拒绝而0.6.5.14 段合法。这个上限防止把任意长字符串误当作版本号剩余部分归入suffix点分数字前缀之后的所有内容如-rc2、RC2、.Final、-groovy-2.4原样保留为suffix不做语义解释仅参与后续的字符串比较。解析结果是一个{ release: number[], suffix: string }结构release是点分数字段组成的数组如2.4.100→[2, 4, 100]suffix是尾部原文如beta、-M3。四、比较算法逐行拆解_compare比较逻辑位于 index.ts 第 31-70 行分三步protected override _compare(version: string, other: string): number { const parsed1 this._parse(version); const parsed2 this._parse(other); // 任一版本无法解析则不做比较 if (!(parsed1 parsed2)) { return 1; } // 第 1 步逐段比较 release 数组较短者更小 const length Math.max(parsed1.release.length, parsed2.release.length); for (let i 0; i length; i 1) { const part1 parsed1.release[i]; const part2 parsed2.release[i]; if (part1 undefined) { return -1; } // 2.1 2.1.0 if (part2 undefined) { return 1; } if (part1 ! part2) { return part1 - part2; } } // 第 2 步suffix 双有则按数字感知排序比较 if (parsed1.suffix parsed2.suffix) { return parsed1.suffix.localeCompare(parsed2.suffix, undefined, { numeric: true, }); } // 第 3 步带 suffix 的预发布语义排在正式版之前 if (parsed1.suffix) { return -1; } if (parsed2.suffix) { return 1; } return 0; }关键设计决策有三个“短版本小于长版本”与基类 GenericVersioningApi 将缺失段补 0 的方式不同loose 显式采用2.1 2.1.0源码注释即注明 shorter is smaller。这是 loose 最独特的语义——它不假设2.4与2.4.0等价suffix 数字感知排序localeCompare的{ numeric: true }选项保证1.1.5-99 1.1.5-100测试 第 67 行 验证了该用例不会出现字典序下100 99的常见错误无法解析的版本不参与比较两个版本只要有任意一个解析失败_compare直接返回 1配合上层排序逻辑unsortable 版本实际上被“忽略”掉这正是原始文档中“best effort”语义的实现方式。五、对外行为与测试验证由于大部分公共方法继承自 GenericVersioningApiloose 的对外行为完全由_parse/_compare驱动且因为不支持范围isVersion(v)、isValid(v)、isCompatible(v)、isSingleVersion(v)均等价于“_parse能否成功”getSatisfyingVersion与matches退化为精确相等判断generic.ts 第 110-118 行getNewValue继承基类实现当当前值是v1.2.3而新值是v1.3.0时会去掉新值的v前缀写回保持原文件的书写风格generic.ts 第 120-129 行。测试文件 用表格驱动方式覆盖了核心边界摘出几个有代表性的判定场景输入结果短版本判定1.1、1.3.RC2、2.1-rc2isVersion均为 true超长点分拒绝1.2.3.4.5.6.77 段非法commit hash 拒绝0a1b2c3、0a1b2c3d非法hash 混入大写字母0a1b2C3合法不是纯 hash 形态不等价判定equals(2.4.0, 2.4)false短版本更小的体现数字感知比较isGreaterThan(2.4.100, 2.4.99)true预发布靠后isGreaterThan(2.4.beta, 2.4)false日期戳比较2024-07-21T11-33-05.abc1232023-06-21T11-33-05.abc123true最后一行说明 loose 对CalVer日期戳风格同样有效日期戳被解析为 6 段以内的点分数字前缀自然按数值先后排序。六、仓库中 loose 的实际使用位置从源码结构看loose 主要作为数据源与 manager 的默认或回退版本化使用endoflife-date 数据源 将defaultVersioning直接设为loose因为该产品生命周期数据库中记录的产品版本风格差异极大gomod manager 在处理 Go 模块时对非 SemVer 形态的伪版本如v0.0.0-20190227155436-...这类 commit 时间戳版本会将dep.versioning标记为loose进行排序devbox manager 的工具版本处理 也直接引入 loose 模块参与版本比较azure-pipelines-tasks 数据源 引用 loose 的 id 作为其任务版本的比较方案。这些引用印证了文档的定位loose 不是给某个特定生态定制的而是所有“版本格式无法归类”场景的公共兜底。七、如何在配置中启用 loose当默认版本化对某个依赖的排序结果不符合预期时按 Versioning 文档 的建议推荐通过packageRules按包名精确覆盖而不是按 manager 或数据源做全局替换例如让某个 Docker 镜像改用 loose 排序{ packageRules: [ { matchDatasources: [docker], matchPackageNames: [my-org/my-image], versioning: loose } ] }配置时需要注意两个约束loose 不支持范围语法supportsRanges false如果你依赖的是1.0、^2.3这类约束loose 无法表达范围匹配应考虑语义更明确的semver、pep440等版本化不要与matchUpdateTypes组合在同一条规则中官方文档明确提醒versioning生效时 update type 尚未确定Versioning 文档。八、小结loose 是 Renovate 的兜底版本化先尝试按“数字前缀 尾部 suffix”解析能解析就尽力排序解析不了就忽略它的关键判定是 commit hash 排除、最多 6 段点分数字、v/V前缀容忍比较时“短版本更小”、suffix 用数字感知排序它不支持版本范围所有匹配退化为精确相等适合精确版本pin依赖典型启用方式是packageRules中对特定包设置versioning: loose常作为 Docker 等无统一版本规范生态的第二选择排在专用版本化如docker之后、regex自定义方案之前。【免费下载链接】renovateHome of the Renovate CLI: Cross-platform Dependency Automation by Mend.io项目地址: https://gitcode.com/GitHub_Trending/re/renovate创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考