Семантическое версионирование вас не спасет

Семантическое версионирование вас не спасет

это перевод статьи Semver Will Not Save You

Популярный питоновский модуль cryptography перешел на использование Rust для низкоуровневого кода, что породило весьма эмоциональный тред на Гитхабе. Помимо энтузиастов 32-битного оборудования 1990-х годов, была активная группа, которая требовала от разработчиков строго придерживаться сематического версионирования, утверждая, что это могло бы предотвратить все беды. Я покажу вам не только почему это неправильный подход, но и как семантическое версионирование может вам навредить.

Посвящается Алексу и Полу, которые готовы принять огонь на себя за всех нас.

Номера версий

Давайте начнём с того, что определим основную задачу версионирования: показывать, какая версия объекта новее другой. Это можно использовать где угодно, но мы сосредоточимся на программном обеспечении.

Софтверное сообщество привыкло интерпретировать номера версий как кортежи целых чисел, разделенные точкой, читаемые слева направо. Таким образом, 2.0 новее, чем 1.10.0, которая, в свою очередь, новее, чем 1.9.42. В сообществе Python есть PEP 440, который формализует это поведение.

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

Семантическое версионирование

На протяжении многих лет люди пытались внести смысл в эти цифры(разумеется, из благих побуждений). Пожалуй, самый популярный подход - это семантическое версионирование. У вас есть MAJOR.MINOR.MICRO и обещание состоит в том, что пока MAJOR не изменится, ничего не сломается и вы можете спокойно обновлять свои зависимости. Если, конечно, мажорная версия не ноль - в этом случае разработчик волен делать всё, что угодно.

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

Закон Хайрама

Давайте начнём с нарушенных обещаний и, конечно же, на эту тему есть xkcd: Hyram's lawWorkflow by xkcd

Он передает один из фундаментальных законов разработки программного обеспечения:

“При достижении достаточного количества пользователей API уже неважно, какие его особенности вы обещали всем: для любой из возможных особенностей поведения вашей системы найдётся зависящий от неё пользователь.” - Хайрам Райт, Закон Хайрама

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

Вы хотите заявить, что версия 3.2 каким-то образом совместима с версией 3.1, но откуда вам знать? Вы знаете, что всё работает, потому что код покрыт юнит-тестами, но наверняка при переходе на новую версию вы поменяли и тесты, раз уж изменилось поведение, но как вы можете быть уверены, что вы не удалили или не изменили необходимый кому-либо функционал?

За почти 20 лет профессиональной разработки программного обеспечения я заметил, что количество непреднамеренных поломок из-за обновлений намного превышает количество поломок преднамеренных.

Конечно, в этом утверждении есть нюанс. Я пишу это с точки зрения программиста на Python и Go и Python - из-за его динамической природы - гораздо чаще страдает от проблем, вызванных непреднамеренными сайд-эффектами. С другой стороны, прямо сейчас я разгребаю последствия непреднамеренной несовместимости между двумя минорными релизами библиотеки в C.

В сущности, полагаться на то, что если разработчик не планировал ничего сломать, то обновление ничего не сломает, всё равно что полагаться на то, что в ПО не бывает багов.

Всё это не означает, что SemVer плохое или бесполезное. Знать намерения разработчика может быть полезно - особенно, когда что-то ломается. Хотя бы потому, что SemVer это, по сути, TL;DR чейнджлога.

Что это означает на самом деле - так это то, что вы не можете полагаться на семантическое значение SemVer и должны рассматривать каждое обновление как потенциально опасное. Если поднятие минорной версии ещё никогда не ломало ваш продакшен, вам просто надо подождать ещё немного. В конце концов вам тоже повезет - я обещаю.

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

Принимаем ответственность

Но есть кое-что, с чем лучше разобраться, чем жить перед ним в страхе.

– Джо Аберкромби, “Кровь и железо"

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

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


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

  1. Используйте тесты с хорошим покрытием.
  2. Закрепите точные версии ваших зависимостей и их зависимостей тоже. Модули в Go, Cargo в Rust и npm в JavaScript поступают так по-умолчанию. Я использую pip-tools, но и простой pip freeze лучше чем ничего. Вы должны отделить свои зависимости, в которых написано, например, Flask, от ваших pin-файлов, в которых указано Flask == 1.1.2, вместе с зависимостями Flask и, в идеале, их хешами. В противном случае каждая сборка - это лотерея.
  3. Регулярно пробуйте обновлять ваши зависимости до их последних версий. Существует специальный инструментарий, который поможет вам в этом.
  4. Если тесты прошли, закрепите новые версии. Если нет, то:
    • Почините и затем закрепите и закоммитьте новые версии.
    • Если какая-то версия зависимости сломана и мейнтейнер планирует починить её в следующем релизе, заблокируйте эту конкретную версию (например, Flask!=1.1.2, но не Flask<1.1.2)
    • Если же намеренно были произведены изменения, ломающие обратную совместимость и при этом была поднята мажорная версия, заблокируйте её (например, Flask<2), но, если только у вас нет договоренности о долгосрочной поддержке старой версии, немедленно начните адаптировать ваш код к новой версии. Или поищите альтернативные варианты.
  5. GOTO 3

Ничего не мешает вам использовать тот же процесс для опенсорсных проектов. Сервисы вроде Dependabot помогут вам в этом.


Собственно, это всё, что вам нужно сделать, чтобы сторонние пакеты не сломали ваш проект или даже ваш бизнес. Большинство тех, кто был недоволен тем, что изменения в пакете cryptography сломали их билды, просто не следовали шагу №2.

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

Это так же справедливо для экосистем Rust или Go, в которых SemVer вшит в систему управления пакетами2. Большинство непредвиденных последствий, которые я перечислю далее, применимы и к ним тоже.

Непредвиденные последствия: ZeroVer

Зачастую разработчики полагают, что они обязаны придерживаться SemVer, порой из-за незнания альтернативы, порой из-за того, что “все так делают” а порой - по настоятельному требованию влиятельных пользователей. И в то время как в теории SemVer обещает больше свободы(технически, вы можете ломать обратную совместимость с каждым релизом, только не забывайте бампать мажорную версию), в реальности он лишь добавляет давления и работы.

В результате этого давления мы и наблюдаем то, что в шутку называют 0-based Versioning.

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

Стандарт SemVer четко обозначает, что проект, готовый к использованию в продакшене должен быть версии 1.0. К сожалению, неприятие увеличения мажорной версии - совершенно обычное дело.3 Так что люди застревают в 0ver и номера версии больше ничего не значат, хотя она и заявлена как семантическая.

По сути, дистрибутив который позиционирует себя как production-ready и в то же время имеющий нулевую мажорную версию - это парадокс!

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

Непредвиденные последствия: Отсутствие обновлений безопасности

Причина, по которой многие были злы на разработчиков пакета cryptography в том, что они были убеждены, что если уж пакет следует правилам SemVer, то достаточно закрепить мажорную версию и ничего не сломается4.

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

На самом деле, история с закреплением версий даже хуже: Большинство опенсорс-проектов не имеют возможности поддерживать несколько мажорных веток.

Open Source Reality Open Source Reality by CommitStrip

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

В отличие от npm, в основных утилитах управления пакетами в Python нет концепции уязвимых версий, для решения этих проблем вам необходим дополнительный инструментарий. Это означает, что закрепление мажорной версии ваших зависимостей в один прекрасный день приведет к тому, что ваше приложение будет битком набито уязвимостями из CVE и вы никогда об этом не узнаете. 5

Но подождите, будет ещё хуже!

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

Представьте приложение, которое зависит от чудесной urllib3 и ваш пакет тоже. Теперь, если вы закрепите версию urllib3 на <2, пользователь вашего пакета не сможет когда-либо снова получить обновление от urllib3, как только urllib3 повысит свою основную версию до 2 и выше.6 Они могут даже не осознавать насколько серьезны их проблемы.

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

Никогда не ограничивайте мажорные версии (вроде <2), если только вы не уверены, что они не работают.

Некоторые питоновские утилиты управления пакетами адоптировали закрепление мажорной версии в стиле npm (^) по умолчанию, несмотря на отсутствие таких же, как у npm, механизмов обеспечения безопасности. Не забудьте убрать это вручную, если возможно.

Неожиданные последствия: Конфликт версий

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

Version Conflict Version Conflict.

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

Заключение

Если вы разработчик и вам нравится SemVer как дополнительный плюс для пользователей: дерзайте! Я здесь не для того, чтобы учить вас как тратить ваше время. В добавлении семантики в версии есть смысл.

Я здесь для того, чтобы сказать - использование SemVer совершенно необязательно и если это вас напрягает, или вы навечно застряли на планете 0ver(что означает, что оно вас напрягает, но вы этого не замечаете или не признаете), то рассмотрите некоторые альтернативы.


Как пользователь, я надеюсь, что я показал, вера в SemVer:

  • не предотвратит проблем. В лучшем случае, отложит их. Что ещё хуже.
  • …приводит к конфликтам версий, что расстраивает ваших пользователей.
  • …приводит к проблемам с безопасностью, что, в свою очередь, расстраивает вашего босса и ваших потребителей.
  • …увеличивает нагрузку на разрабочиков, что делает несчастными уже их.

Существует множество известных проектов, которые выглядят как SemVer, но таковыми не являются: Linux, Python, Django, glibc… и это нормально!

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


  1. Как мейнтейнеры setuptools, которые на момент написания статьи достигли версии 53.1.0. Следствием этого является то, что любой проект с однозначным номером версии, в котором не используется лазейка типа 0.x, скорее всего, достаточно слабо заботится о своем версионировании, чтобы быть истинным SemVer. ↩︎

  2. Добавляя foo = 1.0 в cargo.toml вы можете быть уверены, что будут установлены только пакеты серии 1.0. Бамп мажорной версии в Go означает создание нового пути импорта. Что настолько болезненно, что даже Google пытается избежать этого в своих собственных проектах. ↩︎

  3. Hauptversionserhöhungsangst! (боязнь увеличения основного номера версии(нем.) - прим. перев.) ↩︎

  4. Довольно забавно, что изменения в системе билда, которые не влияют на публичный интерфейс фактически не нарушают правил и не требуют бампа мажорной версии - по сути, они ломают совместимость с платформами, которые никогда и не поддерживались авторами - но давайте оставим это за скобками. ↩︎

  5. Также имейте в виду, что многие небольшие проекты никогда не регистрируются в CVE и просто исправляют проблемы с безопасностью по мере их поступления. Таким образом, даже новые причудливые предупреждения системы безопасности GitHub вам не помогут. ↩︎

  6. Это не такая серьёзная проблема для языков, вроде Rust, Go или JavaScript, где можно установить более одной версии пакета. Но у вас все еще есть проблема, что ваш пакет работает с небезопасной версией зависимости. ↩︎

comments powered by Disqus