Вкратце
Эта статья — рассказ о небольшом и полезном скрипте для обновления зависимостей go-mod-bump. Здесь вас может заинтересовать проблематика, решение или история написания скрипта. Если вы хотите потрогать скрипт руками, его можно найти в публичном репозитории.
Дисклеймер
- Скрипт не использовался в CI и может быть непригоден для такого использования.
- История описана постфактум, спустя значительный промежуток времени, поэтому может содержать явные несостыковки или искажения.
- Скрипт изменяет файлы
go.mod
иgo.sum
в рабочем каталоге git, а также меняет список staged файлов; вы можете потерять свои изменения при бездумном его использовании. - Этот инструмент — не конкурент популярным решениям по автоматизации обновления зависимостей и не призван занять их нишу.
Проблема
Я работаю со множеством репозиториев как на работе, так и в личных проектах, и в них регулярно нужно обновлять зависимости. Они хранятся в разных сервисах для хостинга проектов, в приватных и публичных репозиториях. И, к сожалению, не для всех этих проектов настроена автоматизация для обновления зависимостей, а для некоторых из них нет подходящих готовых решений.
Ещё об обновлении зависимости нужно как-то узнать, следить за сотнями репозиториев — это непосильная задача для меня, да и заниматься этим не хотелось бы.
Ограничения
- Обновление зависимостей делается одной командой.
- Обновление каждого модуля — в отдельном коммите.
- Сообщения коммитов подобны тем, какие формируют популярные решения обновления зависимостей.
Решение
Автоматизировать набор рутинных операций, чтобы я ими занимался минимально или вовсе не занимался. По сути, нужно автоматизировать несколько шагов:
- Узнать о наличии новой версии модуля.
go get github.com/xorcare/pointer@v1.2.2
- обновить модуль.go mod tidy
- синхронизировать зависимости в go.mod и go.sum, и заодно проверить, не появилось ли проблем.go build ./...
- сделать минимальные проверки, в этом случае проверить, что весь исходный код компилируется.git commit -m 'Bump github.com/xorcare/pointer from v1.2.1 to v1.2.2'
- создать коммит с понятным названием.- Повторить эти шаги для всех модулей от которых зависит обслуживаемый нами модуль.
История решения
Окей, погрузимся немного в историю. Стоит задача сделать себе небольшой скрипт — помощник обновления зависимостей.
Шаг первый
Нужно как-то узнать, что появились новые версии зависимостей. К счастью, в инструментарии Go уже
есть подходящая команда go get -u all
, она вполне справляется с обновлением всего и вся. Исходя из
наличия этой команды, можно сделать 2 вывода: Go умеет проверять наличие новых версий пакетов, и при
использовании этой команды мы никак не можем управлять процессом.
К моменту создания скрипта я уже знаком с командой go list
, и вместо go get -u all
можно
покопать её. Немного покопавшись в go help list
, можно обнаружить два важных флага:
-m
, который позволяет получить список модулей, используя командуgo list -m all
.-u
, который так же, как и вgo get -u
, позволит проверить наличие новых версий.
И вот мы уже имеем более управляемую команду go list -u -m all
, которая выводит нам список
модулей, версию, используемую сейчас в проекте, и самую новую версию, если она отличается от
установленной. Пример вывода команды:
github.com/xorcare/tornado
golang.org/x/mod v0.21.0
golang.org/x/net v0.22.0 [v0.29.0]
golang.org/x/sys v0.21.0 [v0.25.0]
Из него мы видим, что для модуля golang.org/x/mod
текущая версия — v0.21.0
, и более новой версии
нет, а, например, для golang.org/x/net
текущая версия — v0.21.0
, и есть новая
версия [v0.29.0]
.
Подытожим: был найден вариант, как получить список модулей и при этом узнать о наличии новых версий.
Шаг второй
Есть список модулей и версий, осталось проитерироваться по нему. В bash это не проблема, но есть нюансы.
- Каждая строка выглядит следующим образом:
golang.org/x/net v0.22.0 [v0.29.0]
; из неё нам нужно получить 3 значения, а я не сильно люблю парсить строки. - Нужны только direct модули.
- Нужны только те модули, для которых есть новая версия.
На предыдущем шаге в go help list
можно применить параметр -f
, который задаёт формат вывода. Я
воспользовался им и сделал подобие csv с разделителем @
(на момент создания скрипта я думал, что
этот символ никогда не встретится в строке, но сейчас уже не уверен).
go list -u -m -f '{{.Path}}@{{.Version}}@{{if .Update}}{{.Update.Version}}{{end}}{{if not .Update}}<SKIP>{{end}}{{if .Indirect}}<SKIP>{{end}}' all | grep -v '<SKIP>'
Получаем список модулей, которые точно импортируются нашим модулем и точно нуждаются в обновлении:
golang.org/x/net@v0.22.0@v0.29.0
Такой формат просто парсить с помощью cut
:
module=$(echo "$mdl" | cut -f1 -d@)
current_version=$(echo "$mdl" | cut -f2 -d@)
latest_version=$(echo "$mdl" | cut -f3 -d@)
Шаг третий
Мы получили список зависимостей, которые успешно обновляем. К тому моменту скрипт выглядел примерно так:
#!/usr/bin/env bash
set -e
MODULES_FOR_UPDATE=$(go list -m -u -f "{{.Path}}@{{.Version}}@{{if .Update}}{{.Update.Version}}{{end}}{{if not .Update}}<SKIP>{{end}}" all | grep -v '<SKIP>')
for mdl in $MODULES_FOR_UPDATE; do
module=$(echo "$mdl" | cut -f1 -d@)
current_version=$(echo "$mdl" | cut -f2 -d@)
latest_version=$(echo "$mdl" | cut -f3 -d@)
go get "$module@${latest_version}"
go mod tidy
go build ./...
git reset HEAD -- . >/dev/null
git add go.mod go.sum >/dev/null
git cm -a -m "${PREFIX}Bump ${module} from ${current_version} to ${latest_version}" >/dev/null
echoerr "go-mod-bump: upgraded ${module} ${current_version} => [${latest_version}]"
done
На этом шаге мы имеем рабочий инструмент, который можно использовать. Но есть ещё некоторые нюансы.
Шаг четвёртый
На проектах с большим количеством зависимостей скрипт работает уж очень медленно. Это происходит
потому, что go list -u
проверяет наличие обновлений вообще для всех модулей независимо от того,
являются ли они direct или indirect.
Чтобы ускорить работу скрипта, можно разделить работу скрипта на 2 этапа.
Этап первый: собираем список только direct модулей; похожие действия делались на предыдущих шагах, поэтому довольно просто рождается идея следующей команды:
go list -m -f '{{.Path}}{{if .Indirect}}<SKIP>{{end}}' all | grep -v '<SKIP>'
github.com/xorcare/tornado
golang.org/x/net
Этап второй: сохранив этот список в переменную $DIRECT_MODULES
, можем запросить проверку
обновлений только для direct модулей:
go list -u -m -f '{{.Path}}@{{.Version}}@{{if .Update}}{{.Update.Version}}{{end}}{{if not .Update}}<SKIP>{{end}}' $DIRECT_MODULES | grep -v '<SKIP>'
Таким образом, мы получим список модулей, которые нужно обновлять, не совершая обход вообще всех модулей.
Шаг пятый
Уже в ходе использования выясняется, что далеко не всегда модули можно обновить, и нужно как-то пропускать модули, обновление которых приводит к ошибкам. Это можно реализовать, объединив команды, отвечающие за проверку и обновление зависимостей в одну функцию:
function update_module() {
go get "$1"
go mod tidy
go build ./...
}
А дальше в случае неуспеха просто пропускать коммит, откатывая изменения в go.mod и go.sum:
if ! update_module "${module}@${latest_version}" >/dev/null 2>&1 >/dev/null; then
git checkout -f HEAD -- go.mod go.sum
fi
После чего приступаем к обновлению следующего модуля из списка, пока список не закончится.
На этом этапе основная механика скрипта уже похожа на опубликованную версию.
Шаг последний — но не по значению
Сначала я пользовался скриптом только сам, и не было смысла его украшать. Но позже я поделился скриптом в публичном репозитории, предварительно облагородив его.
- Убрал захардкоженный
all
и дал возможность разработчику самому выбирать список модулей. Не явно это дало возможность обновлять Go в go.mod. - Добавил флаги
-h
и-help
с примерами использования. А также добавил флаг-p
, чтобы можно было добавить префикс к заголовку фиксации, если вы следуете каким-то соглашениям об именовании коммитов. - Для случаев, когда обновление модуля завершается ошибкой, добавил вывод подробного сообщения и команд, которые можно скопировать и запустить для ручной диагностики проблемы.
Заключение
В итоге работы получился удобный скрипт go-mod-bump, который решает поставленную задачу и делает это хорошо. Он относительно кроссплатформенный, так как использует небольшое количество команд и проверен мной на macOS, Debian, и Ubuntu.
Возможно, стоило сделать этот инструмент в духе Go и написать его на Go. Но, на тот момент, для простоты я выбрал bash, а переписывать на Go пока нет вдохновения.
Послесловие
Если для вас близка изложенная проблематика, то вам может зайти использование скрипта. Если у ваших хостеров git репозиториев есть готовые инструменты по автоматизации обновления зависимостей, то используйте их. Эти инструменты почти наверное будут решать задачу лучше, запускаться сами по расписанию и делать за вас красивые запросы на слияние.