
Renovate 的 semver-coerced 版本方案用寬松語義化版本識別非標準版本號【免費下載鏈接】renovateHome of the Renovate CLI: Cross-platform Dependency Automation by Mend.io項目地址: https://gitcode.com/GitHub_Trending/re/renovateRenovate 在識別依賴版本時默認使用semver-coercedCoerced 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說明v11.0.0前綴v 僅主版本號2.12.1.0缺少補丁號patch~3.13.1.0攜帶范圍前綴~1.1-foo1.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)為falseisBreaking(1.0.0, 2)為trueisBreaking(0.1, 0.2.1)為true。三、源碼剖析各 API 函數如何寬容semver-coerced模塊最終導出一個完整的VersioningApi對象index.ts#L153-L171覆蓋了 types.ts 中定義的版本方案接口。下面按功能分組說明關鍵實現。1. 校驗類isValid / isVersion / isSingleVersionfunction 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-L119const 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 再取對應段位無法轉換時返回nullindex.ts#L41-L54function getPatch(a: string | SemVer): number | null { const aCoerced semver.coerce(a); return aCoerced ? semver.patch(aCoerced) : null; }測試表 getPatch 用例 展示了寬容度的具體邊界輸入getPatch 結果v2.10缺失的 patch 補 0v1.0.4-alpha4忽略預發布段取 patchver1.2.33容忍非標準ver前綴two1.00abcnull完全無法 coerce3. 比較類equals / isGreaterThan / sortVersions這三個函數都是先 coerce 雙方再交給semver包比較equals(v1.0, 1.0.0)返回trueequals(xxx, 1.2.3)返回falseequals 測試isGreaterThan在任一版本無法 coerce 時保守地返回false例如isGreaterThan(e.e.e, 4.1.0)為falsesortVersions使用semver.compare對v1.0與1.x這樣的等價非嚴格版本返回0對無法轉換的版本也返回0而非報錯sortVersions 測試。4. 穩定性判斷isStableisStable是這個方案中唯一沒有直接依賴semver.coerce的函數它先用一條正則提取版本骨架再用semver-stable判斷index.ts#L14-L32function 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 的版本保持原樣、只對非法版本 coerceindex.ts#L71-L82后者則統一 coerce。測試 用例 展示getSatisfyingVersion([v1.0, 1.0.4-foo], ^1.0)返回1.0.0——v1.0被 coerce 成1.0.0參與匹配。6. 值生成getNewValuegetNewValue處理更新時新值的書寫格式邏輯與嚴格semver方案一致index.ts#L128-L137function getNewValue({ currentValue, currentVersion, newVersion, }: NewValueConfig): string { if (currentVersion v${currentValue}) { return newVersion.replace(regEx(/^v/), ); } return newVersion; }即當原始文件里寫的是v1.0.0currentVersion為v1.0.0currentValue為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 直接將其導出為defaultVersioningexport 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 管理器文檔Aversioningcapture group, or aversioningTemplateconfig field. If neither are present, Renovate defaults tosemver-coerced3. 指定數據源的默認方案gitlab-tags / gitlab-releases兩個數據源文檔均聲明默認使用semver-coercedgitlab-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 時返回falsegetMajor/getMinor/getPatch返回nullsortVersions返回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選項、以及排查為什么我的版本號沒被識別這類問題的關鍵?!久赓M下載鏈接】renovateHome of the Renovate CLI: Cross-platform Dependency Automation by Mend.io項目地址: https://gitcode.com/GitHub_Trending/re/renovate創作聲明:本文部分內容由AI輔助生成(AIGC),僅供參考