Semver — семантическое версионирование
Сегодня хочется затронуть важную и сложную тему — версионирования программного обеспечения. В сегодняшних реалиях, когда код пишется в поражающем по скорости темпе, когда проекты декомпозируются в взаимосвязанные микросервисы, а ежедневные обновления фреймворков и библиотек требуют особого инфраструктурного внимания — рано или поздно вас настигает проблема контроля зависимостей.
В этих жестких условиях постоянных изменений очень важно сохранять констистентность и работоспособность ваших систем и сервисов. В аду зависимостей (dependency hell), где каждое новое обновления проистекает как болезненный процесс — к вам на помощь придёт простая и красивая идея семантического версионирования.
Всё настолько просто, что объяснение можно вместить в одно предложение. Каждую доставку вашего ПО маркируйте следующей схемой: A.B.C
, где A
— это глобальные изменения, ломающие обратную совместимость; B
— доставка новых функций (работоспособность прошлых версий, соответственно, сохраняется); C
— мелкие правки, патчи и горячие фиксы. Также иногда добавляют D
, но через тире, что обозначает номер сборки (Build number), текущий спринт или любая другая мета-информация проекта.
Разберём пример. Допустим вы пишите библиотеку, решающую какую-то батчевую (множественную идентичную операцию для разных входящих данных) задачу. К примеру, делаем бекап-копии для N-файлов. В момент, когда всё готово и у вас есть реализованный метод Backup(string[] filePath);
— мы выпускаем версию 1.0.0 (иногда, предрелизные версии начинают инкрементировать от нуля, т.е. 0.0.1 — это дело уже личное дело каждого).
Допустим, что стало известно о каких-то мелких багах. Каждое изменение — добавляют единицу к patch-сегменту версии (к последней версии) и мы получаем продукт 1.0.4. Через неделю к вам приходит продукт оунер и предлагает добавить возможность ZIP-архивирования ваших сохранённых копий. Здесь есть два пути развития:
1. Мы сохраняем обратную совместимость и просто добавляем новый метод BackupZIP(string[] filePath);
. Тем самым все пользователи ничего не почувствуют при обновлении, их функционал продолжить работать. В таком случае нам нужно будет «накатить» минорную версию на единичку: 1.1.4;
2. Мы не сохраняем обратную совместимость и расширяем сигнатуру метода булевым флагом ZIP: Backup(string[] filePath, bool ZIP);
. Данный подход чреват тем, что пользователи, которые обновят библиотеку — получат неработающий проект и нужно будет вносить правки с их стороны. Конечно, в нашем случае изменения минимальны, буквально на строчку, но это ведь всего-лишь пример. Если мы ломаем обратную связь — инкремент приходится к первой мажорной цифре версии: 2.0.4. Изменение на целую версию должно предупредить пользователь, что при обновлении мы гарантировано получим проблемы с работоспособностью проекта.
Из пункта 1 и 2 можно сделать ещё один вариант, если ваш язык программирования поддерживает опциональные значения в сигнатуре методов. Таким образом Backup(string[] filePath, bool ZIP = false);
не поломает ничего и это изменение будет касаться минорного обновления значения версии.
Кроме триады значений я предпочитаю также добавлять номер версии сборки или же Build Number, который получаю через глобальные переменные окружения при сборке в CI. Почти везде в моих продуктах версии выглядят как-то так: 2.6.12-b34. Не могу сказать, является ли этот подход best practice, т.к. мы светим, возможно, не нужной информацией. Но мне такой формат ведения версий нравится чисто эстетически.
Многие вместо build number добавляют номер спринта, в котором было сделано обновление. Это полезно, когда мы интегрируемся с какими-то таск-менеджерами типа Jira.
Намного более подробно идея описаны по ссылке. Надеюсь, что это всё поможет разобраться с графом зависимостей и ваш каждый деплой будет содержать самый минимум возможных проблем.