Development Containers, or DevContainers for short, are lightweight and portable development environments defined and configured through a devcontainer.json
file. This file allows referencing existing images, Dockerfile
s, or docker-compose.yml
files for setup. However, specifying a particular version of a container image in the devcontainer.json
file presents a challenge, as manual updates become necessary with each new release, potentially leading to errors and increased time consumption.
Renovate is one of the tools in automated dependency management within software development workflows, offering solutions to such challenges.
This blog post aims to explore the seamless configuration of Renovate to handle devcontainer.json files, enabling automatic updates for outdated image references, thus streamlining the development process.
Suppose that we have the following configuration file:
{ "name": "My Dev Container", "image": "ghcr.io/org/repo/some-container:1.0.0", // other customizations go here // ... }
This file references the ghcr.io/org/repo/some-container:1.0.0
image. Opting against the latest
tag is crucial to avoid unexpected disruptions. Nevertheless, staying informed about new releases of the image is essential for review and potential integration into our container. As of the time of writing, neither Dependabot nor Renovate natively supports devconrtainer.json
files. However, it is possible to teach Renovate to handle them. There is an open issue to add a devcontainer.json
manager to Renovate.
Renovate employs the so-called “custom managers” to handle new file formats. The only available custom manager is the Regex manager, which operates based on regular expressions and named capture groups.
To configure a manager, typically, the following components are required:
- Regular expression-based capture rules and/or templates to match the dependency name, its current version, and some additional information like data source or registry URL.
- Regular expressions to match files within a repository to parse and extract relevant information.
Let us start with file name matching rules. According to the specification, the valid filenames are:
.devcontainer/devcontainer.json
.devcontainer.json
.devcontainer/<folder>/devcontainer.json
(where<folder>
is a sub-folder, one level deep)
With this in mind, we can construct the following rules:
... "extends": ["config:recommended"], "customManagers": [ { "customType": "regex", "fileMatch": [ "^.devcontainer/([^/]+/)?devcontainer\\.json$", "^.devcontainer\\.json$" ], } ] ...
Now, let us move to the matching rules.
To configure the manager effectively, the following information is necessary:
- Dependency name: Corresponds to the name of the image.
- Current version: Represents the tag of the image.
- Data source: Always set to
docker
since we are matching a Docker image; it also establishes the versioning scheme asdocker
. - Current digest: Refers to the SHA-256 digest of the image, if available.
Thus, to match the following pattern:
"image": "<dependencyName>:<currentVersion>@sha256:<currentDigest>"
We use the following regular expression rule:
"matchStrings": [ "\"image\"\\s*:\\s*\"(?<depName>[^:\"]+):(?<currentValue>[^@\"]+)(@(?<currentDigest>sha256:[a-f0-9]+))?\"" ],
This is the configuration we have now:
{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["config:recommended"], "customManagers": [ { "customType": "regex", "fileMatch": [ "^.devcontainer/([^/]+/)?devcontainer\\.json$", "^.devcontainer\\.json$" ], "matchStrings": [ "\"image\":\\s\"(?<depName>[^:\"]+):(?<currentValue>[^@\"]+)(@(?<currentDigest>sha256:[a-f0-9]+))?\"" ], "datasourceTemplate": "docker" } ] }
However, this configuration did not work as expected. Renovate job logs showed the following:
DEBUG: latest commit (branch="renovate/pin-dependencies")
{
"branchName": "master"
"latestCommitDate": "2024-02-11T08:49:13+02:00"
}
DEBUG: manager.getUpdatedPackageFiles() reuseExistingBranch=false (branch="renovate/pin-dependencies")
DEBUG: Starting search at index 36 (branch="renovate/pin-dependencies")
{
"packageFile": ".devcontainer/devcontainer.json"
"depName": "ghcr.io/sjinks/codespaces/wordpress-all-in-one"
}
DEBUG: Found match at index 36 (branch="renovate/pin-dependencies")
{
"packageFile": ".devcontainer/devcontainer.json"
"depName": "ghcr.io/sjinks/codespaces/wordpress-all-in-one"
}
DEBUG: Digest is not updated (branch="renovate/pin-dependencies")
{
"manager": "regex"
"packageFile": ".devcontainer/devcontainer.json"
"expectedValue": "sha256:6dcfb4b02deaa6bbe814dc6462290f63720290bcaa0cf0f2b7cdf8a0410b7b81"
}
WARN: Error updating branch: update failure (branch="renovate/pin-dependencies")
This is what has happened: Renovate tried to pin the digest of the image (ghcr.io/sjinks/codespaces/wordpress-all-in-one). However, it could not find that digest when it checked the file after the replacement.
Digging deeper, we see this:
"regex": [
{
"deps": [
{
"depName": "ghcr.io/sjinks/codespaces/wordpress-all-in-one",
"currentValue": "3",
"datasource": "docker",
"replaceString": "\"image\": \"ghcr.io/sjinks/codespaces/wordpress-all-in-one:3\"",
"updates": [
{
"isPinDigest": true,
"updateType": "pinDigest",
"newValue": "3",
"newDigest": "sha256:6dcfb4b02deaa6bbe814dc6462290f63720290bcaa0cf0f2b7cdf8a0410b7b81",
"branchName": "renovate/pin-dependencies"
}
],
"packageName": "ghcr.io/sjinks/codespaces/wordpress-all-in-one",
"versioning": "docker",
"warnings": [],
"registryUrl": "https://ghcr.io",
"currentVersion": "3",
"fixedVersion": "3"
}
],
"matchStrings": [
"\"image\"\\s*:\\s*\"(?<depName>[^:\"]+):(?<currentValue>[^@\"]+)(@(?<currentDigest>sha256:[a-f0-9]+))?\""
],
"datasourceTemplate": "docker",
"packageFile": ".devcontainer/devcontainer.json"
}
]
If we look at the replaceString
, we will see that it has no digest. This explains why Renovate could not find the image digest after applying the update.
To solve this, we need to provide our own replacement pattern. This is possible with the help of the autoReplaceStringTemplate setting. Renovate’s templates are flexible enough and use handlebars under the hood.
If we examine the replaceString
, we will notice that it lacks a digest. This absence elucidates why Renovate failed to find the image digest.
To address this, we must furnish our replacement pattern. This task is achievable through the autoReplaceStringTemplate
setting. Renovate’s templates, powered by handlebars, offer customization flexibility.
"autoReplaceStringTemplate": "\"image\": \"{{{depName}}}:{{{newValue}}}{{#if newDigest}}@{{{newDigest}}}{{/if}}\"",
The final configuration will look like this:
{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["config:recommended"], "customManagers": [ { "customType": "regex", "fileMatch": [ "^.devcontainer/([^/]+/)?devcontainer\\.json$", "^.devcontainer\\.json$" ], "matchStrings": [ "\"image\":\\s\"(?<depName>[^:\"]+):(?<currentValue>[^@\"]+)(@(?<currentDigest>sha256:[a-f0-9]+))?\"" ], "autoReplaceStringTemplate": "\"image\": \"{{{depName}}}:{{{newValue}}}{{#if newDigest}}@{{{newDigest}}}{{/if}}\"", "datasourceTemplate": "docker" } ] }
With this configuration, Renovate successfully created the Pull Request: