Skip to content

Latest commit

 

History

History
187 lines (162 loc) · 16.6 KB

File metadata and controls

187 lines (162 loc) · 16.6 KB

Политика версионирования

Канон схемы нумерации версий проекта. Вынесено из CONTRIBUTING.md, который остался коротким онбордингом. Как менялись релизы по существу — в ../../HISTORY.md; полный список изменений — в ../../CHANGELOG.md; решение о динамической версии — ADR-0005.

Это НЕ SemVer. Проект использует собственную схему. Не применяйте привычные правила SemVer — они сломают инвариант «каждый тег = vX.Y.0».

MAJOR . MINOR . PATCH
  │       │       │
  │       │       └─ +1 на принятое изменение (смерженный PR — по его номеру);
  │       │          обнуляется при инкременте MINOR
  │       │
  │       └─ +1 ВСЕГДА при постановке git-тега + GitHub Release
  │
  └─ меняется только при фундаментальных изменениях:
     выход за пределы локального инструмента,
     поддержка других языков программирования и т.п.

Следствия схемы:

  • Теги ставятся только на границе MINOR → все теги имеют вид vX.Y.0. PATCH-тегов не существует, поэтому коллизий версий нет.
  • PATCH — счётчик принятых изменений с последнего тега, а не номер «патч-релиза». В «логическом» счётчике (scripts/version.py, README-бейдж) изменение опознаётся по номеру PR(#NNNN) в теме коммита либо Merge pull request #NNNN; номера складываются во множество, поэтому один PR даёт +1 независимо от того, на сколько коммитов он разбит и сколько раз попал в историю. Коммит без номера (прямой пуш в main) считается отдельно, но только с first-parent линии — иначе внутренние коммиты слитой ветки завышали бы счёт. CI-бот (chore(ci): update badges) и склейки git pull (Merge branch 'main' of …) не считаются. Например, 1.2.17 — это «17 принятых изменений после тега v1.2.0», а НЕ «17-й патч-релиз». Отдельного релиза 1.2.17 не существует — на PyPI/Releases уходят только тегированные X.Y.0. (Метаданная setuptools-scm X.Y.0.postN — независимая «сырая дистанция»: считает все коммиты, включая CI-бота и merge; см. § Release vs dev.)
  • Почему по номерам, а не по графу истории. Прежняя формула считала коммиты на first-parent линии, то есть меряла форму истории — а форма зависит от окна. В свежем клоне она линейная (squash-мержи), но git pull merge'ом уводит всё пришедшее с GitHub во второй родитель, и на first-parent линию оно не попадает: два принятых PR + один локальный коммит давали 2 вместо 3, а сама склейка добавляла +1 за ничто. Перейти на --no-merges было нельзя — тогда мерж ветки из трёх коммитов давал 6 вместо 4. Ни одна топологическая формула не даёт обе цифры, поэтому считаются сущности (номера PR), а не рёбра графа. Оба сценария закреплены тестами на настоящем git-репозитории (tests/test_version_script.py): мок обхода графа подменял бы ровно то место, где жил дефект.
  • Клон без тегов версию не показывает. git describe там падает, и MAJOR.MINOR неоткуда взять — scripts/version.py печатает предупреждение в stderr («подтяните теги: git fetch --tags») и отдаёт неполную версию. Так клонирует облачная сессия и actions/checkout без fetch-depth: 0; в CI-джобах, которым нужна версия, fetch-depth: 0 обязателен.
  • CHANGELOG.md: блок [Unreleased] копится и при теге переносится в [X.Y.0]. Промежуточные PATCH-версии в CHANGELOG не документируются построчно (иначе CHANGELOG превратится в git log).
  • Краткость и ротация CHANGELOG. Запись = одна строка на изменение (- <что> (#PR)), детали — в PR/issue; многострочные пересказы — антипаттерн (раздули [Unreleased] перед v1.8.0). В живом CHANGELOG.md держим только [Unreleased] + три последних MINOR; более старые релизы ротируются в ../archive/changelog-archive.md, а scripts/check_docs_guardrails.py стережёт лимит в 3 версионных заголовка.
  • Секция [X.Y.0] — это и есть тело GitHub-релиза. scripts/extract_release_notes.py вырезает её по имени тега и отдаёт в body_path (workflow release.yml); автосписок заголовков PR остаётся ниже приложением «Full changelog». Отсюда практическое следствие: забытая запись под тег роняет релиз — скрипт завершается с ошибкой, а не отдаёт пустое тело. Проверка стоит в job'е verify, от которого зависят оба публикующих job'а: пока она жила только в публикации GitHub Release, PyPI выходил при любом состоянии CHANGELOG — эти два job'а независимы друг от друга по построению. Пустой релиз читался бы как «ничего не изменилось», что хуже автосписка, который он заменяет.

Release-версия vs dev-версия

Git-теги — единственный источник истины. pyproject.toml больше НЕ объявляет version статически: она вычисляется из git-тегов через setuptools-scm (dynamic = ["version"], version_scheme = "post-release"). Вручную править [project].version нельзя — статической строки там нет, а CI-проверка её возврат заблокирует (см. ниже).

Из-за этого одновременно живут две формы одного и того же номера:

Форма Когда Пример Откуда берётся Для чего
Release (метаданные пакета на теге) HEAD стоит ровно на теге vX.Y.0 1.5.0 setuptools-scm То, что уходит на PyPI / GitHub Release; чистый PEP 440 без local-сегмента
Dev (метаданные пакета вне тега) N коммитов после последнего тега (все коммиты, включая CI-бота и merge) 1.5.0.post3+g1a2b3c4 setuptools-scm (post-release) Технически отличимая сборка «после релиза 1.5.0» — видно, что это не официальный релиз
Логический счётчик (человекочитаемый) всегда 1.5.3 scripts/version.py Удобный «тег + число смерженных PR» (first-parent, без CI-бота) для тегирования и заметок; это не PEP 440 и не метаданные пакета

Почему две формы сосуществуют. setuptools-scm обязан выдавать валидный PEP 440 (X.Y.0.postN+g<hash>) — это то, что понимают pip/PyPI и что попадает в метаданные установленного пакета. Логическая схема проекта (X.Y.N, где N = число принятых изменений после тега — first-parent коммитов без CI-бота, ≈ смерженных PR) удобнее человеку, но PEP 440 не является — поэтому она остаётся отдельным справочным счётчиком в scripts/version.py, а не источником для сборки. (Из-за разной логики подсчёта логический N обычно меньше postN-дистанции setuptools-scm, которая считает все коммиты.)

Что показывают бейджи README

Формы две, а бейджа в шапке тоже два — и они не дублируют друг друга:

Бейдж Значение Источник Когда меняется
release/pypi 1.10 scripts/generate_release_badge.py на релизе
version 1.10.109 scripts/generate_version_badge.py на каждое принятое изменение

.0 в подписи релиза не показывается намеренно: PATCH в теге всегда ноль, это константа, а не номер патча.

release/pypi — не витрина, а проверка: скрипт сам сверяет последний релизный тег с версией, реально опубликованной на PyPI. Сошлись — зелёный 1.10; публикация отстала — красный 1.11 ≠ pypi 1.10; до PyPI не достучались — серый 1.10 · pypi ? (проверка не выполнена, и бейдж говорит об этом, вместо того чтобы зеленеть на нехватке данных).

Раньше на этом месте стояли два отдельных бейджа — Release (git-тег) и PyPI (опубликованный пакет). Пока публикация работает, они показывают одно и то же и выглядят лишними; их смысл появляется ровно в момент расхождения, о котором ни один из них не сообщал — сравнивать надо было глазами. Один проверяющий бейдж занимает меньше места и не молчит.

Релизным считается только тег вида vX.Y.Z: рядом живут служебные (v-checkpoint-...), и git describe --tags без маски выбирает их наравне с релизными.

Когда какая используется:

  • stepik_grader.__version__ и stepik-grader --version читают метаданные пакета (importlib.metadata) — то есть release- или dev-форму, вычисленную setuptools-scm при pip install/сборке. На теге увидишь 1.5.0, вне тега — 1.5.0.postN+g<hash>.
  • scripts/version.py — печатает логический X.Y.N; нужен человеку при подготовке тега/заметок, не при сборке.

UX вывода --version. На теге --version печатает чистые метаданные пакета (1.5.0) без изменений. Вне тега к тем же сырым метаданным добавляется явная пометка: 1.5.0.post3+g1a2b3c4 (dev build, not a release) — чтобы пользователь не принял PEP 440 postN/local-сегмент за официальный релиз. Логика — cli._format_version_for_display() / cli._is_dev_build() (наличие + в версии); способ вычисления самой версии (setuptools-scm) не менялся.

Когда поднимать/тегировать релиз:

  • MINOR (vX.(Y+1).0) — качественный скачок (см. таблицу выше). Ставится git-тег vX.Y.0 + GitHub Release; setuptools-scm подхватит его сам.
  • PATCH не тегируется — это просто «дистанция коммитов» от последнего тега, она растёт автоматически.
  • MAJOR (v2.0.0) — выход за рамки «локальный инструмент для Python-задач Stepik» (другие языки/платформы).
  • Ручное редактирование версии в pyproject.toml не требуется и запрещено — всё делает тег.
  • ⛔ Блокирующий шаг ПЕРЕД тегом vX.Y.0 — ротация CHANGELOG. До постановки тега перенеси самый старый MINOR из CHANGELOG.md в ../archive/changelog-archive.md дословно, чтобы в живом CHANGELOG.md осталось ровно [Unreleased] + три последних MINOR (см. «Краткость и ротация CHANGELOG» выше), и переименуй [Unreleased][X.Y.0] — ДАТА, добавив сверху новый пустой [Unreleased]. Это не опциональная уборка, а гейт: check_docs_guardrails.py держит версионный бюджет CHANGELOG.md = 3, и без ротации CI на релизе падает. Гейт стоит в job'е verify workflow release.yml — до того, как что-либо опубликовано. Раньше эта фраза была обещанием, а не фактом: release.yml guard документации не запускал вовсе, и релиз с четырьмя версиями в живом CHANGELOG проходил насквозь.

Гейт перевода стоит там же, до публикации. Запись, оставшаяся английской, уезжает в GitHub Release и на PyPI необратимо, поэтому scripts/check_changelog_translated.py --strict проверяет извлечённые release notes в том же job verify: без единой кириллической буквы вне кода запись считается непереведённой и роняет прогон. На PR та же проверка только предупреждает — там запись ещё может быть черновиком.

CI-защита от дрейфа. scripts/check_version_consistency.py следит, чтобы (1) статический version не вернулся в pyproject.toml и (2) верхняя релизная запись CHANGELOG.md (и, мягко, таблица метрик CLAUDE.md) не расходилась с последним git-тегом. Baseline берётся из git describe --tags. Поэтому отдельной ручной сверки версий по репозиторию делать не нужно — за это отвечает CI.

Полную таблицу эволюции релизов (от v1.0.0 до текущего) и отличия от оригинала см. в ../../HISTORY.md — он ссылается на эту политику, а не копирует её.