Прямой ответ
Версия пакета должна иметь один источник истины. Распространённая ошибка — версия указана в нескольких местах, и при релизе обновляется не все, поэтому установленный пакет сообщает версию, не соответствующую опубликованной.
Нумерация следует семантическому версионированию:
| Изменение | Номер версии | Пример |
|---|---|---|
| Исправление без изменения интерфейса | Патч | 1.2.3 → 1.2.4 |
| Новая совместимая функциональность | Минорная | 1.2.3 → 1.3.0 |
| Ломающее изменение интерфейса | Мажорная | 1.2.3 → 2.0.0 |
Правила формирования версии для Python определены отдельно и допускают суффиксы предварительных выпусков: 1.3.0rc1, 1.3.0.post1. Инструменты установки учитывают эти суффиксы при разрешении версий; о нумерации — в статье про семантическое версионирование.
Единый источник версии — тег git, из которого версия извлекается при сборке:
# pyproject.toml
[project]
name = "my-package"
dynamic = ["version"]
[tool.setuptools.dynamic]
version = {attr = "my_package.__version__"}
# src/my_package/__init__.py
__version__ = "1.3.0"
Либо версия берётся непосредственно из тега, что исключает ручное обновление файла:
[build-system]
requires = ["setuptools>=68", "setuptools-scm>=8"]
build-backend = "setuptools.build_meta"
[tool.setuptools_scm]
При таком подходе версия определяется по ближайшему тегу и числу коммитов после него, поэтому расхождение между кодом и версией невозможно.
Порядок релиза:
1. Обновить журнал изменений.
2. Создать тег версии.
3. Конвейер собирает дистрибутивы из тега.
4. Конвейер публикует дистрибутивы в реестр.
5. Публикуется уведомление о версии.
Публикация выполняется из конвейера по тегу, а не вручную; о процессе — в статье про публикацию в PyPI.
Совместимость и обещание версии
Номер версии — обещание пользователю, и его нарушение ломает автоматические обновления.
Что считается ломающим изменением. Удаление или переименование публичной функции, изменение сигнатуры, изменение типа возвращаемого значения, изменение поведения по умолчанию. Всё это требует увеличения мажорной версии.
Что не считается ломающим. Добавление новой функции, новый необязательный параметр со значением по умолчанию, исправление ошибки, при котором поведение становится соответствовать документации.
Серая зона. Изменение поведения, которое было недокументированным, формально не нарушает интерфейс, но ломает код, который на него полагался. Такие изменения описываются в журнале явно, даже если версия увеличивается как минорная.
Обещание версии 0.x. Мажорная версия 0 означает, что интерфейс может измениться в любой минорной версии. Пользователи это понимают, но инструмент обновления может не учитывать, поэтому ломающие изменения в 0.x также описываются в журнале.
Поддержка старых версий. Публикация исправления для прежней мажорной версии требует отдельной ветки. Без неё пользователь, не готовый к обновлению, не получает исправлений безопасности.
Практическое правило: если изменение может сломать код пользователя, оно относится к мажорной версии. Занижение номера версии экономит одно обсуждение и создаёт проблему у каждого, кто обновляется автоматически.
Практика: организация релизов
1. Используйте один источник версии. Тег git либо атрибут модуля, но не оба одновременно. Двойное описание расходится при первом же релизе.
2. Создавайте релиз из тега, а не из ветки. Сборка по тегу воспроизводима: тот же тег даёт тот же дистрибутив в любой момент.
3. Ведите журнал изменений. Перечень изменений между версиями нужен пользователям для оценки обновления; о ветвлении — в статье про trunk-based разработку.
4. Не публикуйте версии вручную. Ручная публикация не фиксирует, из какого коммита собран дистрибутив.
5. Проверяйте дистрибутив перед публикацией. Ошибка в метаданных после публикации неисправима: удалённую версию нельзя загрузить повторно.
6. Различайте предварительные и стабильные выпуски. Версия с суффиксом rc не устанавливается по умолчанию, что позволяет проверить её до стабильного релиза.
7. Не удаляйте опубликованные версии. Удаление ломает установки с фиксацией версии. Ошибочную версию отзывают.
8. Фиксируйте версию в зависимостях диапазоном. Пользователи пакета указывают нижнюю границу, а не точную версию.
Рекомендации:
- Начинайте с версии 0.x до стабилизации интерфейса. Мажорная версия 1 означает обещание совместимости.
- Увеличивайте мажорную версию при ломающих изменениях. Сокрытие ломающего изменения в минорной версии ломает пользователей при автоматическом обновлении.
- Автоматизируйте проверку номера версии. Конвейер отклоняет публикацию, если версия уже существует в реестре.
- Публикуйте предварительные выпуски для крупных изменений. Пользователи получают возможность проверить совместимость до стабильной версии.
- Связывайте тег с коммитом. Возможность определить, из какого кода собран дистрибутив, необходима при разборе проблем.
Практический сценарий: расхождение версии пакета и содержимого из-за двух источников
Контекст: пользователи сообщали, что установленная версия ведёт себя не так, как описано в журнале изменений для этого номера.
- Диагностика. Версия указывалась в двух местах: в конфигурации пакета и в атрибуте модуля. При релизе обновлялась конфигурация, а атрибут оставался прежним. Дистрибутив публиковался под новым номером, но код внутри соответствовал предыдущей версии.
- Причина. Ручное обновление версии в двух файлах выполнялось по памяти, а проверка соответствия не выполнялась. Расхождение не обнаруживалось, поскольку установка проходила успешно.
- Действия. (а) Версию перевели на извлечение из тега git, устранив ручной ввод; (б) добавили проверку в конвейер: номер версии из тега сравнивается с номером в собранных метаданных; (в) ретроспективно проверили последние десять версий и нашли два случая расхождения; (г) ошибочные версии отозвали и опубликовали корректные под новыми номерами; (д) добавили шаг сборки из тега вместо сборки из ветки.
- Результат. Расхождение стало невозможным, поскольку версия формируется из тега автоматически, а проверка в конвейере отклоняет публикацию при несоответствии.
Вывод: два источника версии расходятся независимо от дисциплины. Извлечение версии из тега git устраняет ручной ввод, а проверка соответствия в конвейере предотвращает публикацию дистрибутива с неверными метаданными.
FAQ
Как определить, какую версию выпускать? По характеру изменения: исправление — патч, совместимая функциональность — минорная, ломающее изменение интерфейса — мажорная.
Можно ли переиспользовать номер версии после удаления? Нет. Реестр не принимает повторную загрузку под тем же номером, поэтому ошибочная версия публикуется под новым.
Зачем нужны предварительные выпуски? Они не устанавливаются по умолчанию и позволяют пользователям проверить совместимость до стабильного релиза.
Как связать дистрибутив с коммитом? Сборкой из тега: тег указывает на конкретный коммит, поэтому дистрибутив воспроизводим.
Источники: Semantic Versioning, Python Packaging Guide (single-sourcing version), PEP 440 — ссылки в шапке.