Эволюция методологий версионирования
Эволюция методологий версионирования

Привет, Хабр. Всех с наступившим Новым Годом.

На днях наткнулся на статью Махмуда Хашеми, в которой обсуждаются некоторые недостатки методологии семантического версионирования (SemVer), и в качестве решения этих недостатков предлагается использовать календарное версионирование (CalVer). В организации, где я работаю, по стандарту разработки требуется обязательно версионировать приложения по SemVer. Из собственного опыта использования SemVer скажу, что нашёл в ней ещё ряд недостатков, для исправления которых пришлось искать новый способ версионирования.

Что ещё не так с SemVer?

Кратко напомню схему семантического версионирования:

MAJOR.MINOR.PATCH

Где MAJOR изменяется при несовместимых изменениях, MINOR - при совместимом добавлении функционала, и PATCH - при совместимом исправлении ошибок.

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

Определение версии в CI

Я работаю в финтех организации и разрабатываю несколько микросервисов крупной системы. Мы используем стандартизированный pipeline сборки с доставки наших приложений. У нас настроен CI/CD, который собирает, проверяет и доставляет доработки в промышленный контур автоматически после сливания pull request в главную ветку. Такая схема требует на начальном этапе сборки автоматически установить версию приложению. И вот тут как раз SemVer даёт сбой!

Не существует алгоритма, который позволяет определить, совместимые или же нет сделаны доработки по отношению в предыдущей версии. И более того, даже сами разработчики не всегда это могут определить.

К примеру, мы декомпозируем одну полноценную доработку на мелкие задачи, после выполнения каждой из которых часть целой доработки в отключённом состоянии проходит весь pipeline и устанавливается в промышленный контур. Только когда все части доработки будут реализованы и протестированы, тогда эту доработку можно будет включить в промышленном контуре. При этом ещё одна пересборка не требуется, достаточно переключить тоггл в конфигурации. Учитывая такой подход, в какой момент мы должны повысить минорную версию? При выполнении первой части доработки, или последней? А если параллельно выполняется несколько доработок?

На практике, в такой серьёзно автоматизированной системе SemVer вырождается в следующую схему:

STATIC_PREFIX.BUILD_NUMBER

Где STATIC_PREFIX - практически всегда 1.0, а BUILD_NUMBER - номер сборки в pipeline. Фактически версионирование свелось к обычному порядковому номеру, с бесполезной припиской.

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

STATIC_PREFIX.PIPELINE_NUMBER.BUILD_NUMBER

Где STATIC_PREFIX равен 1, а PIPELINE_NUMBER увеличивается при смене pipeline. Вот такой SemVer.

Восприятие SemVer

Помимо разработки я ещё провожу аттестацию других разработчиков. Вопрос о SemVer задаётся всем сотрудникам в обязательном порядке. Вот тут я был очень удивлён, но многие разработчики не понимают, когда и какую часть версии следует увеличивать. Конечно это более характерно начинающим разработчикам, но и от матёрых ребят доводилось слышать, что после версии 1.0.9 должна следовать версия 1.1.0.

Задавая конкретный вопрос: "Когда увеличивается мажорная часть версии?", я часто получаю ответ "Когда происходят масштабные доработки программы". Эта проблема кстати тоже упоминается в указанной в начале статье.

Практика показывает, что увеличение мажорной части версии, далеко не всегда сопровождается "масштабными" изменениями. Оно скорее всего сопровождается масштабным рефакторингом с вашей стороны. Мой любимый нелюбимый пример такого изменения версии - переход со spring boot 2 на spring boot 3, где нужно по всему коду и по всем библиотекам изменить пакеты javax на jakarta.

Всё это заставляет сделать вывод, что части семантической версии вовсе не очевидны по своему смыслу, и создают заблуждения среди разработчиков.

Несколько мажорных версий

Вернёмся к примеру со spring boot. Требуемый рефакторинг для перехода между мажорными версиями нельзя сделать частично, поскольку невозможно одновременно использовать несколько версий одной библиотеки. И даже такой незначительный рефакторинг, как смена названий пакета, не всегда можно сделать за один раз. Либо всё, либо ничего.

Кроме того, выпуск новой мажорной версии означает прекращение развития предыдущей мажорной ветви приложения. В лучшем случае выпускаются исправления безопасности.

Разработчик в таких условиях попадает в неопределённую ситуацию: на одной чаше весов сложный рефакторинг, который не принесёт явной пользы, поскольку нет нового функционала, с другой - накопление техдолга, который только сильнее будет осложнять рефакторинг. Скажите, много ли из вас тех, кто уже забыл про java8?

И даже если рефакторинг стоит затраченного на него времени, всё равно будет сложный переходный период. Вспомните как изощрялись linux дистрибутивы для одновременной поддержки python 2 и 3.

CalVer - календарное версионирование

Календарное версионирование преподносится как способ решение проблем семантического версионирования. У CalVer нет строгой схемы, есть только набор рекомендаций и вариантов построения, и каждый может настроить вид версии на свой вкус. Но если брать условно среднее, то схема получится такой:

YY.MM.PATCH

Где YY - год, MM - месяц, PATCH - порядковый номер.

Посмотрим как CalVer решит описанные выше проблемы:

  1. Автоматически рассчитать и назначить версию можно без проблем, поскольку требуется лишь текущая дата.

  2. С восприятием тоже всё хорошо. Год и месяц поймут даже люди далёкие от разработки ПО.

  3. А вот проблему нескольких мажорных версий с помощью CalVer решить не получится.

Недостатки CalVer

Хоть CalVer не решает все наши проблемы, она достаточно хороша. Признаюсь честно, до момента возникновения замысла этой статьи, я и сам склонялся к идеи использовать CalVer для своих приложений. Для меня, и для многих других разработчиков её достаточно. Но...

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

По CalVer при переходе с одной версии на другую невозможно понять, были ли несовместимые изменения в новой версии по отношению к старой!

Я считаю это очень серьёзным недостатком методологии, даже не смотря на то, что в моих приложениях этот недостаток скорее всего бы не проявился.

Требование к методологии версионирования

В статье, на которую я сослался в начале, выдвигаются 3 требования, которые должны выполняться при версионировании приложений:

  1. Версия должна быть числом.

  2. Версия должна только увеличиваться.

  3. Номер версии должен коррелировать с уровнем развития ПО. Чем выше версия, тем более зрелое приложение.

Я предлагаю добавить ещё 4 требования.

4. Все части версии должны нести смысл

Не должно в версии быть констант вроде 1.0. Каждая часть версии должна давать какую-то полезную информацию.

5. Версия должна отражать несовместимые изменения

Не должна повторяться ошибка, которую допустили в CalVer.

6. Версия должна строго вычисляться алгоритмически

Требования для удобства настройки CI/CD.

7. Пересборка одной и той же версии кода должна давать тот же результат

Последний пункт больше относится к системе сборки, но методология версионирования точно не должна его нарушать.

Внезапно немного биологии

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

Вид — это репродуктивно связанная совокупность популяций.

Другими словами, особи одного вида способны свободно скрещиваться между собой и давать плодовитое потомство. Как только между особями теряется репродуктивная совместимость, то можно говорить о разделении на новые виды. А для нового вида необходимо придумать новое название.

Скрытый текст

Я понимаю, что при биологическом разделении невозможно сказать, какой из видов новый, по отношению к исходному. Более правильно сказать, что исходный вид вымер, но при этом эволюционировал в 2 или более разных новых видов.

К чему была эта биологическая вставка? При несовместимых изменениях между особями одного вида, выделяется новый вид. Давайте воспользуемся аналогичным приёмом при версионировании приложений.

EvoVer - эволюционное версионирование

Предлагаю ввести новую методологию, в которой возьмём лучшие свойства из SemVer, CalVer и биологической систематики. К вашему вниманию - эволюционное версионирование (EvoVer).

SPECIES EPOCH[.PERIOD].GENERATION
ВИД     ЭПОХА[.ПЕРИОД].ПОКОЛЕНИЕ

Где:

  • EPOCH (ЭПОХА) - увеличивается при переходе в следующую эпоху. Лучше всего использовать год, в любом удобном для вас формате: 2026, 26.

  • PERIOD (ПЕРИОД) - опциональный элемент, увеличивающийся при переходе в следующий период. Это может быть квартал, месяц, неделя, спринт или что-нибудь ещё.

  • GENERATION (ПОКОЛЕНИЕ) - порядковый номер сборки в периоде/эпохе.

  • SPECIES (ВИД) - название вашего приложения/сервиса/библиотеки/фреймворка/плагина/API. Изменяется при несовместимых изменениях.

Пример того, как бы менялать версия приложения sirius при различных доработках:

SemVer

CalVer

EvoVer

Исходная версия, декабрь 2025

sirius 1.1.5

sirius 25.12.5

sirius 25.12.5

Совместимое добавление функционала, декабрь 2025

sirius 1.2.0

sirius 25.12.6

sirius 25.12.6

Совместимое исправление дефекта, январь 2026

sirius 1.2.1

sirius 26.01.0

sirius 26.01.0

Несовместимое изменение, январь 2026

sirius 2.0.0

sirius 26.01.1

altair 26.01.0

Совместимое исправление дефекта, январь 2026

sirius 2.0.1

sirius 26.01.2

altair 26.01.1

Совместимое исправление того же дефекта в предыдущей мажорной версии, январь 2026

sirius 1.2.2

нет возможности

sirius 26.01.1

А теперь попытаюсь предвидеть некоторые ваши вопросы и возражения.

Это же тот же CalVer, только с другими названиями!

Действительно, часть EPOCH[.PERIOD].GENERATION это и есть CalVer практически в чистом виде. Главное отличие здесь в том, что мы запрещаем несовместимые изменения в приложении, чего не было в CalVer.

Когда вы хотите сделать несовместимое изменение, вы должны создать другое приложение с другим названием.

Можете воспринимать EvoVer, как CalVer 2.0.0, но так как мы запрещаем несовместимые изменения, то придумываем для методологии новое название - EvoVer.

Почему именно ЭВОЛЮЦИОННОЕ версионирование?

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

И когда вам необходимо внести несовместимые изменения, вы не сбрасываете версию в 1.0.0 или даже в 0.0.1, а продолжаете развитие вашей идеи под новым названием. Вы не начинаете новое приложение с чистого листа, вы создаёте новую ветвь в дереве программного обеспечения.

Именно так и происходит эволюция.

А зачем новые названия для частей версии?

Исключительно поэтический момент. Чтобы была корреляция с названием методологии.

Первое требование - версия должна быть числом, а здесь появляется название приложения!

А версия и является числом! За версию отвечает часть EPOCH[.PERIOD].GENERATION, которая состоит только из чисел. SPECIES введён для того, чтобы отразить принцип работы с несовместимыми изменениями.

Почему PERIOD определён не чётко, и ещё опциональный?

Для той же гибкости, которая присуща CalVer.

В компании, где я работаю, используется квартальное планирование. Задачи, бюджет, отчётность, всё строится и выделяется на квартал. Логично и PERIOD использовать для указания квартала и увеличивать число в течении года от 1 до 4. Такой подход будет понятен бизнесу, и по номеру версии можно определять прогресс за учётный период.

Но не все компании используют квартальное планирование. Кому-то удобно будет использовать номер месяца, кому-то недели, кому-то спринты или что-нибудь ещё. Но если вы не видите полезного применения для PERIOD, то вспомните четвёртое требование: "Все части версии должны нести смысл". Не нужно - не используйте.

А если у меня несущественные несовместимые изменения. Зачем ради этого менять название?

Смотрите на свою ситуацию сами. Я могу сказать только одно:

Несовместимые изменения и точка!

Если вы тоже переходите со spring boot 2 на 3, то для конечных сервисов это не должно повлиять на совместимость, но для стартеров придётся придумать новое название.

Если вы хотите удалить устаревший метод, то лучше пометьте его deprecated. А когда устаревшего кода будет слишком много, и поддерживать его будет тяжело - создайте новое приложение. Считайте вашу первую попытку черновиком. А теперь настало время переписать всё на чистовик.

У меня программа в процессе проектирования, мы ещё не нашли нужное API.

Речь о мажорной версии 0. Посмотрите ещё раз, что я написал в предыдущем пункте. Если вы хотите несовместимо переделать API, у вас 2 пути:

  1. Написать рядом, а старое пометить deprecated.

  2. Создать новое приложение без всего лишнего.

Это относится не только к приложениям, но и к библиотекам, фреймворкам, API, плагинам, и ко всему, что можно версионировать.

Зато представьте какие вы получите от этого преимущества:

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

  • В новом API вы сможете предоставить адаптер для предыдущего решения, и упростить переход для ваших потребителей.

  • Гораздо проще становится управление зависимостями. Указывая версию зависимости, вы определяете минимально необходимую версию. Для сборки можно спокойно выбрать максимальную из требуемых транзитивно версий, а можно и просто самую свежую.

  • Если вам ваш проект кажется устаревшим, то не факт, что с вами согласятся те, кто от него зависят. Позвольте им продолжить развитие вашей первоначальной идеи, а сами развивайте новую.

Задумайтесь над тем, что вы пытаетесь сохранить при выпуске новой мажорной версии. Вы сохраняете только название! Нужно ли оно вам так сильно, чтобы попортить жизнь коллегам, которые зависят от вашего ПО?

Название является брендом! Мы вложили в него много средств и собрали вокруг инструмента комьюнити. Нам не выгодно менять его!

Спросите любого разработчика, что он думает по поводу перехода со spring boot 2 на spring boot 3. Ни один разработчик не будет в восторге от переименования javax на jakarta. Выпуск новой мажорной версии - удар по репутации.

Есть много случаев, когда несовместимой версии давали новое название, и это приводило к успеху:

А так же есть случаи, когда не стали переименовывать, и создали этим проблемы и скандалы:

  • Выпуск python 3 заставил linux дистрибутивы поддерживать обе версии языка, искусственно вводя пакет python3 наравне с python, что собственно и можно рассматривать как разные названия.

  • Gnome переделав полностью интерфейс в версии 3 получили массу критики, в том числе и от Линуса Торвальдса. По итогу мы получили развитие прошлой оболочки под названием Mate.

    Скрытый текст

    Лично мне новый gnome нравится больше

  • Ну и мой любимый нелюбимый пример, передача JavaEE в Eclipse с переименованием в JakartaEE, что породило огромное число бесполезных мажорных переходов, в роде spring boot 3, хотя можно было сделать переход аккуратнее.

Если вы всё же так дорожите своим названием, попробуйте найти компромисс:

  • Используйте дополнительное название для вашей мажорной версии, как это делается в Ubuntu.

  • Или включите мажорный номер в ваше название, а не в версию, как это делает Windows.

Но если мои доводы вас не убедили, и компромисс вас не устраивает, то поищите вдохновение в методологии версионирования, принятой в Haskell. Там целых 2 мажорные версии.

Итог

Я бы не сказал, что EvoVer является чем-то радикально новым. EvoVer - очередной шаг в развитии систем версионирования. Многие уже приходили к аналогичным мыслям. Даже автора SemVer посещала похожая идея.

Теперь EvoVer отправляется на ваш суд. В конце лишь приведу сводную таблицу со сравнениями всех методологий:

SemVer

CalVer

EvoVer

Требования к версионированию

Версия должна быть числом

+

+

+

Версия должна только увеличиваться

+

+

+

Номер версии должен коррелировать с уровнем развития ПО

+

+

+

Расширенные требования

Все части версии должны нести смысл

+

+

+

Версия должна отражать несовместимые изменения

+

-

+

Версия должна строго вычисляться алгоритмически

-

+

+

Пересборка одной и той же версии кода должна давать тот же результат

+

+

+

Дополнительные свойства

Прозрачный переход мажорной версии от 0 к 1

-

+

+-

Неоправданные ожидания от мажорного перехода

-

+

+

Существования нескольких мажорных версий

-

-

+