git-monorepo-release — Релиз монорепо с независимыми версиями
Цель: в репозитории, где живёт несколько пакетов со своими версиями, определить состав релиза и провести его так, чтобы версии не поехали друг относительно друга.
Перед началом прочитай ../../rules/git-conventions.md — разделы про Semver и теги.
Постановку одного тега с changelog делает git-release-tag. Этот скилл решает, какие теги ставить и почему, и вызывает git-release-tag на каждый.
Границы
Скилл применим, когда в репозитории больше одного независимо версионируемого пакета. Проверь это до всего остального:
git tag --list --sort=-v:refname | head -20
ls -d */ | head -20
Признаки монорепо с независимыми версиями: теги вида <name>/vX.Y.Z, несколько директорий с собственными манифестами (go.mod, package.json, pyproject.toml, *.csproj).
Если пакет один — скажи об этом и передай работу git-release-tag. Не изобретай монорепо там, где его нет.
Шаг 1. Определи затронутые пакеты
Возьми диапазон с последнего релиза каждого пакета и посмотри, что реально менялось:
# последний тег конкретного пакета
git tag --list 'sdk-go/v*' --sort=-v:refname | head -1
# что изменилось в его директории с тех пор
git log sdk-go/v0.3.0..HEAD --oneline -- sdk-go/
Пройди так по каждому пакету. Пакет без коммитов в своей директории не релизится — даже если релизится соседний.
Осторожно с изменениями вне директорий пакетов: правка общей спецификации, protobuf-схемы или генератора может затрагивать всех потребителей, не оставив коммитов в их папках. Такие коммиты покажи пользователю отдельно и спроси, чьи версии они двигают.
Шаг 2. Определи bump для каждого пакета
Semver считается для каждого пакета отдельно. Breaking change в Go SDK не двигает версию Python SDK.
| изменение | что бампить |
|---|---|
| только спецификация, реализации не тронуты | ничего — до момента реализации в пакетах |
| баг в одном пакете | только его, patch |
| фича, реализованная в нескольких пакетах | каждый затронутый, отдельным тегом, minor |
| breaking change в пакете версии < 1.0 | только его, minor |
| breaking change в пакете версии ≥ 1.0 | только его, major |
Правило нулевой мажорной версии: до v1.0.0 breaking change идёт в minor, а не в major. Пакеты в монорепо часто находятся на разных стадиях зрелости, поэтому проверяй текущую версию каждого, а не применяй одно правило ко всем.
Если коммиты не по Conventional Commits и bump не выводится — покажи их пользователю и спроси. Не угадывай.
Шаг 3. Покажи план релиза целиком
До любых команд покажи таблицу и дождись подтверждения:
Пакет текущая новая причина
sdk-go v0.3.0 → v0.4.0 feat(sdk-go): streaming API
sdk-python v0.2.1 → v0.2.2 fix(sdk-python): retry on 429
sdk-java v0.5.0 — нет изменений
sdk-csharp v0.1.2 — нет изменений
Пакеты без изменений показывай явной строкой с прочерком. Пользователь должен видеть, что они рассмотрены и сознательно пропущены, а не забыты.
Шаг 4. Проведи релиз
Для каждого релизуемого пакета вызови git-release-tag — он соберёт changelog из коммитов и поставит annotated-тег.
Теги ставятся на один и тот же коммит main, поэтому сначала убедись, что дерево чистое и main актуален:
git checkout main && git pull --ff-only
git status --porcelain # должно быть пусто
Пуш нескольких тегов — одной командой, чтобы CI увидел релиз целиком, а не по частям:
git push origin sdk-go/v0.4.0 sdk-python/v0.2.2
Не пушь без явного подтверждения. Тег, за который зацепился release-pipeline, откатывается тяжело.
Шаг 5. GitHub Releases
Один Release на один тег. Каждый содержит changelog только своего пакета — не сваливай изменения всех SDK в общий текст.
gh release create sdk-go/v0.4.0 --title "sdk-go v0.4.0" --notes-file <changelog>
Если CI уже создаёт Releases по тегу — не дублируй вручную, просто запушь тег и скажи об этом пользователю.
Backport в старую версию
Когда часть пользователей осталась на старой версии, а main ушёл вперёд:
# ветка от старого тега
git checkout -b release/sdk-go-v0.1 sdk-go/v0.1.0
# только нужный фикс
git cherry-pick <commit-sha>
# patch поверх старой линии
git tag -a sdk-go/v0.1.1 -m "sdk-go v0.1.1"
git push origin release/sdk-go-v0.1 sdk-go/v0.1.1
Release-ветку заводи только когда backport действительно нужен. В обычной ситуации фикс едет в main и выходит следующим minor.
После backport проверь, что фикс есть и в main — иначе следующий релиз его потеряет.
Почему формат тега именно такой
<пакет>/vX.Y.Z — не стилистический выбор. Go требует ровно такой формат, чтобы go get умел забрать модуль из поддиректории монорепо: путь тега должен совпадать с путём модуля относительно корня репозитория. Для единообразия остальные пакеты используют тот же формат, хотя их тулчейны свободнее.
Отсюда следует: не ставь общий тег vX.Y.Z на монорепо с независимыми версиями. Он не соответствует ни одному пакету и ломает разрешение версий у Go.
Чего не делать
- не ставь общий тег на все пакеты сразу — версии разъедутся, а Go перестанет находить модуль
- не бампь пакет, в котором нет изменений, ради «ровных» номеров
- не переиспользуй и не передвигай опубликованный тег: пользователи уже получили этот код под этой версией
- не выводи major из
feat!вслепую — сначала посмотри, дошёл ли пакет до 1.0 - не релизь при грязном рабочем дереве: тег укажет на коммит, которого нет у остальных