Another tool for updating dependencies
In brief
This article is a story about a small and useful script for updating go-mod-bump dependencies. Here you may be interested in the problem, solution or history of writing the script. If you want to touch the script with your hands, you can find it here in the public repository.
Disclaimer
The script has not been used in CI and may not be suitable for such use.
The story is described post factum, after a significant period of time, and therefore may contain obvious inconsistencies or distortions.
The script modifies files
go.mod
Andgo.sum
in the git working directory, and also changes the list of staged files; you can lose your changes if you use it thoughtlessly.This tool is not a competitor to popular solutions for automating dependency updates and is not intended to take their place.
Problem
I work with many repositories both at work and in personal projects, and they regularly need to update dependencies. They are stored in different project hosting services, in private and public repositories. And, unfortunately, not all of these projects have automation set up for updating dependencies, and for some of them there are no suitable ready-made solutions.
I also need to somehow find out about updating a dependency, monitor hundreds of repositories – this is an overwhelming task for me, and I wouldn’t want to do it anyway.
Restrictions
Updating dependencies is done with one command.
Each module is updated in a separate commit.
The commit messages are similar to those generated by popular dependency update solutions.
Solution
Automate a set of routine operations so that I have to do them minimally or not at all.
Essentially, there are several steps that need to be automated:
Find out if a new version of the module is available.
go get github.com/xorcare/pointer@v1.2.2
– update the module.go mod tidy
– synchronize dependencies in go.mod and go.sum, and at the same time check if any problems have appeared.go build ./...
– make minimal checks, in this case check that all source code compiles.git commit -m 'Bump github.com/xorcare/pointer from v1.2.1 to v1.2.2'
– create a commit with a clear name.Repeat these steps for all modules on which the module we are servicing depends.
History of the solution
Okay, let's dive into history a little. The task is to make a small script for yourself – a dependency update assistant.
Step one
We need to know somehow that new versions of dependencies have appeared. Luckily, the Go toolkit already has a suitable command go get -u all
it copes well with updating everything and everyone. Based on the presence of this command, we can draw 2 conclusions: Go can check for new versions of packages, and when using this command, we cannot control the process in any way.
By the time I created the script, I was already familiar with the command go list
and instead go get -u all
you can dig into it. Having dug a little into go help list
two important flags can be found:
-m
which allows you to get a list of modules using the commandgo list -m all
.-u
which is the same as ingo get -u
will allow you to check for new versions.
And now we have a more manageable team. go list -u -m all
which gives us a list of modules, the version currently used in the project, and the newest version if it differs from the installed one. Example of command output:
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]
From it we see that for the module golang.org/x/mod
current version – v0.21.0
and there is no newer version, but, for example, for golang.org/x/net
current version – v0.21.0
and there is a new version [v0.29.0]
.
To summarize: a way was found to get a list of modules and at the same time find out about the availability of new versions.
Step two
There is a list of modules and versions, all that remains is to iterate through it. In bash this is not a problem, but there are nuances.
Each line looks like this:
golang.org/x/net v0.22.0 [v0.29.0]
; we need to get 3 values from it, and I don’t really like parsing strings.Only direct modules are needed.
Only those modules are needed for which there is a new version.
In the previous step in go help list
you can apply the parameter -f
which sets the output format. I used it and made something like csv with a separator @
(at the time of creating the script I thought that this symbol would never appear in the string, but now I'm not sure).
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>'
We get a list of modules that are definitely imported by our module and definitely need to be updated:
golang.org/x/net@v0.22.0@v0.29.0
This format is easy to parse using cut
:
module=$(echo "$mdl" | cut -f1 -d@)
current_version=$(echo "$mdl" | cut -f2 -d@)
latest_version=$(echo "$mdl" | cut -f3 -d@)
Step three
We got a list of dependencies that we successfully updated. At that point, the script looked something like this:
#!/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
At this point we have a working tool that we can use. But there are still some nuances.
Step four
On projects with a large number of dependencies, the script works very slowly. This happens because go list -u
checks for updates for all modules in general, regardless of whether they are direct or indirect.
To speed up the script, you can divide the script's work into 2 stages.
Step one: we collect a list of only direct modules; similar actions were done in the previous steps, so the idea of the following command is born quite easily:
go list -m -f '{{.Path}}{{if .Indirect}}<SKIP>{{end}}' all | grep -v '<SKIP>'
github.com/xorcare/tornado
golang.org/x/net
Step two: storing this list in a variable $DIRECT_MODULES
we can request a check for updates only for direct modules:
go list -u -m -f '{{.Path}}@{{.Version}}@{{if .Update}}{{.Update.Version}}{{end}}{{if not .Update}}<SKIP>{{end}}' $DIRECT_MODULES | grep -v '<SKIP>'
This way we will get a list of modules that need to be updated without having to go through all the modules.
Step five
Already during use, it turns out that modules cannot always be updated, and it is necessary to somehow skip modules whose update leads to errors. This can be implemented by combining the commands responsible for checking and updating dependencies into one function:
function update_module() {
go get "$1"
go mod tidy
go build ./...
}
And then, in case of failure, simply skip the commit, rolling back the changes in go.mod and go.sum:
if ! update_module "${module}@${latest_version}" >/dev/null 2>&1 >/dev/null; then
git checkout -f HEAD -- go.mod go.sum
fi
After that, we proceed to update the next module from the list until the list is complete.
At this stage, the basic mechanics of the script are already similar to published version.
The last step, but not the least important
At first I used the script only myself, and there was no point in decorating it. But later I shared the script in a public repository, having previously refined it.
Removed the hardcoded one
all
and gave the developer the ability to choose the list of modules themselves. Implicitly, this gave the ability to update Go in go.mod.Added flags
-h
And-help
with usage examples. And also added a flag-p
so that you can add a prefix to the commit title if you follow some commit naming conventions.For cases when the module update ends with an error, I added the output of a detailed message and commands that can be copied and run for manual diagnosis of the problem.
Conclusion
As a result of the work, I got a convenient script go-mod-bump, which solves the task and does it well. It is relatively cross-platform, as it uses a small number of commands and I tested it on macOS, Debian, and Ubuntu.
Perhaps it would have been worthwhile to make this tool in the spirit of Go and write it in Go. But, at that moment, for simplicity, I chose bash, and I have no inspiration to rewrite it in Go yet.
Afterword
If the problem described is close to you, then you may like to use a script. If your git repository hosts have ready-made tools for automating dependency updates, then use them. These tools will almost certainly solve the problem better, run themselves on schedule and make beautiful merge requests for you.