Keeping Dependencies Current
Working on a project of almost any size will almost always require the use of external dependencies that can often put us in a position of alert fatigue where we might have to chase our projects for CVE’s that have been announced where it can feel like constantly plugging a dam. Using a technique to automate this chore which can be reviewed at a later date is extremely helpful.
There are widely available tools in the ecosystem, however; I’m presenting this as a fun exercise to demonstrate how we can get in touch with the machinery that we can implement to do this.
What we will do:
- Use start.spring.io to create a simple project.
- Configure pom project with specific dependency rules.
- Create
dependencies.shscript. - Add GitLab CI job to perform automatic dependency checks on a
chore/version-updatesbranch.
Create Your Project
Using the spring starter at start.spring.io, create a maven based project. As we’re using the versions-maven-plugin we will require this to be a maven project.
You can use this template which is a bookmark of a spring 4.0.6 project using maven and JDK 26.
Configure Project to Ignore pre-releases
I’ve found that in maven central, some projects will release release
candidates, milestones, alpha and other releases that I do not want to have my
dependency management script to try an upgrade to. My original approach to
solve this was to set the maven.version.ignore maven property to ignore these
patterns, like the following:
create our own tool to do the same job.
<properties>
<maven.version.ignore>(?i).*[\-\.](m|rc|dev|alpha|beta)[\-\.]?[0-9]*</maven.version.ignore>
<processDependencyManagementTransitive>${processDependencyManagementTransitive}</processDependencyManagementTransitive>
</properties>
This works well, however; if you want to have rules where you may want to be
using pre-releases or milestones from specific vendors, i.e. you may be testing
the newest spring milestones, you will need to extend this by adding a ruleSet
to the build/plugins section of your pom.
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>versions-maven-plugin</artifactId>
<configuration>
<ruleSet>
<rules>
<rule>
<ignoreVersions>
<ignoreVersion>
<type>regex</type>
<version>(?i).*[\-\.](m|rc|dev|alpha|beta)[\-\.]?[0-9]*</version>
</ignoreVersion>
</ignoreVersions>
</rule>
<rule>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
</rule>
<rule>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-bom</artifactId>
</rule>
</rules>
</ruleSet>
<processDependencyManagementTransitive>${processDependencyManagementTransitive}</processDependencyManagementTransitive>
</configuration>
</plugin>
</plugins>
</build>
Note that I’ve also set the property processDependencyManagementTransitive,
keep this in your properties section as our script will set this later.
Dependency Update Script
We will now create a script that will perform the dependency update check and
optionally perform a git commit if updates were found.
There are two sets of maven goals we could use
To show only:
versions:display-dependency-updatesversions:display-plugin-updatesversions:display-property-updates
Or, to perform the updates my modifying the pom.
versions:update-parentversions:update-propertiesversions:use-latest-versions
Refer to the versions-maven-plugin documentation for more on what these goals do.
We will write a script that gives us the following capabilities:
Usage: dependencies.sh [-cCsStu]
-c : Perform git commit opening editor for review
-C : Perform git commit
-s : Don't perform snapshot updates
-S : Force update snapshot updates
-t : Process transitive dependencies
-u : Perform update
For this script, we will begin with the setup and a getopts block that takes
these command line arguments.
#!/usr/bin/env bash
goals="
versions:display-dependency-updates
versions:display-plugin-updates
versions:display-property-updates
"
snaps=""
margs=""
while getopts cChsStu OPTION; do
case $OPTION in
h) echo "
Usage: $0 [-cCsStu]
-c : Perform git commit opening editor for review
-C : Perform git commit
-s : Don't perform snapshot updates
-S : Force update snapshot updates
-t : Process transitive dependencies
-u : Perform update
" >&2
exit 1
;;
c) commit_mode=edit ;;
C) commit_mode=perform ;;
s) snaps="-nsu" ;;
S) snaps="-U" ;;
t) margs="${margs} -DprocessDependencyManagementTransitive=true" ;;
u) goals="
versions:update-parent
versions:update-properties
versions:use-latest-versions
"
;;
esac
done
shift $((OPTIND - 1))
This allows the user to either view (default) or update (-u). When viewing the
caller can test for transitive dependency updates with -t. We can also commit
changes automatically with -c to open an editor or have -C perform the
commit.
In order to capture the dependencies that were updated, we will tee the
output from maven (using ./mvnw here) to allow us to parse the content when
complete.
mvn_log="$(mktemp)" || {
echo "Couldn't create temp file" >&2
exit 1
}
trap 'rm -f "$mvn_log"' EXIT
echo ./mvnw $snaps $margs $goals
./mvnw $snaps $margs $goals | tee $mvn_log
Here we will parse the output file to list off all our changes. A regular expression here formats the content into the following form:
- {{ DEPENDENCY }}: {{ FROM }} -> {{ TO }}
This format will strip out property managed versions such as ${spring-modulith.version}
and replace it with spring-modulith to make our git commit more readable.
changes="$(
grep "Updated " "$mvn_log"|\
sed -E 's/^.* Updated (\$\{)?([^.}]*)(.version)?\}? from (.*) to (.*)$/- \2: \4 -> \5/')"
We’re creating an expression that looks for lines that have the following core pattern:
^.* Updated: Looks from the start of line until we reache the wordUpdatedwith a leading and trailing space.(\$\{)?: May contain the sequence${which we want to ignore so must remember to skip this capture group.([^.}]): Capture all characters that are not'.'or'}', there is a slight issue with this in that if you use a property that doesn’t end in.versionbut does have a dot'.'in it’s name, it might not be processed correctly.(.version)?\}?: skip the optional phrase.versionand the optional}character.from (.*) to (.*)$capture the from and to versions.
If all these sequences match the line, it will be replaced with the capture
groups we are interested in - \2: \4 -> \5.
We can now test for this to see if there were any changes detected, if there are, we can then perform the determined git commit option chosen by the caller.
if [ -z "$changes" ]; then
echo "No changes detected to commit."
else
commit_msg="[chore] Dependency and plugin updates
$changes"
echo -e "$commit_msg" > .git/COMMIT_EDITMSG
if [ -z "$commit_mode" ]; then
echo -e "\n --- Changes staged for commit ---"
else
echo -e "\n--- Committing Changes ---"
git add pom.xml **/pom.xml 2>/dev/null
if [ "$commit_mode" == "edit" ]; then
git commit -m "$commit_msg" -e
else
git commit -m "$commit_msg"
fi
fi
fi
Setup GitLab Automation
This could now be setup on an automated chore/version-updates branch to be
performed once a week or on a schedule of your choosing.
Configure your .gitlab-ci.yml with blocks similar to the following, your
configuration may look different, however; in my case I use GraalVM CE edition
for my use-cases.
.maven-base:
image:
name: ghcr.io/graalvm/jdk-community:25
entrypoint: [""]
cache:
key: "maven-shared-cache"
paths:
- .m2/repository
- .sonar/cache
version_updates:
extends: .maven-base
stage: test
script: |
microdnf install -y git
git config --global user.name "GitLab CI Bot"
git config --global user.email "brett.ryan+gitlab-bot@gmail.com"
git remote set-url origin https://oauth2:${GITLAB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git
./dependencies.sh -u -C
if [[ "$(git rev-list --count @{u}..HEAD)" != "0" ]]; then
git pull origin ${CI_COMMIT_REF_NAME} --rebase
git push origin HEAD:${CI_COMMIT_REF_NAME}
fi
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule" && $CI_COMMIT_BRANCH == "chore/version-updates"'
when: always
- when: never
GITLAB_TOKEN. This token will need to have write_repository access.In this script we take advantage of the script using the -C option to perform
the automated commit, which; can then be detected with git rev-list --count @{u}..HEAD
to detect if changes were made. If they were, then we can then push the commits.
Next Steps
There are several ways you could progress this by having full regression tests performed, notifications to slack or teams to notify the development team when the branch has been updated.
You could now integrate this into a release cycle where a QA manager performs
validation and owns the chore branch, and; what should be done in the
negative scenario where there may be versions that are not desired? Would you
make it a practice to include them into the ruleSet? This could even be
automated based on build failures.