語義版本控制 2.0.0

語義版本控制 2.0.0 | 語義版本控制

語義化版本 2.0.0

摘要

版本格式:主版本號.次版本號.修訂號,版本號遞增規則如下:

  1. 主版本號:當你做了不兼容的 API 修改,
  2. 次版本號:當你做了向下兼容的功能性新增,
  3. 修訂號:當你做了向下兼容的問題修正。

先行版本號及版本編譯信息可以加到“主版本號.次版本號.修訂號”的後面,作爲延伸。

簡介

在軟件管理的領域裏存在着被稱作“依賴地獄”的死亡之谷,系統規模越大,加入的包越多,你就越有可能在未來的某一天發現自己已深陷絕望之中。

在依賴高的系統中發佈新版本包可能很快會成爲噩夢。如果依賴關係過高,可能面臨版本控制被鎖死的風險(必須對每一個依賴包改版才能完成某次升級)。而如果依賴關係過於鬆散,又將無法避免版本的混亂(假設兼容於未來的多個版本已超出了合理數量)。當你項目的進展因爲版本依賴被鎖死或版本混亂變得不夠簡便和可靠,就意味着你正處於依賴地獄之中。

作爲這個問題的解決方案之一,我提議用一組簡單的規則及條件來約束版本號的配置和增長。這些規則是根據(但不侷限於)已經被各種封閉、開放源碼軟件所廣泛使用的慣例所設計。爲了讓這套理論運作,你必須先有定義好的公共 API。這可能包括文檔或代碼的強制要求。無論如何,這套 API 的清楚明瞭是十分重要的。一旦你定義了公共 API,你就可以透過修改相應的版本號來向大家說明你的修改。考慮使用這樣的版本號格式:X.Y.Z(主版本號.次版本號.修訂號)修復問題但不影響 API 時,遞增修訂號;API 保持向下兼容的新增及修改時,遞增次版本號;進行不向下兼容的修改時,遞增主版本號。

我稱這套系統爲“語義化的版本控制”,在這套約定下,版本號及其更新方式包含了相鄰版本間的底層代碼和修改內容的信息。

語義化版本控制規範(SemVer)

以下關鍵詞 MUST、MUST NOT、REQUIRED、SHALL、SHALL NOT、SHOULD、SHOULD NOT、 RECOMMENDED、MAY、OPTIONAL 依照 RFC 2119 的敘述解讀。

  1. 使用語義化版本控制的軟件必須(MUST)定義公共 API。該 API 可以在代碼中被定義或出現於嚴謹的文檔內。無論何種形式都應該力求精確且完整。

  2. 標準的版本號必須(MUST)採用 X.Y.Z 的格式,其中 X、Y 和 Z 爲非負的整數,且禁止(MUST NOT)在數字前方補零。X 是主版本號、Y 是次版本號、而 Z 爲修訂號。每個元素必須(MUST)以數值來遞增。例如:1.9.1 -> 1.10.0 -> 1.11.0。

  3. 標記版本號的軟件發行後,禁止(MUST NOT)改變該版本軟件的內容。任何修改都必須(MUST)以新版本發行。

  4. 主版本號爲零(0.y.z)的軟件處於開發初始階段,一切都可能隨時被改變。這樣的公共 API 不應該被視爲穩定版。

  5. 1.0.0 的版本號用於界定公共 API 的形成。這一版本之後所有的版本號更新都基於公共 API 及其修改內容。

  6. 修訂號 Z(x.y.Z | x > 0)必須(MUST)在只做了向下兼容的修正時才遞增。這裏的修正指的是針對不正確結果而進行的內部修改。

  7. 次版本號 Y(x.Y.z | x > 0)必須(MUST)在有向下兼容的新功能出現時遞增。在任何公共 API 的功能被標記爲棄用時也必須(MUST)遞增。也可以(MAY)在內部程序有大量新功能或改進被加入時遞增,其中可以(MAY)包括修訂級別的改變。每當次版本號遞增時,修訂號必須(MUST)歸零。

  8. 主版本號 X(X.y.z | X > 0)必須(MUST)在有任何不兼容的修改被加入公共 API 時遞增。其中可以(MAY)包括次版本號及修訂級別的改變。每當主版本號遞增時,次版本號和修訂號必須(MUST)歸零。

  9. 先行版本號可以(MAY)被標註在修訂版之後,先加上一個連接號再加上一連串以句點分隔的標識符來修飾。標識符必須(MUST)由 ASCII 字母數字和連接號 [0-9A-Za-z-] 組成,且禁止(MUST NOT)留白。數字型的標識符禁止(MUST NOT)在前方補零。先行版的優先級低於相關聯的標準版本。被標上先行版本號則表示這個版本並非穩定而且可能無法滿足預期的兼容性需求。範例:1.0.0-alpha、1.0.0-alpha.1、1.0.0-0.3.7、1.0.0-x.7.z.92。

  10. 版本編譯信息可以(MAY)被標註在修訂版或先行版本號之後,先加上一個加號再加上一連串以句點分隔的標識符來修飾。標識符必須(MUST)由 ASCII 字母數字和連接號 [0-9A-Za-z-] 組成,且禁止(MUST NOT)留白。當判斷版本的優先層級時,版本編譯信息可(SHOULD)被忽略。因此當兩個版本只有在版本編譯信息有差別時,屬於相同的優先層級。範例:1.0.0-alpha+001、1.0.0+20130313144700、1.0.0-beta+exp.sha.5114f85。

  11. 版本的優先層級指的是不同版本在排序時如何比較。

    1. 判斷優先層級時,必須(MUST)把版本依序拆分爲主版本號、次版本號、修訂號及先行版本號後進行比較(版本編譯信息不在這份比較的列表中)。

    2. 由左到右依序比較每個標識符,第一個差異值用來決定優先層級:主版本號、次版本號及修訂號以數值比較。

      例如:1.0.0 < 2.0.0 < 2.1.0 < 2.1.1。

    3. 當主版本號、次版本號及修訂號都相同時,改以優先層級比較低的先行版本號決定。

      例如:1.0.0-alpha < 1.0.0。

    4. 有相同主版本號、次版本號及修訂號的兩個先行版本號,其優先層級必須(MUST)透過由左到右的每個被句點分隔的標識符來比較,直到找到一個差異值後決定:

      1. 只有數字的標識符以數值高低比較。

      2. 有字母或連接號時則逐字以 ASCII 的排序來比較。

      3. 數字的標識符比非數字的標識符優先層級低。

      4. 若開頭的標識符都相同時,欄位比較多的先行版本號優先層級比較高。

      例如:1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0。

合法語義化版本的巴科斯範式語法

<valid semver> ::= <version core>
                 | <version core> "-" <pre-release>
                 | <version core> "+" <build>
                 | <version core> "-" <pre-release> "+" <build>

<version core> ::= <major> "." <minor> "." <patch>

<major> ::= <numeric identifier>

<minor> ::= <numeric identifier>

<patch> ::= <numeric identifier>

<pre-release> ::= <dot-separated pre-release identifiers>

<dot-separated pre-release identifiers> ::= <pre-release identifier>
                                          | <pre-release identifier> "." <dot-separated pre-release identifiers>

<build> ::= <dot-separated build identifiers>

<dot-separated build identifiers> ::= <build identifier>
                                    | <build identifier> "." <dot-separated build identifiers>

<pre-release identifier> ::= <alphanumeric identifier>
                           | <numeric identifier>

<build identifier> ::= <alphanumeric identifier>
                     | <digits>

<alphanumeric identifier> ::= <non-digit>
                            | <non-digit> <identifier characters>
                            | <identifier characters> <non-digit>
                            | <identifier characters> <non-digit> <identifier characters>

<numeric identifier> ::= "0"
                       | <positive digit>
                       | <positive digit> <digits>

<identifier characters> ::= <identifier character>
                          | <identifier character> <identifier characters>

<identifier character> ::= <digit>
                         | <non-digit>

<non-digit> ::= <letter>
              | "-"

<digits> ::= <digit>
           | <digit> <digits>

<digit> ::= "0"
          | <positive digit>

<positive digit> ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"

<letter> ::= "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J"
           | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T"
           | "U" | "V" | "W" | "X" | "Y" | "Z" | "a" | "b" | "c" | "d"
           | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n"
           | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x"
           | "y" | "z"

爲什麼要使用語義化的版本控制?

這並不是一個新的或者革命性的想法。實際上,你可能已經在做一些近似的事情了。問題在於只是“近似”還不夠。如果沒有某個正式的規範可循,版本號對於依賴的管理並無實質意義。將上述的想法命名並給予清楚的定義,讓你對軟件使用者傳達意向變得容易。一旦這些意向變得清楚,彈性(但又不會太彈性)的依賴規範就能達成。

舉個簡單的例子就可以展示語義化的版本控制如何讓依賴地獄成爲過去。假設有個名爲“救火車”的函數庫,它需要另一個名爲“梯子”並已經有使用語義化版本控制的包。當救火車創建時,梯子的版本號爲 3.1.0。因爲救火車使用了一些版本 3.1.0 所新增的功能,你可以放心地指定依賴於梯子的版本號大於等於 3.1.0 但小於 4.0.0。這樣,當梯子版本 3.1.1 和 3.2.0 發佈時,你可以將直接它們納入你的包管理系統,因爲它們能與原有依賴的軟件兼容。

作爲一位負責任的開發者,你理當確保每次包升級的運作與版本號的表述一致。現實世界是複雜的,我們除了提高警覺外能做的不多。你所能做的就是讓語義化的版本控制爲你提供一個健全的方式來發行以及升級包,而無需推出新的依賴包,節省你的時間及煩惱。

如果你對此認同,希望立即開始使用語義化版本控制,你只需聲明你的函數庫正在使用它並遵循這些規則就可以了。請在你的 README 文件中保留此頁鏈接,讓別人也知道這些規則並從中受益。

FAQ

在 0.y.z 初始開發階段,我該如何進行版本控制?

最簡單的做法是以 0.1.0 作爲你的初始化開發版本,並在後續的每次發行時遞增次版本號。

如何判斷髮布 1.0.0 版本的時機?

當你的軟件被用於正式環境,它應該已經達到了 1.0.0 版。如果你已經有個穩定的 API 被使用者依賴,也會是 1.0.0 版。如果你很擔心向下兼容的問題,也應該算是 1.0.0 版了。

這不會阻礙快速開發和迭代嗎?

主版本號爲零的時候就是爲了做快速開發。如果你每天都在改變 API,那麼你應該仍在主版本號爲零的階段(0.y.z),或是正在下個主版本的獨立開發分支中。

對於公共 API,若即使是最小但不向下兼容的改變都需要產生新的主版本號,豈不是很快就達到 42.0.0 版?

這是開發的責任感和前瞻性的問題。不兼容的改變不應該輕易被加入到有許多依賴代碼的軟件中。升級所付出的代價可能是巨大的。要遞增主版本號來發行不兼容的改版,意味着你必須爲這些改變所帶來的影響深思熟慮,並且評估所涉及的成本及效益比。

爲整個公共 API 寫文檔太費事了!

爲供他人使用的軟件編寫適當的文檔,是你作爲一名專業開發者應盡的職責。保持項目高效的一個非常重要的部分是掌控軟件的複雜度,如果沒有人知道如何使用你的軟件或不知道哪些函數的調用是可靠的,要掌控複雜度會是困難的。長遠來看,使用語義化版本控制以及對於公共 API 有良好規範的堅持,可以讓每個人及每件事都運行順暢。

萬一不小心把一個不兼容的改版當成了次版本號發行了該怎麼辦?

一旦發現自己破壞了語義化版本控制的規範,就要修正這個問題,併發行一個新的次版本號來更正這個問題並且恢復向下兼容。即使是這種情況,也不能去修改已發行的版本。可以的話,將有問題的版本號記錄到文檔中,告訴使用者問題所在,讓他們能夠意識到這是有問題的版本。

如果我更新了自己的依賴但沒有改變公共 API 該怎麼辦?

由於沒有影響到公共 API,這可以被認定是兼容的。若某個軟件和你的包有共同依賴,則它會有自己的依賴規範,作者也會告知可能的衝突。要判斷改版是屬於修訂等級或是次版等級,是依據你更新的依賴關係是爲了修復問題或是加入新功能。對於後者,我經常會預期伴隨着更多的代碼,這顯然會是一個次版本號級別的遞增。

如果我變更了公共 API 但無意中未遵循版本號的改動怎麼辦呢?(意即在修訂等級的發佈中,誤將重大且不兼容的改變加到代碼之中)

自行做最佳的判斷。如果你有龐大的使用者羣在依照公共 API 的意圖而變更行爲後會大受影響,那麼最好做一次主版本的發佈,即使嚴格來說這個修復僅是修訂等級的發佈。記住, 語義化的版本控制就是透過版本號的改變來傳達意義。若這些改變對你的使用者是重要的,那就透過版本號來向他們說明。

我該如何處理即將棄用的功能?

棄用現存的功能是軟件開發中的家常便飯,也通常是向前發展所必須的。當你棄用部分公共 API 時,你應該做兩件事:(1)更新你的文檔讓使用者知道這個改變,(2)在適當的時機將棄用的功能透過新的次版本號發佈。在新的主版本完全移除棄用功能前,至少要有一個次版本包含這個棄用信息,這樣使用者才能平順地轉移到新版 API。

語義化版本對於版本的字符串長度是否有限制呢?

沒有,請自行做適當的判斷。舉例來說,長到 255 個字符的版本已過度誇張。再者,特定的系統對於字符串長度可能會有他們自己的限制。

“v1.2.3” 是一個語義化版本號嗎?

“v1.2.3” 並不是一個語義化的版本號。但是,在語義化版本號之前增加前綴 “v” 是用來表示版本號的常用做法。在版本控制系統中,將 “version” 縮寫爲 “v” 是很常見的。比如:git tag v1.2.3 -m "Release version 1.2.3" 中,“v1.2.3” 表示標籤名稱,而 “1.2.3” 是語義化版本號。

是否有推薦的正則表達式用以檢查語義化版本號的正確性?

有兩個推薦的正則表達式。第一個用於支持按組名稱提取的語言(PCRE[Perl 兼容正則表達式,比如 Perl、PHP 和 R]、Python 和 Go)。

參見:https://regex101.com/r/Ly7O1x/3/

^(?P<major>0|[1-9]\d*)\.(?P<minor>0|[1-9]\d*)\.(?P<patch>0|[1-9]\d*)(?:-(?P<prerelease>(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P<buildmetadata>[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$

第二個用於支持按編號提取的語言(與第一個對應的提取項按順序分別爲:major、minor、patch、prerelease、buildmetadata)。主要包括 ECMA Script(JavaScript)、PCRE(Perl 兼容正則表達式,比如 Perl、PHP 和 R)、Python 和 Go。 參見:https://regex101.com/r/vkijKf/1/

 
 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章