ci: гейт «версия в pyproject.toml против тега релиза» #29

Merged
claude-secretary merged 1 commit from ci/version-tag-guard into main 2026-08-27 14:21:38 +07:00

Закрывает #28.

Зачем

Версия живёт в pyproject.toml, тег ставится руками, и разойтись они могут молча — что и произошло: девять версий (v0.10.0v0.18.0) прожили невыпущенными, а rev: из README у консьюмера не резолвился вовсе. Дыру заметили только когда стали разбирать трекер. Проверять это должен CI, а не внимательность.

Что проверяется

Где Условие Диагноз при нарушении
main версия имеет одноимённый тег «релиз забыт», с командой для выпуска
ветка версия новая и больше последней выпущенной «номер занят» или «ветка отстала»

Второе — не теоретическое: за сегодня три PR подряд претендовали на соседние номера, и очередь версий приходилось держать в голове и пересчитывать при каждом мерже. Теперь это ловит CI.

fetch-depth: 0 в checkout: при shallow-клоне тегов нет вовсе, и гейт падает с объяснением, а не отдаёт зелёный вхолостую.

Тесты гейта — отдельным скриптом

fixtures/check_version_tag_cases.sh поднимает временные репозитории с заданным набором тегов. На живом репо вердикт гейта меняется от коммита к коммиту (сегодня зелёный, завтра красный после бампа), поэтому кейсы в run_smoke.sh были бы нестабильны по построению.

Восемь кейсов: тег есть / тега нет на main; новая, занятая и отставшая версия на ветке; версионная сортировка (0.10.0 больше 0.9.0, а не наоборот — лексикографически было бы иначе); отсутствие тегов вовсе; отсутствие строки version.

Мутации

Каждая роняет самопроверку: не требовать тега на main, не сравнивать версию с последним тегом, лексикографическое сравнение вместо версионного.

Отдельно стоит отметить одну: «не проверять занятость тега на ветке» вердикт не меняет — обе ветки логики дают FAIL на одном и том же входе. Но меняет диагноз, а от него зависит, что человек пойдёт чинить. Поэтому два кейса пиннят текст: «уже выпущена» против «не больше выпущенной».

По дороге в самом гейте нашлась подстановка команды: сообщение об ошибке содержало `rev: ${tag}` в двойных кавычках, то есть bash пытался бы выполнить rev: v0.21.0.

Версия

0.21.00.22.0. Порядок выпуска описан в README рядом с политикой версионирования.

Закрывает [#28](https://git.homedevlab.ru/senokosov/pre-commit-hooks/issues/28). ## Зачем Версия живёт в `pyproject.toml`, тег ставится руками, и разойтись они могут молча — что и произошло: девять версий (`v0.10.0`–`v0.18.0`) прожили невыпущенными, а `rev:` из README у консьюмера не резолвился вовсе. Дыру заметили только когда стали разбирать трекер. Проверять это должен CI, а не внимательность. ## Что проверяется | Где | Условие | Диагноз при нарушении | |---|---|---| | `main` | версия имеет одноимённый тег | «релиз забыт», с командой для выпуска | | ветка | версия новая **и** больше последней выпущенной | «номер занят» или «ветка отстала» | Второе — не теоретическое: за сегодня три PR подряд претендовали на соседние номера, и очередь версий приходилось держать в голове и пересчитывать при каждом мерже. Теперь это ловит CI. `fetch-depth: 0` в checkout: при shallow-клоне тегов нет вовсе, и гейт падает с объяснением, а не отдаёт зелёный вхолостую. ## Тесты гейта — отдельным скриптом `fixtures/check_version_tag_cases.sh` поднимает временные репозитории с заданным набором тегов. На живом репо вердикт гейта меняется от коммита к коммиту (сегодня зелёный, завтра красный после бампа), поэтому кейсы в `run_smoke.sh` были бы нестабильны по построению. Восемь кейсов: тег есть / тега нет на `main`; новая, занятая и отставшая версия на ветке; версионная сортировка (`0.10.0` больше `0.9.0`, а не наоборот — лексикографически было бы иначе); отсутствие тегов вовсе; отсутствие строки `version`. ## Мутации Каждая роняет самопроверку: не требовать тега на `main`, не сравнивать версию с последним тегом, лексикографическое сравнение вместо версионного. Отдельно стоит отметить одну: «не проверять занятость тега на ветке» **вердикт не меняет** — обе ветки логики дают FAIL на одном и том же входе. Но меняет диагноз, а от него зависит, что человек пойдёт чинить. Поэтому два кейса пиннят текст: «уже выпущена» против «не больше выпущенной». По дороге в самом гейте нашлась подстановка команды: сообщение об ошибке содержало `` `rev: ${tag}` `` в двойных кавычках, то есть bash пытался бы выполнить `rev: v0.21.0`. ## Версия `0.21.0` → **`0.22.0`**. Порядок выпуска описан в README рядом с политикой версионирования.
ci: гейт «версия в pyproject.toml против тега релиза»
All checks were successful
ci / smoke (push) Successful in 18s
ci / smoke (pull_request) Successful in 18s
e1a1252191
Закрывает #28

Версия живёт в pyproject.toml, тег ставится руками, и разойтись они
могут молча — что и произошло: девять версий (v0.10.0–v0.18.0) прожили
невыпущенными, а `rev:` из README у консьюмера не резолвился вовсе.
Проверять это должен CI, а не внимательность.

Проверка разная по ветвям, и это не придирка:
  * на main версия обязана иметь одноимённый тег — иначе релиз забыт;
  * на ветке версия обязана быть новой и больше последней выпущенной —
    иначе два параллельных PR молча займут один номер. За сегодня это
    едва не случилось трижды.

fetch-depth: 0 в checkout — при shallow-клоне тегов нет вовсе, и гейт
падает с объяснением, а не отдаёт зелёный вхолостую.

Тесты гейта — отдельным скриптом на временных репозиториях: на живом
репо его вердикт меняется от коммита к коммиту, сегодня зелёный, завтра
красный после бампа. Восемь кейсов, включая версионную сортировку
(0.10.0 больше 0.9.0, а не наоборот) и отсутствие тегов вовсе.

Мутации, каждая роняет самопроверку: не требовать тега на main, не
сравнивать версию с последним тегом, лексикографическое сравнение
вместо версионного. Мутация «не проверять занятость тега на ветке»
вердикт не меняет — обе ветки логики дают FAIL, — но меняет диагноз,
поэтому два кейса пиннят текст: «уже выпущена» против «не больше
выпущенной».

Версия 0.21.0 → 0.22.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
claude-secretary force-pushed ci/version-tag-guard from e1a1252191
All checks were successful
ci / smoke (push) Successful in 18s
ci / smoke (pull_request) Successful in 18s
to 089b9e24ff
All checks were successful
ci / smoke (push) Successful in 19s
ci / smoke (pull_request) Successful in 19s
2026-08-27 14:20:58 +07:00
Compare
Author
Owner

Ревью (субагент, свежий контекст): блокер подтверждён и исправлен.

B1 — гейт красил бы собственный релизный тег. Воспроизвёл: on: push срабатывает и на пуш тега, ref_name там — имя тега, гейт уходил в ветку «это фича-ветка», видел существующий тег и печатал «версия уже выпущена, поднимите номер». То есть корректный выпуск диагностировался как ошибка, и красным помечался ровно тот коммит, на который консьюмер ставит rev:. Ирония в том, что гейт против молчаливого расхождения сам был бы источником постоянного красного.

Теперь пуш тега — отдельная ветка проверки: версия обязана совпадать с тегом. Кейс release_tag_push_passes; мутация «убрать эту ветку» роняет самопроверку.

B1-следствие (окно красного main). Оно остаётся и остаётся намеренно — это буквальное требование приёмки #28: main с версией без тега = релиз забыт. Но выхода из красного теперь нет только у прогона, отработавшего до тега; после пуша тега его можно перезапустить, и он зелёный. Порядок выпуска и это следствие дописаны в README.

Ниты — взяты почти все:

  • N1/N2git rev-parse --verify refs/tags всегда возвращал 1 (refs/tags это каталог, не ref), так что guard сводился к [[ -z "$(git tag)" ]]. Заменён на явную проверку «мы в git» плюс git tag --list 'v*'; guard и выбор последнего тега теперь смотрят на одно множество — репозиторий с тегами не по маске больше не проходит guard с пустым latest.
  • N4 — все четыре невыловленные мутации закрыты: guard пустой версии и guard отсутствия тегов проверяются на ветке (на main они краснели по другой причине, то есть кейс пиннил не то, что заявлял), лексикографический latest ловится кейсом с v0.9.0 + v0.10.0.
  • N5/N6 — фикстуры изолированы: unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE (под git-хуком они коммитили и тегали в посторонний репозиторий, печатая «пройден» вхолостую — спасибо за воспроизведение), init.templateDir= и core.hooksPath=/dev/null, commit --no-verify, cd … || exit 1.
  • N8 — комментарий обещал вызов из run_smoke.sh, которого нет.
  • N9 — сделано, а не отложено: на main проверяется ещё и что тег стоит на коммите с той же версией. Тег на постороннем коммите — это rev:, резолвящийся в чужой код, то есть исходная проблема закрывалась лишь наполовину. Кейс tag_points_to_other_version_fails.
  • N3, N10, N11 — записаны в README как явные следствия: пререлизные суффиксы не годятся (sort -V ставит rc1 старше релиза), ветки поддержки не поддержаны, любой PR обязан бампать версию, и два PR на один номер остаются зелёными, пока первый не смержен и не протегирован.

N12 (разбор version первой подходящей строкой) оставляю как есть: сейчас в pyproject.toml одна такая строка, а надёжный разбор TOML в bash-гейте — несоразмерная цена. Если появится вторая таблица с version, кейс «версия не та» поймает это сразу, потому что тег не сойдётся.

Ревью (субагент, свежий контекст): блокер подтверждён и исправлен. **B1 — гейт красил бы собственный релизный тег.** Воспроизвёл: `on: push` срабатывает и на пуш тега, `ref_name` там — имя тега, гейт уходил в ветку «это фича-ветка», видел существующий тег и печатал «версия уже выпущена, поднимите номер». То есть корректный выпуск диагностировался как ошибка, и красным помечался ровно тот коммит, на который консьюмер ставит `rev:`. Ирония в том, что гейт против молчаливого расхождения сам был бы источником постоянного красного. Теперь пуш тега — отдельная ветка проверки: версия обязана **совпадать** с тегом. Кейс `release_tag_push_passes`; мутация «убрать эту ветку» роняет самопроверку. **B1-следствие (окно красного `main`).** Оно остаётся и остаётся намеренно — это буквальное требование приёмки #28: `main` с версией без тега = релиз забыт. Но выхода из красного теперь нет только у прогона, отработавшего *до* тега; после пуша тега его можно перезапустить, и он зелёный. Порядок выпуска и это следствие дописаны в README. **Ниты — взяты почти все:** - **N1/N2** — `git rev-parse --verify refs/tags` всегда возвращал 1 (`refs/tags` это каталог, не ref), так что guard сводился к `[[ -z "$(git tag)" ]]`. Заменён на явную проверку «мы в git» плюс `git tag --list 'v*'`; guard и выбор последнего тега теперь смотрят на одно множество — репозиторий с тегами не по маске больше не проходит guard с пустым `latest`. - **N4** — все четыре невыловленные мутации закрыты: guard пустой версии и guard отсутствия тегов проверяются **на ветке** (на `main` они краснели по другой причине, то есть кейс пиннил не то, что заявлял), лексикографический `latest` ловится кейсом с `v0.9.0` + `v0.10.0`. - **N5/N6** — фикстуры изолированы: `unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE` (под git-хуком они коммитили и тегали в посторонний репозиторий, печатая «пройден» вхолостую — спасибо за воспроизведение), `init.templateDir=` и `core.hooksPath=/dev/null`, `commit --no-verify`, `cd … || exit 1`. - **N8** — комментарий обещал вызов из `run_smoke.sh`, которого нет. - **N9** — сделано, а не отложено: на `main` проверяется ещё и что тег стоит на коммите с той же версией. Тег на постороннем коммите — это `rev:`, резолвящийся в чужой код, то есть исходная проблема закрывалась лишь наполовину. Кейс `tag_points_to_other_version_fails`. - **N3, N10, N11** — записаны в README как явные следствия: пререлизные суффиксы не годятся (`sort -V` ставит `rc1` старше релиза), ветки поддержки не поддержаны, любой PR обязан бампать версию, и два PR на один номер остаются зелёными, пока первый не смержен и не протегирован. **N12 (разбор `version` первой подходящей строкой)** оставляю как есть: сейчас в `pyproject.toml` одна такая строка, а надёжный разбор TOML в bash-гейте — несоразмерная цена. Если появится вторая таблица с `version`, кейс «версия не та» поймает это сразу, потому что тег не сойдётся.
claude-secretary deleted branch ci/version-tag-guard 2026-08-27 14:21:38 +07:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
senokosov/pre-commit-hooks!29
No description provided.