Делим монорепозиторий библиотеки Go на отдельные модули и адаптируем их для локальной разработки
Привет! Меня зовут Илья Сергунин, я Senior Software Engineer из продуктовой команды Авито. Наша библиотека Go состоит из абстракций для работы с транзакциями в Go и нескольких драйверов для баз данных. Сначала там было всего четыре имплементации, и для всех драйверов подходил единый модуль. Потом добавились ещё три драйвера, и у каждого была разная версия Go, единого модуля уже не хватало. Тогда я использовал теги сборки (build tags), но реальная версия языка для каждого драйвера не соответствовала версии в go.mod. Более того, каждому драйверу были нужны свои зависимости, которые не использовались другими, и общий список зависимостей рос. И, чтобы решить эти проблемы, я решил разделить библиотеку на отдельные модули в одном репозитории.
Делим репозиторий на маленькие модули
Вот код первой версии библиотеки на github.
Исходная структура github.com/avito-tech/go-transaction-manager
go.mod
// Интерфейсы транзакций баз данных и менеджером транзакций
trm
// Драйвера, которые реализуют интерфейс транзакций для различных баз данных
sql
sqlx
mongo
redis
// и т.д.
Я перенёс драйвера в директорию drivers
и создал для каждого свой go.mod
.
drivers
...sql
......go.mod
...sqlx
......go.mod
...mongo
...redis
... // и т.д.
В go.mod
полный путь модуля будет выглядеть так:
github.com/avito-tech/go-transaction-manager/drivers/sql/
Если версия модуля 2 и выше, то путь будет выглядеть следующим образом:
github.com/avito-tech/go-transaction-manager/drivers/sql/v{version}
Настраиваем разработку монорепозитория для локальной среды
После деления библиотеки на отдельные модули, первой задачей стало упростить локальную разработку. Для этого можно использовать симлинк (в любых версиях go), заменять модуль в go.mod
через go mod replace
(go.11+, 24 августа 2018 г.) или workspaces: туториал, документация, go 1.18+, 15 марта 2022 г.
Чтобы создать симлинк на локальном хосте, нам нужно написать shell скрипт. Скрипт нужно подготовить для разных ОС и протестировать — например, в Windows нет команды ln
, а нужно использовать mklink
. Поэтому я не использовал этот подход.
Для директивы replace нам нужно написать shell скрипт, который добавит директиву replace во все файлы go.mod,
и написать pre-commit для git, которая удалит директиву replace перед commit кода в репозиторий. Этот подход тоже показался не оптимальным.
// Добавляем replace в `go.mod`
go mod edit -replace=github.com/avito-tech/go-transaction-manager/trm/v2=../../
// Удаляем replace в `go.mod`
go mod edit -dropreplace=github.com/avito-tech/go-transaction-manager/trm/v2
Свой выбор я остановил на workspaces. Для них нам нужно создать файл go.work
и запустить go work sync
. Я выбрал этот подход, потому что его легко использовать.
Считаем покрытие кода в монорепозитории
Следующей задачей было посчитать покрытие кода тестами.
Если запустим go test ./...
в корневом каталоге репозитория библиотеке без go.mod
, получим ошибку pattern ./...: directory prefix. does not contain modules listed in go.work or their selected dependencies.
go test ./...
работает только в каталогах с go.mod
, поэтому я написал shell скрипт (код), который запускает тесты в trm
и каталогах драйверов, а затем комбинирует покрытие кода через команду go tool covdata
(документ, исходный код), которая была добавлена в версии 1.20.
sh
// Циклический запуск подсчета покрытия кода для каждого моделяgo test $modulePath -cover -covermode=atomic -test.gocoverdir=$ROOT/coverage
// Комбинирование покрытия кода в единых файл
go tool covdata textfmt -i="$ROOT/coverage" -o "$ROOT/coverage.out"
Исходный код CI/CD на основе Github Actions.
Тегируем и версионируем монорепозиторий для публикации модулей
После того, как я разделил модули и добавил тесты с покрытием кода, я не смог опубликовать свой модуль: он не появлялся в списке доступных пакетов и модулей на сайте pkg.go.dev. К счастью, я увидел библиотеку gocache, которая решила эту проблему, и скопировал их подход.
Нам нужно создать тег для каждого модуля, который содержит путь до пакета внутри репозитория:
Для
github.com/avito-tech/go-transaction-manager/trm/v2
, нужно создать тегtrm/v{version}
.Для
github.com/avito-tech/go-transaction-manager/drivers/sql/v2
, нужно создать тегdrivers/sqlx/v{version}
.
К сожалению, существующие решения для Github Actions (tag-bump, tag, tag-release-on-push-action) не дают такой возможности. Чтобы упростить этот процесс, я снова написал shell скрипт, он помечает все модули одной и той же версией. Однако идеальным решением было бы обновление версии только изменённых модулей и автоматическое обновление версии в зависимых модулях при необходимости.
Вывод
Чтобы создать удобный монорепозиторий для библиотеки Go, используем:
go workspace
для упрощения локальной разработки.go tool covdata
, чтобы посчитать покрытие кода.Скрипты для упрощения разметки релизов.
Исходный код библиотеки с множеством модулей, запакованный в монорепозиторий.