Семантика на версиите 2.0.0

Резюме

Използвайки номериране на версиите като ГЛАВНА.ВТОРОСТЕПЕННА.КРЪПКА, трябва да увеличите:

  1. ГЛАВНАта версия, при направени несъвместими промени в приложно-програмният интерфейс (API).

  2. ВТОРОСТЕПЕННАта версия, когато промените са обратно съвместими.

  3. КРЪПКА-номера, когато са направени обратно съвместими поправки.

Допускат се допълнителни етикети за предварителни издания и билд-метаданни, като разширения на формата за версия ГЛАВНА.ВТОРОСТЕПЕННА.КРЪПКА.

Въведение

В света на софтуерния мениджмънт съществува страховито място, наречено „ад на зависимостите“. Колкото по-голяма става вашата система и колкото повече библиотеки интегрирате в проекта си, толкова по-голяма е вероятността един ден да се окажете в тази яма на отчаяние.

В системи с множество зависимости пускането на нови версии на някои от тях може бързо да се превърне в кошмар. Ако спецификациите за зависимост са твърде стриктни, рискувате да блокирате версията (невъзможността да обновите пакет, освен ако не пускате нова версия при всяка промяна в библиотека, от която той зависи). Ако зависимостите са посочени твърде слабо, неизбежно ще ви застигне несъвместимостта на версиите (поради заявена съвместимост с повече бъдещи версии, отколкото е разумно). Адът на зависимостта е мястото, където се намирате, когато блокирането на версията и/или несъвместимостта на версията ви пречат лесно и безопасно да движите проекта си напред.

За справяне с този проблем предлагам прост набор от правила и изисквания, които определят как се присвояват и увеличават номерата на версиите. Тези правила се основават на (но не непременно са ограничени до) съществуващи широко разпространени общи практики. Те са приложими както в затворени проекти, така и в такива с отворен код. За да работи тази система, първо трябва да определите публичен API. Той може да бъде описан в документация или да се определя от самия код. Важното е този API да бъде ясен и точен. След като идентифицирате публичния си API, вие съобщавате промените в него с конкретни увеличения в номера на вашата версия. Нека разгледаме формат на версия X.Y.Z (ГЛАВНА.ВТОРОСТЕПЕННА.КРЪПКА). Поправки на грешки, които не засягат версията на API увеличават версията на кръпката, обратно съвместими добавки/промени, увеличават второстепенната версия, а обратно несъвместими промени в API увеличават главната версия.

Аз наричам това „Семантика на версиите“ (Semantic Versioning). При тази схема, номерата на версиите и начинът, по който се променят, предават значение за състоянието на изходния код и какво е променено между две версии.

Спецификация на Семантика на версиите (SemVer)

Ключовите думи „ЗАДЪЛЖИТЕЛНО“ (MUST), „ЗАДЪЛЖИТЕЛНО НЕ“ (MUST NOT), „ИЗИСКВА“ (REQUIRED), „БИ ТРЯБВАЛО“ (SHELL), „НЕ БИ ТРЯБВАЛО“ (SHELL NOT), „ТРЯБВА“ (SHOULD), „НЕ ТРЯБВА“ (SHOULD NOT), „ПРЕПОРЪЧИТЕЛНО Е“ (RECOMMENDED), „МОЖЕ“ (MAY) и „НЕЗАДЪЛЖИТЕЛНО“ (OPTIONAL) в този документ се тълкуват, както е описано в RFC 2119.

  1. Продуктите, използващи „Семантика на версиите“, ЗАДЪЛЖИТЕЛНО декларират публичен API. Този API може да бъде деклариран в самия код или да съществува строго в документацията. Независимо от метода, ТРЯБВА да бъде прецизен и изчерпателен.

  2. Нормалният номер на версията ЗАДЪЛЖИТЕЛНО има вид X.Y.Z, където X, Y и Z са неотрицателни цели числа и ЗАДЪЛЖИТЕЛНО НЕ съдържат водещи нули. X е главната версия, Y е второстепенната, а Z е версията на кръпката. Всеки елемент ЗАДЪЛЖИТЕЛНО се увеличава числено. Например: 1.9.0 -> 1.10.0 -> 1.11.0.

  3. След като бъде пусната дадена версия, съдържанието на тази версия ЗАДЪЛЖИТЕЛНО НЕ се променя. Всякакви модификации е ЗАДЪЛЖИТЕЛНО да бъдат пуснати като нова версия.

  4. Главна версия нула (0.y.z) е за първоначална разработка. Всичко може да се промени по всяко време. Публичният API НЕ ТРЯБВА да се счита за стабилен.

  5. Версия 1.0.0 определя публичния API. Начинът, по който се увеличава номерът на версията след това издание, зависи от това, как се променя той.

  6. Версия на кръпката Z (x.y.Z | x > 0) ЗАДЪЛЖИТЕЛНО се увеличава, ако се правят само обратно съвместими корекции на грешки. За такива (bug fix) се считат вътрешни промени, които коригират неправилно поведение.

  7. Второстепенната версия Y (x.Y.z | x > 0) ЗАДЪЛЖИТЕЛНО се увеличава, когато в публичния API се въвежда нова, обратно съвместима функционалност. ЗАДЪЛЖИТЕЛНО е тя да се увеличи, ако някоя публична API функция е маркирана като оттеглена (depricated). МОЖЕ да се увеличи, ако се въведе съществена нова функционалност или подобрения в рамките на не-публичния програмен код. МОЖЕ да включва промени характерни за кръпка. Версията на кръпката ЗАДЪЛЖИТЕЛНО се връща на 0, когато второстепенната версия се увеличава.

  8. Главната версия X (X.y.z | X > 0) ЗАДЪЛЖИТЕЛНО се увеличава, когато в публичния API се въведат каквито и да е обратно несъвместими промени. МОЖЕ да включва и второстепенни промени или кръпки. Версията на кръпката и второстепенната версия ЗАДЪЛЖИТЕЛНО се нулират (X.0.0), когато основната версия се увеличава.

  9. Предварително издание (pre-release) МОЖЕ да бъде обозначено веднага след версията на кръпката чрез добавяне на тире, следвано от разделени с точки идентификатори. Те ЗАДЪЛЖИТЕЛНО съдържат само ASCII буквено-цифрови последователности и тире [0-9A-Za-z-]. Идентификаторите ЗАДЪЛЖИТЕЛНО НЕ трябва да са празни. Числовите идентификатори ЗАДЪЛЖИТЕЛНО НЕ включват водещи нули. Предварителните издания имат по-нисък приоритет от свързаната основна версия. Предварителното издание показва, че версията е нестабилна и може да не отговаря на предвидените изисквания за съвместимост, заявени от свързаната с нея нормална версия. Примери: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, 1.0.0-x.7.z.92.

  10. Билд-метаданни МОЖЕ да бъдат обозначени чрез добавяне на знак плюс и серия от разделени с точка идентификатори непосредствено след версията на кръпка или на предварителното издание. Идентификаторите ЗАДЪЛЖИТЕЛНО съдържат само ASCII буквено-цифрови последователности и тире [0-9A-Za-z-]. Идентификаторите ЗАДЪЛЖИТЕЛНО НЕ трябва да са празни. Билд-метаданните ЗАДЪЛЖИТЕЛНО се игнорират при определяне на приоритета на версията. По този начин две версии, които се различават само в билд-метаданните са с еднакъв приоритет. Примери: 1.0.0-alpha+001, 1.0.0+20130313144700, 1.0.0-beta+exp.sha.5114f85.

  11. Приоритетът определя как версиите се нареждате една спрямо друга. При изчисление на приоритетът, версията ЗАДЪЛЖИТЕЛНО се разделя на ГЛАВНА, ВТОРОСТЕПЕННА, КРЪПКА и идентификатори за предварително издание в този ред (билд-метаданните се пренебрегват при определянето му). Подредбата се определя от първата разлика при сравняване на всеки от тези компоненти отляво надясно, както следва: Главните, второстепенните и версиите на кръпките винаги се сравняват като числа. Пример: 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.1 < 1.0.0.

Бакус-Наур форма на граматиката за валидни SemVer версии

<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 файл, така че значението на промените във версията да бъде ясно за всички.

Често задавани въпроси

Какво да правя с версиите в началната фаза на разработка 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 помага за безпроблемната работа.

Какво да направя, ако случайно пусна обратно несъвместима промяна като второстепенна версия

Веднага щом разберете, че сте нарушили спецификацията на SemVer, поправете проблема и пуснете нова вторична версия, която коригира проблема и възстановява обратната съвместимост. Дори и в такава ситуация е неприемливо да подменяте вече издадена версия. Ако е подходящо, документирайте некоректната версия и информирайте потребителите си за проблема с обратната съвместимост в нея.

Какво трябва да направя, ако обновя собствените си зависимости, без да променя публичния API

Това може да се разглежда като съвместимо изменение, тъй като не засяга публичния API. Софтуер, който има същите зависимости като вашия пакет трябва да има свои спецификации за зависимост и авторът да забележи евентуални конфликти. За определяне дали промяната е ниво на кръпка или второстепенна модификация зависи от това дали сте актуализирали зависимостите си, за да коригирате грешка или да въведете нова функционалност. Обикновено за второто е очакван допълнителен код, при което следва да се промени второстепенната версия.

Какво става, ако по невнимание променя публичния API по начин, който не е съвместим с промяната на номера на версията (тоест кодът неправилно въвежда голяма промяна в версията на кръпка)

Използвайте най-добрата си преценка. Ако имате огромна публика, която ще бъде драстично засегната от промяната на поведението към това, което е планирано в публичният API може би е най-добре да извършите издание на главна версия, въпреки че поправката може строго погледнато да се счита за издаване на кръпка. Не забравяйте, че „Семантика на версиите“ се стреми строго да следва спецификациите. Ако тези промени са важни за вашите потребители, използвайте номера на версията, за да ги информирате.

Как се оттегля функционалност (deprecating)

Оттеглянето на съществуващата функционалност е нормална част от разработката на софтуер и често се изисква за постигне на напредък. Когато оттегляте част от вашия обществен API, трябва да направите две неща: (1) обновете документацията си информирайки потребителите за промяната, (2) направете ново незначително издание с оттеглянето. Преди да премахнете напълно функционалността в нова главна версия трябва да има поне едно второстепенно издание, което съдържа оттеглянето, така че че потребителите могат плавно да преминат към новия API.

Има ли SemVer ограничение на дължината на версията

Не, но използвайте добра преценка. Например, вероятно е излишно версията да е дълга 255 символа. Също така, специфичните системи могат да налагат свои собствени ограничения за дължината на низа.

„v1.2.3“ семантична версия ли е

Не, „v1.2.3“ не е семантична версия. Префиксиране на семантичната версия с „v“ е често срещан начин (на английски език) да се посочи, че става дума за номер на версия. Съкращаването „версия“ като „v“ често се наблюдава при системи за контрол на версиите. Пример: git tag v1.2.3 -m "Release version 1.2.3", в случая „v1.2.3“ е маркер, а семантичната версия е „1.2.3“.

Има ли регулярни изрази (RegEx) за проверка на дали даден низ отговаря на SemVer спецификацията

Съществуват два такива израза. Първият е за системи поддържащи PCRE (Perl Compatible Regular Expressions, тоест 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-]+)*))?$

Другият използва номерирани групи (при него cg1 = ГЛАВНА, cg2 = ВТОРОСТЕПЕННА, cg3 = КРЪПКА, cg4 = предварително издание и cg5 = билд-метаданни) и е съвместим с ECMA Script (JavaScript), PCRE (Perl Compatible Regular Expressions, тоест Perl, PHP и R), Python и Go.

Вижте: https://regex101.com/r/vkijKf/1/

^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$

За автора

Автор на спецификацията „Семантика на версиите“ (SemVer) е Том Престон-Вернер, основател на Gravatars и съучредител на GitHub.

За обратна връзка, моля създайте запитване в GitHub.

Лиценз

Creative Commons — CC BY 3.0