Dieser Artikel ist auch auf Deutsch verfügbar

There are a number of reasons why we should keep our dependencies up to date. Alongside new features that often come with new versions and possible performance improvements, security plays a particularly important role here. New versions correct discovered, known or even unknown security issues, making our application more robust. And over the long term, it is useful to integrate new versions promptly even if they don’t address acute security problems. After all, this ensures we are ready for the next security update when it arrives.

This problem was on clear display with Log4Shell. Prompt security updates were in fact released for this issue, but many applications still have not managed to install the patch. In many cases, this is because the applications are very out of date, making updating to the current version that much more complicated and resulting in a high risk of errors.

To avoid falling into this trap ourselves, we should update our dependencies without undue delay. We face three main challenges here. First is the large number of dependencies. Even with smaller applications, it usually doesn’t take long for the number of dependencies to hit middle double digits. There are the programming language itself, the frameworks and libraries that we use, and the tools that we employ for building and packaging. In the end, each update itself can potentially pose a challenge. Depending on the version jump and the changes involved, this could range from increasing a version number to major modifications to our code. And not to forget: we need to learn about the update in the first place.

Previously, I generally relied on RSS or mailing lists to hear about new updates. This worked well for large projects at least. However, I still have to actively think about carrying out the update. And when there are lots of dependencies, I also have follow a great many news sources.

This is precisely where Renovate comes in as a bot-based solution for this problem. The bot analyzes our source code repository and extracts our dependencies. It then checks whether newer versions are available. If so, the bot creates a new branch, where it increments the version number in a commit and creates a merge request (MR), also called a pull request on GitHub.

To avoid replicating the documentation in this article, we will consider an example to illustrate the use, advanced configuration, and functioning of Renovate.

Example project

Because this article is concerned only with the dependencies of our project, the details of the application source code aren’t important this time and won’t be shown.

Our project is a Java application built with Maven and using Spring Boot as framework. We are also using SDKMAN! for managing the Java and Maven version. For Maven, we are additionally using the Maven wrapper.

Plus, we need Node.js to bundle CSS and JavaScript. In this case, we are making use of the Node Version Manager (nvm) and, within Maven, the frontend-maven-plugin. Since the application is run as a container, there is also a Dockerfile.

A self-hosted GitLab is used as source code repository, which makes use of a pipeline with GitLab CI for building and packaging the application. This requires its own container image, which is built via the file Dockerfile.release. Finally, the pipeline also utilizes jbang to execute scripts written in Java.

This stack was not conjured from thin air, by the way. It reflects the actual stack of an application developed by me and used internally. There are certainly simpler stacks – but also more complicated ones.

To avoid a ton of listings, I am dispensing with showing all the configuration files in detail. However, when the specific content is relevant or must be modified for Renovate, it is shown, of course.

Onboarding and initial configuration

When the bot examines our repository for the first time, it creates an MR (see Figure 1) for onboarding our project. The actual code modification consists here of adding the file renovate.json with the content from Listing 1. The description of the MR already provides a good overview of what Renovate has detected and which updates directly resulted from this.

Fig. 1: Onboarding Merge Request
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json"
}
Listing 1: Onboarding renovate.json

After having merged this MR, we still needed to configure the bot further. For one thing, we would like for all new update MRs to be assigned to me as processor. For another, the onboarding MR explained that only two MRs are opened per hour. We want to remove this limit in order to receive all MRs as quickly as possible. For this, we add three configuration values to the file renovate.json hinzu, which now looks like Listing 2.

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "assignees": [
    "michael"
  ],
  "prConcurrentLimit": 0,
  "prHourlyLimit": 0
}
Listing 2: renovate.json with configuration values

When the bot runs the next time with no limits on the number of MRs and the instruction to assign them to me, a bunch of new MRs are generated, as shown in Figure 2.

Fig. 2: Renovate Merge Requests

To allow for more complex configurations, Renovate offers us presets in addition to configuration values. These are integrated via the configuration value extends. Renovate supplies a large number of these right out of the box. These include the preset config:recommended. Since this is recommended, as the name says, we’d like to make use of it. The documentation for this preset shows us that it consists in turn of a number of other presets.

Now that we are familiar with presets, we will switch our previous configuration values over to these. In all three cases, there is a specialized preset that is somewhat more readable and shortens the configuration. Plus, we also want Renovate to rebase all open MRs as soon as there are new commits on the main branch. For this, we add the preset :rebaseStalePrs. Our Renovate configuration now looks as shown in Listing 3.

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:recommended",
    ":assignee(michael)",
    ":prConcurrentLimitNone",
    ":prHourlyLimitNone",
    ":rebaseStalePrs"
  ]
}
Listing 3: renovate.json with presets

This new configuration produces three changes during the next run of Renovate. First, all previously opened MRs are rebased since we have made new commits to our main branch due to the configuration changes. Second, a new MR is created for our JDK dependencies because Renovate has now detected this thanks to the recommended configuration.

Third, Renovate has now also created a Dependency Dashboard (see Fig. 3) in the form of an issue, which primarily lists all currently opened MRs. However, it also contains a list of all detected dependencies and can be used to communicate any identified problems. For example, if Renovate could not resolve certain dependencies or if a misconfiguration were detected, these would be reported here. In addition, the dashboard can also be used for interaction with the bot, as we will see in the following section.

Fig. 3: Dependency Dashboard

Configuring updates

As we can see, a large number of required updates are generated quickly, especially in older projects. For instance, the bot recommends an update from Spring Boot 3.0.10 to 3.1.4. There is, however, also another version of 3.0 in the form of 3.0.11. If we would like to receive an MR for this update, we should change the configuration value separateMinorPatch to true or add the preset :separatePatchReleases. Then we will receive an additional MR for the update to 3.0.11. If we don’t want to receive any more additional MRs for the major and minor updates of Spring Boot because we are getting these in another way, we can accomplish this with a specific configuration for individual dependencies (see Listing 4).

{
  ...
  "packageRules": [
    {
      "matchDatasources": ["maven"],
      "matchPackageNames": [
        "org.springframework.boot:spring-boot-starter-parent",
        "org.springframework.boot:spring-boot-dependencies"
      ],
      "matchUpdateTypes": ["major", "minor"],
      "enabled": false
    }
  ]
}
Listing 4: Deactivating major and minor updates for Spring Boot

The recommended middle path in this case would be to use the Dependency Dashboard for these updates, as mentioned above. In this case, we replace the configuration of "enabled": false in Listing 4 with "dependencyDashboardApproval": true. Then the Dependency Dashboard issue contains the part shown in Figure 4 and the MR for the update to Spring Boot 3.1.4 would be created only after checking the checkbox.

Fig. 4: Dashboard approval for minor Spring Boot update

Support for jbang, SDKMAN!, and the frontend-maven-plugin

All use cases are not always directly supported by Renovate. But when it comes to detecting dependencies in previously unsupported files, we can supply our own solution.

The component for detecting dependencies in files is referred to in Renovate as a manager. In addition to many specialized managers, there is also a configurable one that performs the extraction via regular expressions. For example, jbang allows us to specify dependencies via comments at the start of the file (see Listing 5), which are then downloaded to execute the script. Unfortunately, these are not detected by Renovate by default. However, they can be easily identified with a regular expression, meaning that we can configure a custom manager, as shown in Listing 6.

///usr/bin/env jbang "$0" "$@" ; exit $?
//JAVA 17+
//JAVA_OPTIONS -Dapple.awt.UIElement=true
//DEPS org.springframework.boot:spring-boot-dependencies:3.0.10@pom
//DEPS org.springframework.boot:spring-boot-starter-jdbc
//DEPS com.h2database:h2
//DEPS org.json:json:20230227
//DEPS commons-codec:commons-codec

class GenerateDatabase {
    public static void main(String[] args) {
        ...
    }
}
Listing 5: jbang script
{
  ...
  "customManagers": [
    {
      "customType": "regex",
      "fileMatch": [
        "^bin/.*\\.java$"
      ],
      "matchStrings": [
        "//DEPS (?<depName>.*?:.*?):(?<currentValue>[^@\n]*)(@pom)?"
      ],
      "datasourceTemplate": "maven"
    }
  ]
}
Listing 6: RegexManager for jbang scripts

This manager checks for the defined string in all files ending with .java in the bin folder. If it is found, relevant information is extracted via named groups. The group currentValue is required and must contain the currently used version. We must also identify the name of the dependency and the source, called datasource by Renovate. This can be done either via the regular expression or with fixed configuration templates. In this case, we extract the name via the group depName and directly set the source to maven via the template datasourceTemplate.

During the next run, Renovate will now identify two additional dependencies, specifically org.springframework.boot:spring-boot-dependencies and org.json:json. Because our config:recommended also accesses the preset group:recommended and this refers in turn to group:springBoot, only one MR is opened for new Spring Boot versions, which simultaneously updates the version in our jbang script and in the POM.

A similar problem exists for our use of SDKMAN!. This is not automatically supported, but we can solve that with additional RegexManagers (see Listing 7).

{
  ...
  "customManagers": [
    ...
    {
      "customType": "regex",
      "fileMatch": [
        "^\\.sdkmanrc$"
      ],
      "matchStrings": [
        "java=(?<currentValue>.*)\\n"
      ],
      "datasourceTemplate": "java-version",
      "depNameTemplate": "java"
    },
    {
      "customType": "regex",
      "fileMatch": [
        "^\\.sdkmanrc$"
      ],
      "matchStrings": [
        "maven=(?<currentValue>.*)\\n"
      ],
      "datasourceTemplate": "maven",
      "depNameTemplate": "maven",
      "packageNameTemplate": "org.apache.maven:apache-maven",
      "versioningTemplate": "maven"
    }
  ]
}
Listing 7: RegexManagers for Java and Maven versions in SDKMAN!

There is one other point in this project where we make use of a RegexManager solution. Because we are using the frontend-maven-plugin, we must specify the version of Node.js to use. This is also not automatically detected by Renovate. Listing 8 solves this problem, however.

{
  ...
  "customManagers": [
    ...
    {
      "customType": "regex",
      "fileMatch": [
        "^pom.xml$"
      ],
      "matchStrings": [
        "<nodeVersion>v(?<currentValue>.*)</nodeVersion>"
      ],
      "datasourceTemplate": "node-version",
      "depNameTemplate": "node"
    }
  ]
}
Listing 8: RegexManager for Node.js version in the frontend-maven-plugin

As we can see in Figure 5, another five dependencies are detected by these additional configurations, and we have less manual work to do.

Fig. 5: Dependencies detected by the RegexManager

Container and digest pinning/updates

By default, Renovate already includes broad support for containers. For example, the base images in our two Docker files as well as within our GitLab CI configuration were detected (see Fig. 6).

Fig. 6: Detected container dependencies

However, additional packages are installed in Dockerfile.release (see Listing 9) via the package manager of Alpine Linux. In this case, it is recommended to specify a fixed version for these packages as well.

FROM alpine:3.18.2
SHELL ["/bin/sh", "-euo", "pipefail", "-c"]

RUN apk --no-cache add --update \
      bash=5.2.15-r5 \
      docker-cli=23.0.6-r4 \
      httpie=3.2.1-r4
listing 9: Dockerfile.release

Of course, we also want to be notified of updates for these via Renovate. Luckily, a preset that we can use for this already exists in the form of regexManagers:dockerfileVersions. However, we still need to modify our Dockerfile.release as shown in Listing 10. For this, we use a combination of comments and variables to specify the versions, which can then once again be detected under the hood by a RegexManager.

FROM alpine:3.18.2
SHELL ["/bin/sh", "-euo", "pipefail", "-c"]

# renovate: datasource=repology depName=alpine_3_18/bash versioning=loose
ENV APK_BASH_VERSION="5.2.15-r5"

# renovate: datasource=repology depName=alpine_3_18/docker-cli versioning=loose
ENV APK_DOCKER_CLI_VERSION="23.0.6-r4"

# renovate: datasource=repology depName=alpine_3_18/httpie versioning=loose
ENV APK_HTTPIE_VERSION="3.2.1-r4"

RUN apk --no-cache add --update \
      bash="${APK_BASH_VERSION}" \
      docker-cli="${APK_DOCKER_CLI_VERSION}" \
      httpie="${APK_HTTPIE_VERSION}"
Listing 10: Dockerfile.release with Renovate support

However, containers have another special aspect: floating tags. In brief, this means that even if I specify a specific tag of a container image such as eclipse-temurin:17.0.7_7-jdk-alpine, I don’t necessarily always receive the exact same image. This is because container tags are not unmodifiable, meaning that the target they point to can change over time. For this reason, it is a good idea to specify the exact image used. This requires additionally specifying a digest. Renovate is able to help us here again. The first step is to use the preset docker:pinDigests. During the next run, Renovate will create an MR (see Fig. 7) in which all detected images are provided with a digest.

Fig. 7: MR for container digest pinning

While it’s true that tags can change for arbitrary reasons, in practice this generally happens because the version of a dependency, such as for the JDK, is fixed within our tag, although the underlying base image has changed due to package updates. Because we should also carry out these updates, the digest pinning will be followed by Renovate creating a number of MRs in which only the digest is updated. To avoid being overwhelmed by these, it can be useful – assuming appropriate test coverage and security perspective – to allow Renovate to merge these automatically itself, without us needing to do anything. We can use the preset :automergeDigest for this. Renovate will then do the merge itself once the pipeline for a digest update MR has been successfully executed.

Custom presets

With all of these configurations, our renovate.json has now grown a fair bit and become unwieldy in a few places. If we would like to use parts of this configuration in other projects, the time has now come to move these into custom presets. The result for this project is shown in Listing 11.

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "local>example/renovate//presets/java",
    ":assignee(michael)",
    ":automergeDigest",
    ":separatePatchReleases"
  ],
  "packageRules": [
    {
      "matchDatasources": ["maven"],
      "matchPackageNames": [
        "org.springframework.boot:spring-boot-starter-parent",
        "org.springframework.boot:spring-boot-dependencies"
      ],
      "matchUpdateTypes": ["major", "minor"],
      "dependencyDashboardApproval": true
    }
  ],
  "customManagers": [
    {
      "customType": "regex",
      "fileMatch": [
        "^bin/.*\\.java$"
      ],
      "matchStrings": [
        "//DEPS (?<depName>.*?:.*?):(?<currentValue>[^@\n]*)(@pom)?"
      ],
      "datasourceTemplate": "maven"
    },
    {
      "customType": "regex",
      "fileMatch": [
        "^pom.xml$"
      ],
      "matchStrings": [
        "<nodeVersion>v(?<currentValue>.*)</nodeVersion>"
      ],
      "datasourceTemplate": "node-version",
      "depNameTemplate": "node"
    }
  ]
}
Listing 11: Renovate configuration with custom presets

We have removed some of the previously used presets as well as the RegexManager for the SDKMAN! configuration and added the preset local>example/renovate//presets/java in their place. This preset says that Renovate should integrate the file presets/java.json (see Listing 12) in the same GitLab (local) in project example/renovate. The SDKMAN! configuration has now also found a global place in this preset. Plus, a preset is enabled for supporting Maven properties for configuration of dependency versions, regexManagers:mavenPropertyVersions, and another custom preset is also used (see Listing 13).

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "local>example/renovate//presets/default",
    "regexManagers:mavenPropertyVersions"
  ],
  "customManagers": [
    {
      "customType": "regex",
      "fileMatch": [
        "^\\.sdkmanrc$"
      ],
      "matchStrings": [
        "java=(?<currentValue>.*)\\n"
      ],
      "datasourceTemplate": "java-version",
      "depNameTemplate": "java"
    },
    {
      "customType": "regex",
      "fileMatch": [
        "^\\.sdkmanrc$"
      ],
      "matchStrings": [
        "maven=(?<currentValue>.*)\\n"
      ],
      "datasourceTemplate": "maven",
      "depNameTemplate": "maven",
      "packageNameTemplate": "org.apache.maven:apache-maven",
      "versioningTemplate": "maven"
    }
  ]
}
Listing 12: Custom preset for Java projects
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:best-practices",
    ":prConcurrentLimitNone",
    ":prHourlyLimitNone",
    ":rebaseStalePrs",
    "regexManagers:dockerfileVersions"
  ]
}
Listing 13: Own Default Preset

This preset also forms the basis for non-Java projects. We rely here on a slew of useful settings configured via the built-in preset config:best-practices. We also remove the limits and enable automatic rebasing and the support for packages in Docker files.

Configuration and bot setup

We have now learned a lot about the use and configuration of Renovate from a project perspective. Next, we need to address how to operate this bot. In our current setup, we use the existing GitLab function of scheduled pipelines. This means that the pipeline shown in Listing 14 will be run regularly.

stages:
  - renovate

renovate:
  stage: renovate
  image: renovate/renovate:37.11.1@sha256:64090c90f002fc7668921d119d245b85535ee4575330e03993acd7bad48570ab
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
  script:
    - renovate
  cache:
    key: ${CI_COMMIT_REF_SLUG}-renovate
    paths:
      - renovate/cache/renovate/repository/
Listing 14: Renovate GitLab CI configuration

This is based on the current Renovate container image and starts the bot, which is configured via the file config.js (see Listing 15). Due to the use of autodiscover, the bot will automatically manage all projects that it can find. This means that only the technical GitLab user of the bot needs to be invited into projects in which we wish to use Renovate. Our custom onboardingConfig ensures that our custom default preset is configured already during onboarding.

module.exports = {
  platform: 'gitlab',
  endpoint: process.env.CI_API_V4_URL,
  autodiscover: true,
  dryRun: null,
  gitAuthor: 'Renovate Bot <[email protected]>',
  onboardingConfig: {
    '$schema': 'https://docs.renovatebot.com/renovate-schema.json',
    extends: [
      'local>example/renovate//presets/default',
    ],
  },
  baseDir: `${process.env.CI_PROJECT_DIR}/renovate`,
  optimizeForDisabled: true,
  repositoryCache: 'enabled',
  registryAliases: {
    '$CI_REGISTRY': process.env.CI_REGISTRY,
  },
  hostRules: [
    {
      hostType: 'docker',
      matchHost : 'https://index.docker.io',
      username : 'examplebot,
      password : process.env.DOCKERHUB_TOKEN,
    },
    {
      matchHost: `https://${process.env.CI_REGISTRY}`,
      username: 'renovate-bot',
      password: process.env.RENOVATE_TOKEN,
    },
    {
      hostType: 'maven',
      matchHost: process.env.CI_API_V4_URL,
      token: process.env.RENOVATE_TOKEN,
    },
  ],
}
Listing 15: Renovate configuration

The entry under registryAliases allows use of the environment variable CI_REGISTRY in the project-specific GitLab CI configurations. Without this entry, Renovate would not detect that part of the image is a placeholder.

The hostRules serve for authorization with various dependency sources. For instance, we have to log into the public Docker registry, the Docker Hub, to have a higher limit for API requests and avoid being blocked. The two other entries allow querying of the GitLab container and package registry. However, it should also be noted here that the technical user of the bot needs access to the corresponding projects in order to even see them.

Because we are running within the same GitLab in which we are also managing the projects, we can make use of existing environment variables in many places. Nevertheless, there are others that we must define (see Fig. 8). In addition to the token for the Docker Hub, DOCKERHUB_TOKEN, we need a token for GitHub: GITHUB_COM_TOKEN. Release notes are obtained from there via API and inserted into the description of the MR. The RENOVATE_GIT_PRIVATE_KEY is used by Renovate to sign the created commits in Git. Lastly, the RENOVATE_TOKEN serves for accessing GitLab itself and is therefore also required.

Fig. 8: Additional environment variables for the bot

Conclusion

In this article, we got acquainted with Renovate as a bot that helps us keep our dependencies up to date. This is especially important for quickly eliminating security vulnerabilities.

In addition to the base configuration, we also examined a variety of customization options. We learned that we can remove the default limits via configuration values or presets, how to ensure that the MRs always remain at the same commit status, and how to assign them to a single person. Then we saw how to further configure specific updates and how to detect additional dependencies with custom RegexManagers. We also learned about a solution for packages within container images and the use of floating tags.

Finally, we also saw how to operate the bot within GitLab through the use of scheduled pipelines.

TAGS