Делим монорепозиторий библиотеки Go на отдельные модули и адаптируем их для локальной разработки

Привет! Меня зовут Илья Сергунин, я Senior Software Engineer из продуктовой команды Авито. Наша библиотека Go состоит из абстракций для работы с транзакциями в Go и нескольких драйверов для баз данных. Сначала там было всего четыре имплементации, и для всех драйверов подходил единый модуль. Потом добавились ещё три драйвера, и у каждого была разная версия Go, единого модуля уже не хватало. Тогда я использовал теги сборки (build tags), но реальная версия языка для каждого драйвера не соответствовала версии в go.mod. Более того, каждому драйверу были нужны свои зависимости, которые не использовались другими, и общий список зависимостей рос. И, чтобы решить эти проблемы, я решил разделить библиотеку на отдельные модули в одном репозитории. 

832153f29161aed338c58a0626b66f25.png

Делим репозиторий на маленькие модули

Вот код первой версии библиотеки на 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, чтобы посчитать покрытие кода.

  • Скрипты для упрощения разметки релизов.

Исходный код библиотеки с множеством модулей, запакованный в монорепозиторий.

© Habrahabr.ru