Семантическое версионирование (SemVer)
Семантическое версионирование (SemVer)
Семантическое версионирование — это соглашение записывать номер версии тремя числами
MAJOR.MINOR.PATCHи менять каждое из них по строгому правилу, чтобы номер сам по себе отвечал на вопрос «сломается ли у меня что-то, если я обновлюсь». Не описание размера работы, а обещание про совместимость.
История
До конца 2000-х номер версии значил всё что угодно. Компании нумеровали релизы под маркетинг: Windows шёл 95 → 98 → 2000 → XP → Vista → 7. Adobe выпускала Photoshop CS3, CS4, CS5. Solaris прыгнул с 2.6 на 7. Никакого общего смысла в цифрах не было — они продавали, а не сообщали.
Пока софт ставили с дисков, это никого особо не мучило. Проблема выросла вместе с менеджерами пакетов. Когда твой проект тянет 40 чужих библиотек, а каждая из них тянет ещё по 20, и всё это обновляется само — тебе физически нужен способ отличить «безопасное обновление» от «после него всё встанет». Разработчики в 2000-х называли это состояние dependency hell (ад зависимостей).
В 2010 году Том Престон-Вернер (Tom Preston-Werner), один из сооснователей GitHub, опубликовал документ Semantic Versioning. Он не изобрёл схему major.minor.patch — так нумеровали десятилетиями. Он сделал другое: записал её как формальную спецификацию с обязательствами. Не «принято считать, что», а «ЕСЛИ ты объявил, что следуешь SemVer, ТО ты обязан».
Вехи:
- 2010 — первая публикация на
semver.org. Хорошо легла на экосистему npm (менеджер пакетов Node.js, вышел в 2010 же), которая быстро сделала SemVer своим фундаментом. - июнь 2013 — выходит SemVer 2.0.0. Это актуальная версия по сей день. Доработали синтаксис пре-релизов и метаданных сборки, уточнили правила сравнения. С тех пор спецификация практически не менялась — довольно редкий случай стабильности для айтишного стандарта.
- 2018 — Расс Кокс (Russ Cox) в Google предлагает для языка Go Semantic Import Versioning: мажорную версию впаивают прямо в путь импорта (
example.com/lib/v2). Радикальная идея — сделать несовместимость видимой в самом коде, а не только в конфиге. - сегодня — SemVer лежит в основе npm, Cargo (Rust), RubyGems, Packagist (PHP), NuGet (.NET), Go modules, частично PyPI и Maven Central. Это де-факто стандарт для всего, что называется «пакет».
Владельца в корпоративном смысле у спецификации нет: она опубликована под лицензией Creative Commons, живёт на GitHub, есть официальный перевод на русский на самом semver.org. Читается за 15 минут — это редкий стандарт, который реально можно прочесть целиком.
Что это такое
Версия — это три числа через точки:
2.1.153
│ │ └── PATCH — исправили баг, ничего не сломали
│ └───── MINOR — добавили возможность, старое работает как раньше
└─────── MAJOR — сломали обратную совместимость
Правила простые до неприличия:
- PATCH увеличивают, когда чинят ошибку и не меняют внешнее поведение.
1.4.2 → 1.4.3. - MINOR увеличивают, когда добавляют функциональность обратно совместимо — то есть весь код, который работал на
1.4.3, работает и на1.5.0. PATCH при этом обнуляется:1.4.7 → 1.5.0. - MAJOR увеличивают, когда ломают совместимость: убрали функцию, переименовали параметр, изменили формат ответа. Обнуляется всё правое:
1.9.12 → 2.0.0.
Дополнительные правила, которые чаще всего забывают:
Ноль в мажоре — зона без правил. Версии 0.y.z означают «раннюю разработку». Автор ничего не обещает, сломать может в любой момент. Именно поэтому 0.x в проде — всегда осознанный риск.
Опубликованная версия неизменна. Выпустил 1.2.3 — всё, содержимое зафиксировано навсегда. Нашёл ошибку — выпускай 1.2.4. Это не бюрократия, а основа доверия: если 1.2.3 вчера и 1.2.3 сегодня могут отличаться, весь смысл номеров испаряется.
Числа сравниваются как числа, а не как текст. 1.0.10 больше, чем 1.0.9. Ведущие нули запрещены.
Пре-релизы пишутся через дефис и считаются младше обычной версии: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-beta < 1.0.0-rc.1 < 1.0.0. Это позволяет выкатывать кандидаты, не занимая финальный номер.
Метаданные сборки пишутся через плюс — 1.0.0+20260903.a1b2c3 — и при сравнении игнорируются полностью. Это место для хеша коммита или номера сборки, а не для смысла.
SemVer vs CalVer. Календарное версионирование ставит в номер дату: Ubuntu 24.04 — это апрель 2024 года, JetBrains 2026.1 — первый релиз года. CalVer отвечает на вопрос «насколько это старое», SemVer — на вопрос «сломается ли». Разные вопросы, поэтому это не конкуренты, а выбор в зависимости от того, что важнее твоим пользователям.
SemVer vs маркетинговое версионирование. Chrome и Firefox инкрементируют мажор каждые четыре недели просто по календарю релизов. Ядро Linux перешло с 2.6.39 на 3.0 в 2011 году, и Линус Торвальдс прямо сказал, что причина — юбилей и то, что цифры стали слишком длинными, а не поломка совместимости. Это не SemVer и не притворяется им.
SemVer — это обещание, а не свойство
Номер версии не вычисляется машиной из кода. Его ставит человек, исходя из того, что он считает ломающим изменением. Поэтому SemVer работает ровно настолько, насколько дисциплинирован автор пакета. Это социальный контракт в одежде технического стандарта.
Аналогии из жизни
Издания книги
Третье издание, исправленное и дополненное. PATCH — допечатка тиража с исправленными опечатками, страницы те же. MINOR — дополненное издание: добавили главу в конец, всё прежнее на местах. MAJOR — новое издание с перекроенной структурой: главы переставлены, нумерация страниц уехала, и все ссылки «см. с. 245» в чужих работах теперь ведут не туда.
Где ломается: у книги нет автоматической проверки. Если ты цитируешь второе издание, а читатель держит третье — никто не выдаст ошибку, человек просто откроет не ту страницу и не поймёт почему. В софте наоборот: несовместимость чаще всего проявляется громко и сразу. И ещё: издательство нигде формально не обещает, что в «дополненном» издании старая нумерация уцелеет. У SemVer это обещание — центральное.
Розетки и вилки
PATCH — внутри розетки заменили пружинящий контакт на более надёжный, снаружи ничего не изменилось. MINOR — рядом с двумя гнёздами добавили USB-порт: новая возможность, старые вилки втыкаются как прежде. MAJOR — сменился тип розетки, европейская вилка в британскую не лезет физически.
Где ломается: в электрике совместимость двоичная — влезло или не влезло. В софте она частичная и коварная: код может успешно подключиться к новой версии библиотеки, отработать без единой ошибки и выдать другой результат. Хуже того — важнейший параметр розетки, напряжение, в форме вилки вообще не отражён: 110 В и 220 В бывают на одинаковых разъёмах. В SemVer ровно та же дыра — номер версии молчит про то, что функция стала работать в десять раз медленнее или начала жрать втрое больше памяти. Формально совместимо, практически катастрофа.
Меню ресторана
PATCH — повар перешёл с крупной соли на мелкую, вкус тот же. MINOR — в меню добавили три новых позиции, старые никуда не делись. MAJOR — карбонару теперь готовят со сливками, а том-ям убрали совсем.
Где ломается: ресторан не хранит прошлые версии меню — нельзя «откатиться на прошлый квартал», а в софте старые версии остаются доступны годами, и это принципиально. И вторая трещина: посетитель с аллергией зависел от того, чего в меню написано не было. Повар честно считал замену масла мелочью уровня PATCH. Для этого гостя это было ломающее изменение.
Как это работает
Разберём механику по шагам — от автора библиотеки до твоего компьютера.
Шаг 1. Автор решает, что изменилось. Он смотрит на публичный API (публичный интерфейс — то, чем пользуются снаружи) и отвечает: может ли существующий чужой код перестать работать? Да — MAJOR. Нет, но появилось новое — MINOR. Ни того, ни другого — PATCH.
Шаг 2. Автор публикует. Пакет уезжает в реестр (npm, PyPI, crates.io) под этим номером и там замораживается навсегда.
Шаг 3. Ты объявляешь не версию, а диапазон. В этом главная практическая ценность SemVer. Ты пишешь в конфиге не «хочу ровно 1.4.2», а «хочу любую, совместимую с 1.4.2»:
^1.4.2 → от 1.4.2 включительно до 2.0.0 не включая
(карет: «обновляй, пока не ломают»)
~1.4.2 → от 1.4.2 включительно до 1.5.0 не включая
(тильда: «только исправления багов»)
1.4.2 → ровно эта, без вариантов
Отдельная ловушка: для нулевого мажора ^0.4.2 означает >=0.4.2 <0.5.0 — карет там ведёт себя как тильда, потому что в 0.x минорная версия и играет роль мажорной.
Шаг 4. Резолвер решает головоломку. У тебя 40 прямых зависимостей, у них свои. Программа-резолвер ищет набор конкретных версий, удовлетворяющий всем диапазонам сразу. Классическая проблема здесь — ромбовидная зависимость (diamond dependency): библиотека A требует C версии ^1.0, библиотека B требует C версии ^2.0. Одна общая версия C, удовлетворяющая обоим, не существует. Дальше или конфликт, или менеджер ставит две копии C рядом (npm умеет, Python — почти нет).
Шаг 5. Lock-файл фиксирует результат. package-lock.json, Cargo.lock, poetry.lock, uv.lock — это протокол решения: какие именно версии победили, с хешами. Благодаря ему сборка на твоей машине, у коллеги и на сервере даёт побайтово одно и то же.
Ты пишешь: "lib": "^1.4.2" ← намерение, диапазон
Резолвер решил: lib 1.7.3 ← факт, записан в lock
Через месяц: lib 1.9.0 вышла ← подтянется при обновлении,
но не сама по себе
Диапазон без lock-файла — это лотерея
Если в репозитории лежит ^1.4.2 и нет lock-файла, сборка сегодня и сборка через неделю поставят разные версии. Проект «ломается сам по себе», хотя код никто не трогал. Lock-файл обязан лежать в репозитории — это не мусор и не артефакт сборки.
Где встречается в обычной жизни
- «Требуется iOS 18.2 или новее». Приложение объявляет минимальную совместимую версию системы — ровно та же логика нижней границы диапазона.
- Обновления игр. «Патч 1.0.7» правит баланс и не трогает сохранения, а вот выход второй части обычно означает, что старые сейвы не перенесутся.
- Автомобили. Рестайлинг — это MINOR: бамперы новые, но фары и запчасти в основном подходят. Смена поколения — MAJOR: не подходит почти ничего.
- Редакции законов и ГОСТов. «В редакции от 03.09.2026» — по сути метка версии. Ссылка на статью 12 может указывать на разное в разных редакциях, и это ровно та же поломка обратной совместимости.
договор_final_v2_испр_ПОСЛЕДНИЙ.docx. Это то, что происходит, когда версионирования нет вообще. Каждый, кто хоть раз искал в почте актуальный файл, лично пережил цену отсутствия схемы.
Где встречается в IT и бизнесе
- Управление зависимостями. Основное применение. Боты вроде Dependabot и Renovate автоматически открывают запросы на обновление, и решение «мержить автоматически или звать человека» принимается именно по тому, какая цифра выросла.
- Версионирование API.
/api/v1/orders→/api/v2/orders. В URL выносят только мажор: минорные добавления не требуют от клиента ничего. Тут нужен ключевой нюанс — у API поставщик обязан держать v1 живой, пока клиенты не переедут, потому что клиенты не под его контролем. - Реакция на уязвимости. Когда в декабре 2021 года нашли Log4Shell в библиотеке Log4j, весь мир за неделю проходил цепочку
2.14.1 → 2.15.0 → 2.16.0 → 2.17.0. Возможность быстро ответить на вопрос «какая версия у нас стоит и достаточно ли она новая» стоила компаниям миллионов. - Контракты и SLA. «Поддерживаем текущую мажорную версию и одну предыдущую, минимум 24 месяца» — это конкретное коммерческое обязательство, выраженное через номера версий.
- Совместимость клиента и сервера. Мобильное приложение версии 3.4 и бэкенд версии 5.1. Здесь SemVer сам по себе не спасает — нужен отдельный механизм: клиент при подключении сообщает свою версию, сервер решает, обслуживать его, деградировать функциональность или потребовать обновления.
Кто пользуется
- npm — крупнейший реестр пакетов в мире, порядка нескольких миллионов пакетов. SemVer в нём не рекомендация, а встроенное в инструмент правило: синтаксис
^и~— часть форматаpackage.json. - Cargo (Rust) — считается образцовой реализацией. Каретка по умолчанию,
Cargo.lockв репозитории, отдельный инструментcargo-semver-checksумеет автоматически проверять, не сломал ли ты совместимость, не полагаясь на добросовестность автора. - Kubernetes — минорный релиз примерно раз в четыре месяца (три релиза в год), плюс собственная схема зрелости API:
v1alpha1→v1beta1→v1. Обещание стабильности растёт вместе с меткой. - Go modules — единственная массовая экосистема, где мажорная версия физически видна в коде:
import "example.com/lib/v2". Побочный эффект приятный — v1 и v2 могут спокойно сосуществовать в одной программе. - PostgreSQL — с версии 10 перешёл на двухчастную схему:
16.2означает мажор 16, патч 2. Минорной части нет вовсе, потому что в базах данных промежуточной категории «добавили функциональность, ничего не сломав» практически не бывает. - Anthropic — модели именуются иначе:
claude-haiku-4-5-20251001, где есть номер поколения и дата. Это ближе к календарному подходу и решает другую задачу — зафиксировать «ту самую» модель, а не описать совместимость.
Альтернативы и конкуренты
CalVer (календарное версионирование). Ubuntu 24.04, JetBrains 2026.1, pip 26.1.
Плюсы: сразу видно возраст релиза; отлично для продуктов с фиксированным графиком выпуска; не надо спорить, ломающее ли это изменение.
Минусы: номер ничего не говорит о совместимости; автоматика обновлений на него не опирается.
Sequence-based / маркетинговое. Chrome 145, Firefox 138, Windows 11.
Плюсы: понятно человеку — больше значит новее; никаких споров внутри команды.
Минусы: для машин бесполезно; заставляет держать отдельную, невидимую снаружи схему совместимости.
Хеш коммита как версия. Встречается во внутренних сервисах: build-a1b2c3d.
Плюсы: абсолютно точен, всегда однозначно указывает на код.
Минусы: невозможно сравнить «больше/меньше», нельзя выразить диапазон, нельзя понять, что новее.
ZeroVer — «вечная 0.x». Полушутка-полудиагноз (есть сайт 0ver.org, собирающий примеры): проект годами не выпускает 1.0, чтобы не брать на себя обязательств. Terraform прожил в 0.x до 2021 года.
Плюсы: честно, если API правда нестабилен.
Минусы: если проектом уже пользуются в проде, это не честность, а уклонение — пользователи всё равно рассчитывают на стабильность.
Когда НЕ стоит использовать
Когда у продукта нет API. Для приложения с интерфейсом для людей SemVer не даёт ничего: пользователь не «интегрируется», он просто нажимает кнопки. Календарная схема или сквозной номер подходят лучше, потому что отвечают на реальный вопрос пользователя — «у меня свежее?».
Когда ты не готов держать обещание. SemVer хуже отсутствия схемы, если объявить его и нарушать. Люди настроят ^1.0.0 в расчёте на автоматическое безопасное обновление, и ломающее изменение прилетит им в прод ночью, без единого предупреждения. Ложное обещание опаснее честного отсутствия обещания.
Когда «ломающее изменение» невозможно определить. Тут в дело вступает закон Хайрама (Hyrum's Law, сформулирован инженером Google Хайрамом Райтом): при достаточном количестве пользователей API неважно, что записано в контракте — от любого наблюдаемого поведения системы кто-нибудь да зависит. Порядок ключей в ответе, текст сообщения об ошибке, скорость выполнения. Формально ты чинишь баг и ставишь PATCH — а у кого-то на этом баге держался прод.
Где встретилось у меня
Третьего сентября утром автоматика, которая пишет записи в журнал проектов, отработала вхолостую: тридцать три вызова подряд вернули одно и то же — установленная версия CLI не поддерживает запрошенную модель, требуется версия свежее. Между установленной и требуемой лежала почти сотня патч-релизов при неизменных мажоре и миноре.
И это отличная иллюстрация к главному ограничению SemVer. По букве стандарта рост только патч-номера означает «только исправления багов, ничего не сломано». По факту в этой сотне патчей появилась поддержка новых моделей — то есть версия оказалась несовместима не с моим кодом, а с сервером на другом конце. SemVer описывает контракт между библиотекой и кодом, который её вызывает. Совместимость клиента с удалённым сервисом — совсем другая задача, и решается она не номером версии, а согласованием возможностей при подключении. Ошибка, кстати, была написана правильно: она назвала и текущую версию, и минимально требуемую, и команду обновления.
Вторая половина дня прошла в сверке рекламных кампаний с базой проектов: около 1700 групп и 13 тысяч объявлений, чтение через API без единой записи. Там всплыла родственная беда — расхождение между двумя источниками правды, которое накопилось незаметно, потому что связи только добавлялись и никогда не снимались. По сути тот же класс проблем, что и версии зависимостей: состояние, которое дрейфует, пока его специально не сверишь.
Что сделать на этой неделе
Найди в своих проектах места, где версия зависимости записана диапазоном без lock-файла, и зафиксируй их. А в любом коде, который ходит во внешний API, добавь понятную обработку ответа «твоя версия устарела»: сообщение должно называть текущую версию, требуемую и способ обновиться. Ошибка, которая сама говорит, что делать, экономит часы.
Связанные понятия
- Обратная совместимость (backward compatibility) — свойство новой версии принимать всё, что работало со старой.
- Прямая совместимость (forward compatibility) — способность старой версии не падать, встретив данные от новой; редкое и дорогое свойство.
- Lock-файл — снимок конкретных версий всех зависимостей с хешами, обеспечивающий воспроизводимость сборки.
- Ромбовидная зависимость (diamond dependency) — два ваших пакета требуют несовместимые версии третьего.
- Deprecation (устаревание) — пометка «функция скоро исчезнет» перед реальным удалением в следующем мажоре; вежливая форма ломающего изменения.
- Закон Хайрама (Hyrum's Law) — при достаточном числе пользователей все наблюдаемые особенности поведения системы становятся чьими-то зависимостями.
- Feature detection (определение возможностей) — альтернатива проверке версии: спрашивать «умеешь ли ты X», а не «какая ты версия».
Литература и источники
- Официальная спецификация:
semver.org— есть полный русский перевод, читается за 15 минут. Первоисточник, всё остальное это пересказ. - Calendar Versioning:
calver.org— разбор календарного подхода и того, кто и почему его выбрал. - Документация Cargo, раздел «Specifying Dependencies» (
doc.rust-lang.org/cargo) — самое внятное объяснение работы каретки и тильды. - «Software Engineering at Google», Titus Winters, Tom Manshreck, Hyrum Wright, 2020 (en; есть перевод на русский). Глава про зависимости и глава, где сформулирован закон Хайрама. Лучший разбор того, где SemVer перестаёт работать в большом масштабе.
- Wikipedia:
ru.wikipedia.org/wiki/Семантическое_версионирование— краткая справка с примерами. - Для практики поиск по запросу «npm semver calculator» — есть интерактивные калькуляторы, показывающие, какие версии попадают в заданный диапазон. Пять минут с ним объясняют каретку лучше любого текста.
Краткое резюме
MAJOR.MINOR.PATCH: мажор растёт при ломающих изменениях, минор при обратно совместимых добавлениях, патч при исправлениях.- Практический смысл не в самом номере, а в диапазонах:
^1.4.2означает «обновляй, пока не ломают», и именно это позволяет получать исправления безопасности автоматически. - Диапазон обязан сопровождаться lock-файлом в репозитории, иначе сборка перестаёт быть воспроизводимой.
0.x— зона без обязательств. Опубликованная версия неизменна навсегда.- SemVer — обещание живого человека, а не свойство кода. Закон Хайрама гарантирует, что даже честный автор рано или поздно сломает кому-то прод патч-релизом.
- Совместимость клиента с удалённым сервером номер версии не описывает — для этого нужно отдельное согласование возможностей и понятная ошибка «обновись до N».