語意明確的版本變更


iThome 網站首載:語意明確的版本變更


軟體或程式庫版本號,出發點是區別版本差異,然而各種軟體與程式庫由於性質、作用不同,在版本號碼變更上就各不相同,也有不少是基於商業性效果而變更版本號碼,在缺乏規範的情況下,版本號碼甚至會因為開發者隨興所致而變更;版本號碼之目的根本上是想要進行版本管理,開發者應建立對版本號碼的共同認識,賦予號碼間有明確之界線與語意,並進一步對版本之間的功能差異與相容性負起責任。

認識常見的版本名詞

在各式平台、軟體、程式庫之間,確實充滿了各式不同的版本號碼,然而混亂之間倒也存在若干交集,儘管交集的界線模糊,開發者認識這些交集,對於版本辨識仍有幫助。粗淺地先從軟體版本週期來看,開發者通常會看到Alpha、Beta、Release Candidate(RC)、Stable等名詞,Alpha是希臘字母中第一個字母,因而Alpha版本是軟體釋出周期中第一個階段,功能必然未完善,主要做為內部測試或邀請外部相關人員(客戶、合作夥伴等)參與測試之用。

Beta通常是最早對外公開、可公眾參與測試的版本,亦常稱為Preview、Early Access Releases等,這個階段的版本包括承諾的功能,不過會有未修正的臭蟲或安全問題,通常用來大規模測試程式、收集使用者經驗或測試市場反應,進而回饋作為修正軟體之依據。Release Candidate顧名思義,就是正式版釋出前的侯選版本,如果沒有問題也有可能成為正式版本,有時會有RC1、RC2等兩個或更多侯選,Stable就是正式穩定的釋出版本,有時也常見稱為General Availability(GA)。

版本名詞中也常見SNAPSHOT,就字詞為「快照」之意,在軟體開發中是用來進行版本修訂控制的一種概念,用以表示某特定時間特定容器(Repository)下的原始碼狀態。SNAPSHOT代表處於開發中且經常更動的不穩定版本,在一些工具中SNAPSHOT有著具體之意義,例如Maven中,SNAPSHOT版本號與每個階段釋出之版本號對應,如果程式庫版本被設置為Lib-0.3-SNAPSHOT,釋出版本號就是0.3,然而若程式庫原始碼變更、建構、重新部署時,Maven都會加上時間戳記,像是Lib-0.3-20130103-115825-12.jar,如果有其他應用程式依賴在Lib-0.3-SNAPSHOT,那就可以根據時間戳記來判斷、下載最新的JAR檔案,這可以避免版本號碼升得太快,或使用單一版本號無法取得更新的問題;相對地,對於應用程式正式環境來說,不得使用SNAPSHOT版本。

混亂版本號的交集

最常見的版本號制訂方式,就是major.minor(.build)方式。major字面上就有著重大之意義,正式版本前通常為0.x形式,首個正式版本則為1.0,當有重大功能變更、架構更動時會增加major號碼;minor字面意義是較小之意,用於小規模的功能增加、調整或變動,像是小部份API功能新增或加強時,會變更minor版號;如果有build版本部份,通常是指臭蟲修正後重新建構釋出的版本變更。

在最常見的major.minor版本號制訂方式之下,可以看到各種形式的混搭變化。Linux核心版本有個不成文規範,minor若是奇數,代表著不穩定版本,偶數則表示穩定版本;有時號碼後面會有a、b、rc等字樣,代表著alpha、beta、release candidate,像是1.0a、2.0b1、3.0rc2等;build版本號之後可能結合日期或時間戳記,或者直接以日期時間作為build版本號區段,像是App-1.3.20130103。

對於具有標準規範的技術,由於同一規範下可以有不同實作品,規格書版本也必須有所規範,在Oracle官方《The Java Tutorial》中〈Setting Package Version Information〉就可以看到,它建議了在JAR檔案的manifest中應該包括的版本資訊,並建議應該區別規格書版本(Specification-Version)與實作版本(Implementation-Version)。在〈Java™ Product Versioning〉文中建議規格書版本號格式為major.minor.micro,只不過,對major、minor、micro沒有清楚的定義,major.minor(.other)似乎是版本號格式共同的交集,只不過什麼是重大功能變更?哪些又該視為小規模功能增加呢?許多軟體或程式庫即使有專門文件說明版本號格式,然而遞增各區段版號的遞增規則都沒有清楚地定義。


語意化版本規範

有鑑於對於版本號沒有嚴謹的定義,Gravatars創始人兼GitHub共同創辦者Tom Preston-Wernere建立了〈Semantic Versioning〉,試著對版本號格式與遞增方式進行清楚的規範,這份規範是「根據(但不局限於)已經被各種封閉、開放源碼軟體所廣泛使用的慣例所設計」,版本號格式為MAJOR.MINOR.PATCH,版號遞增規則中定義MAJOR為做了不相容的API修改、MINOR為做了向下相容的功能性新增、PATCH為做了向下相容的問題修正,規範中提到「為了讓這套理論運作,你必須先有定義好的公共API」,只有這樣才可以「透過修改相應的版號來向大家說明你的修改」。

必須先有定義好的公共API,等於要求你必須有清楚的規格,無論那是透過文件定義或程式碼強制要求來實現,以Java來看,〈Semantic Versioning〉的MAJOR.MINOR就相當於〈Java™ Product Versioning〉中定義的規格書版本號碼,也就是說,程式庫中任何不相容的修改必須遞增規格書的MAJOR,只是在程式庫中新增API的話則必須遞增規格書的MINOR,〈Semantic Versioning〉的PATCH,則可對應至JAR檔案中manifest的實作版本資訊,因為臭蟲修正屬於實作品的處理範圍。

從另一個角度來看,若只是遞增MINOR.PATCH版本號碼,表示沒有破壞相容性,遵守〈Semantic Versioning〉的規範,可以解決「相依性地獄(Dependency hell)」的問題。以Java來說,如果應用程式相依於程式庫ABC 2.1.0,而程式庫版本號遵守〈Semantic Versioning〉規範,那麼將來可以放心地使用2.1.0至小於3.0.0的ABC程式庫版本,也就是說,只要MAJOR號碼不遞增,API都是相容的。

在版本號碼上清楚地表達出相容性,是〈Semantic Versioning〉要達到的目的之一,另一目的就是建立對版本號碼的共同認知,這個認知必須簡單但沒有岐義,在程式庫的README中看到有聲明使用並遵循這些規則時,開發者必須能在未確認版本號碼相關文件,也能清楚地瞭解版本號碼代表的相容性問題。

版本變更需要明確與責任

〈Semantic Versioning〉的規範並不長,我在閱讀規範的過程中,腦海浮現了OpenStack上〈GIT Commit Good Practice〉談到,Commit時最重要的原則就是「確定每個Commit只會有一個邏輯上的變更」,並應避免「混雜兩個無關的功能變更」或是「在單一個巨大Commit中送出了一堆新特性」,雖然是從更微觀的角度來看待版本變更這回事,不過似乎與〈Semantic Versioning〉規範有些相同的道理,也就是版本變更必須有明確的界線。

〈Semantic Versioning〉表面看來只是將各軟體與程式庫在版本號碼上做的事情,取得大的交集,實際上要求開發者審慎思考規格建立與版本相容性問題,規範中第一條要求必須定義精確且穩定的公共API,就是在要求開發者制訂規格,如果開發者難以制訂規格,每天都在改變API,「那麼你應該仍在主版號為零的階段(0.y.z)」,只有規格明確且穩定的API,才可以是1.0.0版本。

有了第一個公共API之後,每個版號的增加就都要有明確決策,像Maven這類可管理相依性的工具,只是忠實地依據演算法及給定的版本號碼進行管理,只有開發者自己才能知道「這只是個臭蟲修正?」、「這是個新特性?」、「這個變更是否破壞相容性?」等問題,任何公共API的不相容無論幅度多小,都得變更MAJOR號碼,這說明了公共API任何程度的不相容,都會是巨大的變更,就如同〈Semantic Versioning〉中〈FAQ〉提到的「這是開發的責任感和前瞻性的問題。不相容的改變不應該輕易被加入到有許多相依性程式碼的軟體中」。