Семантическое Версионирование 2.0.0-rc.2

В мире управления процессом разработки есть понятие «ад зависимостей» (dependency hell). Чем больше растёт ваша система и чем больше библиотек вы интегрируете в ваш проект, тем больше вероятность оказаться в этой ситуации.

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

В качестве решения данной проблемы я предлагаю простой набор правил и требований, которые определяют, как назначаются и увеличиваются номера версий. Для того чтобы эта система работала, вам необходимо определить публичный API. Он может быть описан в документации или определяться самим кодом. Главное, чтобы это API было ясным и точным. Однажды определив публичное API, вы сообщаете об изменениях в нём особым увеличением номера версий. Рассмотрим формат версий X.Y.Z (главная, второстепенная, патч). Баг-фиксы, не влияющие на API, увеличивают патч версию, обратно совместимые добавления/изменения API увеличивают второстепенную версию и обратно несовместимые изменения API увеличивают главную версию.

Я называю эту систему «Семантическое Версионирование» (Semantic Versioning). По этой схеме номера версий и то, как они изменяются, передают смысл содержания исходного кода и что было модифицировано от одной версии к другой.

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

Слова «ДОЛЖЕН» (MUST), «НЕ ДОЛЖЕН» (MUST NOT), «ОБЯЗАТЕЛЬНО» (REQUIRED), «СЛЕДУЕТ» (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. После этого релиза номера версий увеличиваются в зависимости от того, как изменяется публичное API.

  6. Патч-версия Z (x.y.Z | x > 0) ДОЛЖНА быть увеличена только если содержит обратно совместимые баг-фиксы. Определение баг-фикс означает внутренние изменения, которые исправляют некорректное поведение.

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

  8. Главная версия X (X.y.z | X > 0) ДОЛЖНА быть увеличена, если в публичном API представлены какие-либо обратно несовместимые изменения. Она МОЖЕТ включать в себя изменения, характерные для уровня второстепенных версий и патчей. Когда увеличивается главная версия, второстепенная и патч-версия ДОЛЖНЫ быть обнулены.

  9. Предрелизная версия МОЖЕТ быть обозначена добавлением дефиса и серией разделённых точкой идентификаторов, следующих сразу за патч-версией. Идентификаторы ДОЛЖНЫ содержать только латинские буквы, цифры и дефис [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. Сборочные мета-данные МОГУТ быть обозначены добавлением знака плюс и ряда разделённых точкой идентификаторов, следующих сразу за патч или предрелизной версией. Идентификаторы ДОЛЖНЫ содержать только латинские буквы, номера и дефис [0-9A-Za-z-]. Сборочные мета-данные СЛЕДУЕТ игнорировать, когда определяется старшинство версий. Поэтому два пакета с одинаковой версией, но разными сборочными мета-данными, рассматриваются как одна и та же версия. Примеры: 1.0.0-alpha+001, 1.0.0+20130313144700, 1.0.0-beta+exp.sha.5114f85.

  11. Старшинство ДОЛЖНО рассчитываться путём разделения версии на главную, второстепенную, патч и предрелизные идентификаторы. Именно в перечисленном порядке (Сборочные мета-данные не фигурируют в расчёте старшинства). Главная, второстепенная и патч-версии всегда сравниваются численно. Старшинство предрелизных версий ДОЛЖНО быть определено сравнением наборов идентификаторов, разделённых точкой следующим образом: идентификаторы, состоящие только из цифр, сравниваются численно, буквенные идентификаторы или дефисы сравниваются лексически в ASCII-порядке. Численные идентификаторы всегда имеют низший приоритет, чем символьные. Примеры: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0.

Зачем использовать семантическое версионирование?

Это не новая или революционная идея. Вероятно, вы уже используете что-то подобное. Проблема в том, что «подобное» — не достаточно хорошо. Без соответствия формальной спецификации, номера версий практически бесполезны для управления зависимостями. Ясно определив и сформулировав идею версионирования, становится легче сообщать о намерениях пользователям вашего ПО. Когда эти намерения ясны, гибки (но не слишком), спецификации зависимостей наконец могут быть созданы.

Простой пример демонстрирует, как Семантическое Версионирование может сделать «ад зависимостей» вещью из прошлого. Представим библиотеку, названную «Firetruck». Она требует Семантически Версионированный пакет под названием «Ladder». Когда Firetruck был создан, Ladder был 3.1.0 версии. Так как Firetruck использует функционал версии 3.1.0, вы спокойно можете объявить зависимость от Ladder версии 3.1.0, но менее чем 4.0.0. Теперь, когда доступен Ladder 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.

Не препятствует ли это быстрой разработке и коротким итерациям?

Главная версия 0 как раз и означает быструю разработку. Если вы изменяете API каждый день, вы должны быть на версии 0.y.z или на отдельной ветке разработки работать над следующей главной версией.

Даже если малейшие обратно несовместимые изменения в публичном API требуют

выпуска новой главной версии, не закончится ли это тем, что очень скоро версия станет 42.0.0?

Это вопрос ответственной разработки и предвидения. Несовместимые изменения не должны быть представлены как незначительные в ПО, имеющем много зависимого кода. Стоимость обновления может быть велика. Практика увеличения главных версий релизов с обратно несовместимыми изменениями означает, что вам придётся думать о последствиях ваших изменений и учитывать соотношение цена/качество.

Документирование всего API — слишком много работы!

Это ваша ответственность, как профессионального разработчика, правильно документировать ПО, предназначенное для широкого использования. Управление сложностью ПО очень важная часть поддержки высокой эффективности проекта. Это тяжело сделать, если никто не знает, как использовать ваше ПО или какой метод можно вызывать безопасно. В долгосрочной перспективе Семантическое Версионирование и настойчивость в качественном документировании публичного API поможет всем и всему работать слаженно.

Что мне делать, если я случайно зарелизил обратно несовместимые изменения как второстепенную версию?

Как только вы поняли, что нарушили спецификации Семантического Версионирования, исправьте проблему и выпустите новую второстепенную версию, которая исправляет проблему и восстанавливает обратную совместимость. Даже в таких обстоятельствах неприемлемо модифицировать уже выпущенные релизы. Если это необходимо, укажите в документации о нарушении обратной совместимости, версионирования и проинформируйте ваших пользователей, чтобы они знали о нарушении порядка версий.

Что я должен делать, если я обновляю свои собственные зависимости без изменения публичного API?

Это можно рассматривать как совместимые изменения, так как они не влияют на публичное API. ПО, которое явно зависит от тех же зависимостей что и ваш пакет, должно иметь собственные спецификации зависимостей и автор будет уведомлен о возможных конфликтах. Являются ли данные изменения уровня патча или второстепенного уровня, зависит от того, обновили ли вы свои зависимости чтобы исправить баг или реализовать новый функционал. В последнем случае, как правило, добавляется некоторое количество дополнительного кода и как следствие, увеличивается второстепенная версия.

Что я должен делать, если исправление бага вернуло код в состояние совместимости с публичным API (т.е. появилось расхождение в документации публичного API и кодом)?

На ваше усмотрение. Если у вас огромная аудитория, которая будет поставлена перед фактом возвращения прежнего поведения API, то лучше выпустить новый релиз с увеличением главной версии, даже несмотря на то, что фикс содержит исправления уровня патча. Запомните, в Семантическом Версионировании номера версий изменяются строго следуя спецификации. Если эти изменения важны для ваших пользователей, используйте номер версии, чтобы информировать их.

Что делать с устаревшим функционалом?

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

Об авторе

Авторство спецификаций Семантического Версионирования принадлежит Тому Престон-Вернеру, основателю Gravatars и соучредителю GitHub.

Если вы хотите оставить отзыв, пожалуйста, создайте запрос на GitHub.

Лицензия

Creative Commons — CC BY 3.0 http://creativecommons.org/licenses/by/3.0/