Ещё один инструмент для обновления зависимостей


Cover — Go Mod Bump

Вкратце

Эта статья — рассказ о небольшом и полезном скрипте для обновления зависимостей go-mod-bump. Здесь вас может заинтересовать проблематика, решение или история написания скрипта. Если вы хотите потрогать скрипт руками, его можно найти в публичном репозитории.

Дисклеймер

  • Скрипт не использовался в CI и может быть непригоден для такого использования.
  • История описана постфактум, спустя значительный промежуток времени, поэтому может содержать явные несостыковки или искажения.
  • Скрипт изменяет файлы go.mod и go.sum в рабочем каталоге git, а также меняет список staged файлов; вы можете потерять свои изменения при бездумном его использовании.
  • Этот инструмент — не конкурент популярным решениям по автоматизации обновления зависимостей и не призван занять их нишу.

Проблема

Я работаю со множеством репозиториев как на работе, так и в личных проектах, и в них регулярно нужно обновлять зависимости. Они хранятся в разных сервисах для хостинга проектов, в приватных и публичных репозиториях. И, к сожалению, не для всех этих проектов настроена автоматизация для обновления зависимостей, а для некоторых из них нет подходящих готовых решений.

Ещё об обновлении зависимости нужно как-то узнать, следить за сотнями репозиториев — это непосильная задача для меня, да и заниматься этим не хотелось бы.

Ограничения

  • Обновление зависимостей делается одной командой.
  • Обновление каждого модуля — в отдельном коммите.
  • Сообщения коммитов подобны тем, какие формируют популярные решения обновления зависимостей.

Решение

Автоматизировать набор рутинных операций, чтобы я ими занимался минимально или вовсе не занимался. По сути, нужно автоматизировать несколько шагов:

  1. Узнать о наличии новой версии модуля.
  2. go get github.com/xorcare/pointer@v1.2.2 - обновить модуль.
  3. go mod tidy - синхронизировать зависимости в go.mod и go.sum, и заодно проверить, не появилось ли проблем.
  4. go build ./... - сделать минимальные проверки, в этом случае проверить, что весь исходный код компилируется.
  5. git commit -m 'Bump github.com/xorcare/pointer from v1.2.1 to v1.2.2' - создать коммит с понятным названием.
  6. Повторить эти шаги для всех модулей от которых зависит обслуживаемый нами модуль.

История решения

Окей, погрузимся немного в историю. Стоит задача сделать себе небольшой скрипт — помощник обновления зависимостей.

Шаг первый

Нужно как-то узнать, что появились новые версии зависимостей. К счастью, в инструментарии 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 репозиториев есть готовые инструменты по автоматизации обновления зависимостей, то используйте их. Эти инструменты почти наверное будут решать задачу лучше, запускаться сами по расписанию и делать за вас красивые запросы на слияние.