摘要
版本格式:主版本號(hào).次版本號(hào).修訂號(hào),版本號(hào)遞增規(guī)則如下:
- 主版本號(hào):當(dāng)你做了不兼容的 API 修改,
- 次版本號(hào):當(dāng)你做了向下兼容的功能性新增,
- 修訂號(hào):當(dāng)你做了向下兼容的問題修正。
先行版本號(hào)及版本編譯元數(shù)據(jù)可以加到“主版本號(hào).次版本號(hào).修訂號(hào)”的后面,作為延伸。
簡(jiǎn)介
在軟件管理的領(lǐng)域里存在著被稱作“依賴地獄”的死亡之谷,系統(tǒng)規(guī)模越大,加入的包越多,你就越有可能在未來的某一天發(fā)現(xiàn)自己已深陷絕望之中。
在依賴高的系統(tǒng)中發(fā)布新版本包可能很快會(huì)成為噩夢(mèng)。如果依賴關(guān)系過高,可能面臨版本控制被鎖死的風(fēng)險(xiǎn)(必須對(duì)每一個(gè)依賴包改版才能完成某次升級(jí))。而如果依賴關(guān)系過于松散,又將無法避免版本的混亂(假設(shè)兼容于未來的多個(gè)版本已超出了合理數(shù)量)。當(dāng)你專案的進(jìn)展因?yàn)榘姹疽蕾嚤绘i死或版本混亂變得不夠簡(jiǎn)便和可靠,就意味著你正處于依賴地獄之中。
作為這個(gè)問題的解決方案之一,我提議用一組簡(jiǎn)單的規(guī)則及條件來約束版本號(hào)的配置和增長(zhǎng)。這些規(guī)則是根據(jù)(但不局限于)已經(jīng)被各種封閉、開放源碼軟件所廣泛使用的慣例所設(shè)計(jì)。為了讓這套理論運(yùn)作,你必須先有定義好的公共 API 。這可以透過文件定義或代碼強(qiáng)制要求來實(shí)現(xiàn)。無論如何,這套 API 的清楚明了是十分重要的。一旦你定義了公共 API,你就可以透過修改相應(yīng)的版本號(hào)來向大家說明你的修改。考慮使用這樣的版本號(hào)格式:X.Y.Z (主版本號(hào).次版本號(hào).修訂號(hào))修復(fù)問題但不影響API 時(shí),遞增修訂號(hào);API 保持向下兼容的新增及修改時(shí),遞增次版本號(hào);進(jìn)行不向下兼容的修改時(shí),遞增主版本號(hào)。
我稱這套系統(tǒng)為“語義化的版本控制”,在這套約定下,版本號(hào)及其更新方式包含了相鄰版本間的底層代碼和修改內(nèi)容的信息。
語義化版本控制規(guī)范(SemVer)
以下關(guān)鍵詞 MUST、MUST NOT、REQUIRED、SHALL、SHALL NOT、SHOULD、SHOULD NOT、 RECOMMENDED、MAY、OPTIONAL 依照 RFC 2119 的敘述解讀。(譯注:為了保持語句順暢, 以下文件遇到的關(guān)鍵詞將依照整句語義進(jìn)行翻譯,在此先不進(jìn)行個(gè)別翻譯。)
使用語義化版本控制的軟件必須(MUST)定義公共 API。該 API 可以在代碼中被定義或出現(xiàn)于嚴(yán)謹(jǐn)?shù)奈募?nèi)。無論何種形式都應(yīng)該力求精確且完整。
標(biāo)準(zhǔn)的版本號(hào)必須(MUST)采用 X.Y.Z 的格式,其中 X、Y 和 Z 為非負(fù)的整數(shù),且禁止(MUST NOT)在數(shù)字前方補(bǔ)零。X 是主版本號(hào)、Y 是次版本號(hào)、而 Z 為修訂號(hào)。每個(gè)元素必須(MUST)以數(shù)值來遞增。例如:1.9.1 -> 1.10.0 -> 1.11.0。
標(biāo)記版本號(hào)的軟件發(fā)行后,禁止(MUST NOT)改變?cè)摪姹拒浖膬?nèi)容。任何修改都必須(MUST)以新版本發(fā)行。
主版本號(hào)為零(0.y.z)的軟件處于開發(fā)初始階段,一切都可能隨時(shí)被改變。這樣的公共 API 不應(yīng)該被視為穩(wěn)定版。
1.0.0 的版本號(hào)用于界定公共 API 的形成。這一版本之后所有的版本號(hào)更新都基于公共 API 及其修改內(nèi)容。
修訂號(hào) Z(x.y.Z
|
x > 0)必須(MUST)在只做了向下兼容的修正時(shí)才遞增。這里的修正指的是針對(duì)不正確結(jié)果而進(jìn)行的內(nèi)部修改。次版本號(hào) Y(x.Y.z
|
x > 0)必須(MUST)在有向下兼容的新功能出現(xiàn)時(shí)遞增。在任何公共 API 的功能被標(biāo)記為棄用時(shí)也必須(MUST)遞增。也可以(MAY)在內(nèi)部程序有大量新功能或改進(jìn)被加入時(shí)遞增,其中可以(MAY)包括修訂級(jí)別的改變。每當(dāng)次版本號(hào)遞增時(shí),修訂號(hào)必須(MUST)歸零。
8.主版本號(hào) X(X.y.z |
X > 0)必須(MUST)在有任何不兼容的修改被加入公共 API 時(shí)遞增。其中可以(MAY)包括次版本號(hào)及修訂級(jí)別的改變。每當(dāng)主版本號(hào)遞增時(shí),次版本號(hào)和修訂號(hào)必須(MUST)歸零。
9.先行版本號(hào)可以(MAY)被標(biāo)注在修訂版之后,先加上一個(gè)連接號(hào)再加上一連串以句點(diǎn)分隔的標(biāo)識(shí)符來修飾。標(biāo)識(shí)符必須(MUST)由 ASCII 字母數(shù)字和連接號(hào) [0-9A-Za-z-] 組成,且禁止(MUST NOT)留白。數(shù)字型的標(biāo)識(shí)符禁止(MUST NOT)在前方補(bǔ)零。先行版的優(yōu)先級(jí)低于相關(guān)聯(lián)的標(biāo)準(zhǔn)版本。被標(biāo)上先行版本號(hào)則表示這個(gè)版本并非穩(wěn)定而且可能無法滿足預(yù)期的兼容性需求。范例:1.0.0-alpha、1.0.0-alpha.1、1.0.0-0.3.7、1.0.0-x.7.z.92。
10.版本編譯元數(shù)據(jù)可以(MAY)被標(biāo)注在修訂版或先行版本號(hào)之后,先加上一個(gè)加號(hào)再加上一連串以句點(diǎn)分隔的標(biāo)識(shí)符來修飾。標(biāo)識(shí)符必須(MUST)由 ASCII 字母數(shù)字和連接號(hào) [0-9A-Za-z-] 組成,且禁止(MUST NOT)留白。當(dāng)判斷版本的優(yōu)先層級(jí)時(shí),版本編譯元數(shù)據(jù)可(SHOULD)被忽略。因此當(dāng)兩個(gè)版本只有在版本編譯元數(shù)據(jù)有差別時(shí),屬于相同的優(yōu)先層級(jí)。范例:1.0.0-alpha+001、1.0.0+20130313144700、1.0.0-beta+exp.sha.5114f85。
11.版本的優(yōu)先層級(jí)指的是不同版本在排序時(shí)如何比較。判斷優(yōu)先層級(jí)時(shí),必須(MUST)把版本依序拆分為主版本號(hào)、次版本號(hào)、修訂號(hào)及先行版本號(hào)后進(jìn)行比較(版本編譯元數(shù)據(jù)不在這份比較的列表中)。由左到右依序比較每個(gè)標(biāo)識(shí)符,第一個(gè)差異值用來決定優(yōu)先層級(jí):主版本號(hào)、次版本號(hào)及修訂號(hào)以數(shù)值比較,例如:1.0.0 < 2.0.0 < 2.1.0 < 2.1.1。當(dāng)主版本號(hào)、次版本號(hào)及修訂號(hào)都相同時(shí),改以優(yōu)先層級(jí)比較低的先行版本號(hào)決定。例如:1.0.0-alpha < 1.0.0。有相同主版本號(hào)、次版本號(hào)及修訂號(hào)的兩個(gè)先行版本號(hào),其優(yōu)先層級(jí)必須(MUST)透過由左到右的每個(gè)被句點(diǎn)分隔的標(biāo)識(shí)符來比較,直到找到一個(gè)差異值后決定:只有數(shù)字的標(biāo)識(shí)符以數(shù)值高低比較,有字母或連接號(hào)時(shí)則逐字以 ASCII 的排序來比較。數(shù)字的標(biāo)識(shí)符比非數(shù)字的標(biāo)識(shí)符優(yōu)先層級(jí)低。若開頭的標(biāo)識(shí)符都相同時(shí),欄位比較多的先行版本號(hào)優(yōu)先層級(jí)比較高。范例: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。
為什么要使用語義化的版本控制?
這并不是一個(gè)新的或者革命性的想法。實(shí)際上,你可能已經(jīng)在做一些近似的事情了。問題在于只是“近似”還不夠。如果沒有某個(gè)正式的規(guī)范可循,版本號(hào)對(duì)于依賴的管理并無實(shí)質(zhì)意義。將上述的想法命名并給予清楚的定義,讓你對(duì)軟件使用者傳達(dá)意向變得容易。一旦這些意向變得清楚,彈性(但又不會(huì)太彈性)的依賴規(guī)范就能達(dá)成。
舉個(gè)簡(jiǎn)單的例子就可以展示語義化的版本控制如何讓依賴地獄成為過去。假設(shè)有個(gè)名為“救火車”的函式庫,它需要另一個(gè)名為“梯子”并已經(jīng)有使用語義化版本控制的包。當(dāng)救火車創(chuàng)建時(shí),梯子的版本號(hào)為 3.1.0。因?yàn)榫然疖囀褂昧艘恍┌姹?3.1.0 所新增的功能, 你可以放心地指定依賴于梯子的版本號(hào)大等于 3.1.0 但小于 4.0.0。這樣,當(dāng)梯子版本 3.1.1 和 3.2.0 發(fā)布時(shí),你可以將直接它們納入你的包管理系統(tǒng),因?yàn)樗鼈兡芘c原有依賴的軟件兼容。
作為一位負(fù)責(zé)任的開發(fā)者,你理當(dāng)確保每次包升級(jí)的運(yùn)作與版本號(hào)的表述一致。現(xiàn)實(shí)世界是復(fù)雜的,我們除了提高警覺外能做的不多。你所能做的就是讓語義化的版本控制為你提供一個(gè)健全的方式來發(fā)行以及升級(jí)包,而無需推出新的依賴包,節(jié)省你的時(shí)間及煩惱。
如果你對(duì)此認(rèn)同,希望立即開始使用語義化版本控制,你只需聲明你的函式庫正在使用它并遵循這些規(guī)則就可以了。請(qǐng)?jiān)谀愕?README 文件中保留此頁連結(jié),讓別人也知道這些規(guī)則并從中受益。
FAQ
在 0.y.z 初始開發(fā)階段,我該如何進(jìn)行版本控制?
最簡(jiǎn)單的做法是以 0.1.0 作為你的初始化開發(fā)版本,并在后續(xù)的每次發(fā)行時(shí)遞增次版本號(hào)。
如何判斷發(fā)布 1.0.0 版本的時(shí)機(jī)?
當(dāng)你的軟件被用于正式環(huán)境,它應(yīng)該已經(jīng)達(dá)到了 1.0.0 版。如果你已經(jīng)有個(gè)穩(wěn)定的 API 被使用者依賴,也會(huì)是 1.0.0 版。如果你很擔(dān)心向下兼容的問題,也應(yīng)該算是 1.0.0 版了。
這不會(huì)阻礙快速開發(fā)和迭代嗎?
主版本號(hào)為零的時(shí)候就是為了做快速開發(fā)。如果你每天都在改變 API,那么你應(yīng)該仍在主版本號(hào)為零的階段(0.y.z),或是正在下個(gè)主版本的獨(dú)立開發(fā)分支中。
對(duì)于公共 API,若即使是最小但不向下兼容的改變都需要產(chǎn)生新的主版本號(hào),豈不是很快就達(dá)到 42.0.0 版?
這是開發(fā)的責(zé)任感和前瞻性的問題。不兼容的改變不應(yīng)該輕易被加入到有許多依賴代碼的軟件中。升級(jí)所付出的代價(jià)可能是巨大的。要遞增主版本號(hào)來發(fā)行不兼容的改版,意味著你必須為這些改變所帶來的影響深思熟慮,并且評(píng)估所涉及的成本及效益比。
為整個(gè)公共 API 寫文件太費(fèi)事了!
為供他人使用的軟件編寫適當(dāng)?shù)奈募悄阕鳛橐幻麑I(yè)開發(fā)者應(yīng)盡的職責(zé)。保持專案高效一個(gè)非常重要的部份是掌控軟件的復(fù)雜度,如果沒有人知道如何使用你的軟件或不知道哪些函數(shù)的調(diào)用是可靠的,要掌控復(fù)雜度會(huì)是困難的。長(zhǎng)遠(yuǎn)來看,使用語義化版本控制以及對(duì)于公共 API 有良好規(guī)范的堅(jiān)持,可以讓每個(gè)人及每件事都運(yùn)行順暢。
萬一不小心把一個(gè)不兼容的改版當(dāng)成了次版本號(hào)發(fā)行了該怎么辦?
一旦發(fā)現(xiàn)自己破壞了語義化版本控制的規(guī)范,就要修正這個(gè)問題,并發(fā)行一個(gè)新的次版本號(hào)來更正這個(gè)問題并且恢復(fù)向下兼容。即使是這種情況,也不能去修改已發(fā)行的版本。可以的話,將有問題的版本號(hào)記錄到文件中,告訴使用者問題所在,讓他們能夠意識(shí)到這是有問題的版本。
如果我更新了自己的依賴但沒有改變公共 API 該怎么辦?
由于沒有影響到公共 API,這可以被認(rèn)定是兼容的。若某個(gè)軟件和你的包有共同依賴,則它會(huì)有自己的依賴規(guī)范,作者也會(huì)告知可能的沖突。要判斷改版是屬于修訂等級(jí)或是次版等級(jí),是依據(jù)你更新的依賴關(guān)系是為了修復(fù)問題或是加入新功能。對(duì)于后者,我經(jīng)常會(huì)預(yù)期伴隨著更多的代碼,這顯然會(huì)是一個(gè)次版本號(hào)級(jí)別的遞增。
如果我變更了公共 API 但無意中未遵循版本號(hào)的改動(dòng)怎么辦呢?(意即在修訂等級(jí)的發(fā)布中,誤將重大且不兼容的改變加到代碼之中)
自行做最佳的判斷。如果你有龐大的使用者群在依照公共 API 的意圖而變更行為后會(huì)大受影響,那么最好做一次主版本的發(fā)布,即使嚴(yán)格來說這個(gè)修復(fù)僅是修訂等級(jí)的發(fā)布。記住, 語義化的版本控制就是透過版本號(hào)的改變來傳達(dá)意義。若這些改變對(duì)你的使用者是重要的,那就透過版本號(hào)來向他們說明。
我該如何處理即將棄用的功能?
棄用現(xiàn)存的功能是軟件開發(fā)中的家常便飯,也通常是向前發(fā)展所必須的。當(dāng)你棄用部份公共 API 時(shí),你應(yīng)該做兩件事:(1)更新你的文件讓使用者知道這個(gè)改變,(2)在適當(dāng)?shù)臅r(shí)機(jī)將棄用的功能透過新的次版本號(hào)發(fā)布。在新的主版本完全移除棄用功能前,至少要有一個(gè)次版本包含這個(gè)棄用信息,這樣使用者才能平順地轉(zhuǎn)移到新版 API。
語義化版本對(duì)于版本的字串長(zhǎng)度是否有限制呢?
沒有,請(qǐng)自行做適當(dāng)?shù)呐袛唷Ee例來說,長(zhǎng)到 255 個(gè)字元的版本已過度夸張。再者,特定的系統(tǒng)對(duì)于字串長(zhǎng)度可能會(huì)有他們自己的限制。
關(guān)于
語義化版本控制的規(guī)范是由 Gravatars 創(chuàng)辦者兼 GitHub 共同創(chuàng)辦者 Tom Preston-Werner 所建立。
如果您有任何建議,請(qǐng)到 GitHub 上提出您的問題。