http://jiongks.name/blog/semantic-versioning/
[譯]語義化版本管理
譯自:語義化版本管理 2.0.0
摘要
對於一個給定的版本號 MAJOR.MINOR.PATCH (主、次、補丁),其變化的規律是:
- MAJOR version (主版本) 會在 API 發生不可向下兼容的改變時增大。
- MINOR version (次版本) 會在有向下兼容的新功能加入時增大。
- PATCH version (補丁版本) 會在bug以向下兼容的方式被修復時增大。
我們還可以根據預發佈、構建元數據 (build metadata) 的實際需求,在 MAJOR.MINOR.PATCH 格式之上擴展出額外的標記。
介紹
在軟件管理領域,存在一個叫做“dependency hell (依賴地獄)”的坑。隨着系統越變越大,你集成了越多的軟件包,也越發覺得,有一天,你會陷入絕望。
對於有很多依賴關係的系統來說,發佈新版本的軟件包會迅速變成一場噩夢。如果依賴性規定得太緊,你會陷入 version lock (版本鎖,即每次軟件包的升級無法產生新的版本)。如果依賴性規定得太鬆,你會不可避免的面對 version promiscuity (版本氾濫,假設未來版本是需要考慮兼容性的)。當 version lock 和 version promiscuity 讓你的項目無法安全而又輕鬆的向前推進時,這就是所謂的 dependency hell。
作爲一種解決問題的辦法,我提出了一套簡單的規則和要求來表明版本號該如何確定和增加。這套規則基於但不僅限用於已經廣泛存在的開源閉源軟件的一般實踐。爲了讓這個系統工作起來,你首先需要聲明一個公有的 API,它可以由文檔組成或在代碼層面強制實現,且必須是清晰準確的。一旦你標識了你的公有 API,你就可以通過不同的版本號的增加來交流 API 的各種改變。設想一個形如 X.Y.Z 的版本,不影響 API 的 bug 修復會增大補丁版本,向下兼容的 API 增加或改變會增大次版本,而不兼容的 API 改變會增大主版本。
我把這套系統稱作“語義化版本管理”。在這套系統之下,版本號及其改變傳遞了代碼背後的含義,以及每個相鄰版本之間的變化。
語義化版本管理規範 (SemVer)
原文中的關鍵字 "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" (必須、禁止、要求、應該、不應該、推薦、可以、可選的) 在 RFC 2119 中有相應的解釋和描述。
- 使用語義化版本管理的軟件必須聲明一個公有 API。該 API 可聲明於代碼或文檔中,並且精確而全面。
- 一個普通的版本號必須遵循 X.Y.Z 的格式,其中 X、Y、Z 都是非負整數,禁止包含前置的 0。X 是主版本,Y 是次版本,Z 是補丁版本。每個元素必須以數字形式變大。比如:1.9.0 -> 1.10.0 -> 1.11.0。
- 一旦版本化的軟件包被髮布,那麼該版本的內容就禁止被更改了。今後的任何變化都必須通過新版本的發佈而產生。
- 主版本 0 (0.Y.Z) 表示初始開發階段。在這個階段任何事情都是可以更改的,公有 API 不應該被認爲是穩定的。
- 1.0.0 版本定義了公有 API。從該版本往後,版本號的增加取決於發佈的公有 API 及其變化。
- 只有當向下兼容的 bug 修復被引入時,補丁版本 Z (x.y.Z | x > 0) 必須被增大——bug 修復的定義是通過內部改變修復錯誤的特性。
- 如果新的向下兼容的功能被引入公有 API、如果任何公有 API 功能被廢棄,次版本 Y (x.Y.z | x > 0) 必須被增大。如果顯著的新功能或改進通過私有代碼被引入,次版本可以被增大。次版本的被增大可以包括補丁級別的變化。當次版本被增大時,補丁版本必須被重置爲 0。
- 如果任何不向下兼容的變化被引入時,主版本 X (X.y.z | X > 0) 必須被增大。主版本的被增大可以包括次級別和補丁級別的變化,當主版本被增大時,次版本和補丁版本必須被重置爲 0。
- 一個預發佈版本可以表示爲在補丁版本之後加上一個連字符,再加上一系列由點分隔開的標識符。標識符必須僅由 ACSII 字母數字和連字符組成 [0-9A-Za-z-]。標識符禁止爲空。數字標識符禁止有前置的 0。預發佈版本比相應的普通版本重要性更低。一個預發佈版本意味着該版本不穩定,也許沒有滿足相應版本既定的兼容性需求。比如:1.0.0-alpha、1.0.0-alpha.1、1.0.0-0.3.7、1.0.0-x.7.z.92。
- 構建元數據可以表示爲在補丁版本或預發佈版本之後加上一個加號,再加上一系列由點分隔開的標識符。標識符必須僅由 ASCII 字母數字和連字符組成 [0-9A-Za-z-]。標識符禁止爲空。構建元數據應該在決定版本重要性的時候被忽略。也就是說,如果兩個版本只有構建元數據不一樣,那麼它們的重要性是一樣的。比如:1.0.0-alpha+01、1.0.0+20130313144700、1.0.0-beta+exp.sha.5114f85。
- 重要性用於版本之間在排序時的比較。重要性的計算必須分別通過主、次、補丁、預發佈標識符進行 (構建元數據並不參與重要性比較)。重要性取決於下面從左到右依次比較時出現的第一個不一樣的標識符:主、次、補丁版本,都是數字形式的比較。比如:1.0.0 > 2.0.0 > 2.1.0 > 2.1.1。當主、次、補丁都相同的時候,預發佈版本比普通版本重要性低。比如:1.0.0-alpha < 1.0.0。兩個主、次、補丁版本相同的預發佈版本之間的重要性必須取決於比較每個用點分隔出的標識符,按如下形式從左到右比較出的第一個不同的標識符:純數字的標識符用數字形式進行比較,帶有字母或連字符的標識符用 ASCII 文本形式進行比較。數字標識符重要性低於非數字標識符。如果公有的字段都相同,則字段多的預發佈版本重要性高於字段少的。比如: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。
爲什麼要使用語義化版本管理?
這並不是什麼新的或革命性的東西。事實上你的做事習慣可能已經很接近它了。但問題是“接近”是不夠的。如果不接受一些正式的規範,版本號對依賴性管理是沒有實際意義的。基於上述想法,給定名詞和定義,它就變得易於交流。一旦這些意圖變得清晰,靈活 (且不過分的) 的依賴性規範就會最終產生。
一個簡單的例子就可以向人們展示語義化版本管理會使依賴地獄成爲歷史。想像一個叫做“消防車”的庫,它需要一個名叫“梯子”的經過語義化版本管理的軟件包。當消防車被創建時,梯子的版本是 3.1.0。因爲消防車使用一些在 3.1.0 被首次引入的功能,你可以安全的制定梯子的依賴關係爲大於 3.1.0 且小於 4.0.0。現在當梯子的版本 3.1.1 和 3.2.0 可用時,你可以把它們發佈到你的軟件包管理系統之中並很清楚它們可以和現存的依賴性軟件和平共處。
作爲一個有責任感的開發者,你一定想要驗證任何被公示的軟件包的功能升級。在現實世界中這是一個混亂的地方,我們對此需要警惕但又無能爲力。我們能做的就是讓語義化版本管理提供給你一個清晰的路線,去發佈和升級軟件包,無需在依賴性軟件包的不同版本中翻滾,省去你的時間和煩惱。
如果這一切是你所渴望的,你需要做的就是聲明你開始遵循上述規則來進行語義化的版本管理。在你的 README 中附帶這個網站的鏈接,讓其他人瞭解這個規則,並從中獲益。
問答時間
- 我應該如何處理 0.y.z 的初始化開發階段呢?
最簡單的事情就是當你開始初始化開發時,從 0.1.0 開始,然後在後續的發佈過程中不斷增加次版本。
- 我怎麼知道什麼時候發佈 1.0.0?
如果你的軟件已經用到了產品環境,它可能應該已經是 1.0.0 了。如果你有一個穩定的用戶依賴的 API,它應該是 1.0.0 了。如果你擔心很多向下兼容的問題,它可能應該已經是 1.0.0 了。
- 這和快速開發快速迭代的理念是否有衝突?
主版本 0 是快速開發時期。如果你每天都在改變 API 你應該還處在 0.y.z 或一個獨立的開發分支中,這是爲下一個主版本服務的。
- 如果最小的向下不兼容的公有 API 改變都會導致新的主版本,那麼我豈不是很快就做到 42.0.0 了?
這是一個開發責任感和長遠意識的問題。不兼容的改變不應該很輕鬆就被引入到一個被大量依賴的軟件當中。這必定導致升級的代價昂貴。改變主版本纔可以發佈不兼容的改變也在變相的促使你思考這一變化帶來的影響及其投入產出比。
- 寫出整個 API 的文檔是一項艱鉅的工作!
作爲一名開發者,撰寫軟件文檔以供其他人使用是你的責任。管理軟件複雜度是保障一個項目高效運作的及其重要的部分,如果沒人知道如何使用你的軟件,什麼方法使用起來比較安全,項目將會變得很困難。長期來看,語義化版本管理以及一個被良好定義的公有 API 能夠保障每個人每件事都運轉順利。
- 如果我在此版本中不小心發佈了一個不向下兼容的改變,我該怎麼辦?
當你意識到你打破了語義化版本管理規範之後,立即修復這個問題並且發佈一個新的次版本去修復此問題並恢復向下兼容性。甚至在這個週期裏,不要接受任何其它版本的發佈。如果方便合適的話,記錄下出錯的版本並向你的用戶告知這一問題以便他們警惕這個出錯的版本。
- 如果我更新了我自己的依賴性但是沒有改變公有 API,我該怎麼做?
這回被認爲是兼容的,因爲它並沒有影響到公有 API。軟件顯式依賴你的軟件包相同的依賴,應該有自身的以來規範,作者自然會注意任何衝突。決定這個改變是一個補丁級別還是次級別,取決於你把依賴關係的改變用在了修復一個bug上還是用在了引入新功能上。我通常會在後期期待額外的代碼,很明顯這是一個次級別的增大。
- 如果我不經意改變了公有 API,而且它並沒有遵循版本號的改變 (比如把補丁發佈引入到了一個錯誤的主版本),我該怎麼辦?
做出做合理的判斷。如果你有一大羣用戶,因爲有意把行爲改回到公有 API 會收到劇烈的影響,那麼最好發佈一個主版本,儘管實際的改動也許只是發佈一個補丁。記住,語義化版本管理就是通過版本號的變化傳遞信息。如果這些改變對用戶很重要,就用版本號去通知他們。
- 我該如何處理廢棄的功能?
廢棄已存在的功能是軟件開發的一個正常部分。而且它經常需要提前行動。當你廢棄部分你的公有 API 時,你應該做兩件事:(1) 更新你的文檔,讓用戶知道這一變化,(2) 創建一個此版本發佈的任務。當你發佈主版本完全移除功能之前,至少要有一個次版本發佈,該發佈包含廢棄的動作,以便讓用戶可以平穩的過度到新的 API。
- 語義化版本管理是否存在版本字符串的長度限制?
沒有限制,但要合理使用。比如一個 255 字符的版本字符串就算是有點長了。同樣的,規範系統可以實行自己的字符串長度限制。
關於
語義化版本管理規範由 Gravatars 發明者、Github 的聯合創始人 Tom Preston-Werner撰寫。 如果你想留下寶貴意見,請來 Github 開一個 issue 吧。