My workflow for testing Renovate config changes (2026 edition)

Over the years, I've been a heavy user of Renovate, and I think it's fair to argue I can describe myself as a "power user".
As someone who enjoys the faster feedback of tests, I've spent a good chunk of time honing the process of validating more complex configuration changes before they're merged - especially on repositories where CI builds can take some time, or (understandably) not everyone in the team (including me!) are happy re-reviewing "maybe this will work?" PRs.
I've written about how I do this, the last time in 2023, but I've learned a number of things since then.
While I was at Elastic, part of my role was supporting folks with their Renovate questions. As well as pointing folks to our internal mirror of the docs site, and some pre-baked "How do I...?" on our internal Engineering Productivity docs, I also wrote a more in-depth version of my post above. Given it was for internal use only, I was able to tailor it more closely to our self-hosted usage, and make it more appropriate to the needs of my colleagues.
As part of my new role as a maintainer on Renovate and the Renovate community manager, I end up going through this workflow a lot while working to reproduce issues for Mend's customers or community members.
In my ever present spirit of writing it as a form of blogumentation, I wanted to capture the latest setup I have.
This isn't only useful for myself, and since November I've been promising a few users of ours that "I'll be done writing it soon!", but it always takes a little longer to do a post like this - especially when we have lots of other things to do on the project!
Documenting this process is also for me to be able to distill my process for an agent to then use, so it approaches problems in the same way that I'd want to - or so it could find that actually there's a better way than I've been using!
I'll note that this is how it personally works for me, and this isn't official guidance of the Renovate project - if it were, it'd be in our docs π€
TL;DR
I use the following config.js as my starting point:
/**
* @typedef {import('renovate/dist/config/types').AllConfig} AllConfig
*/
const fs = require('fs')
// NOTE that filename would need to be changed based on the repo's config filename, or adapted to handle JSONC or JSON5
let repoConfig = JSON.parse(fs.readFileSync('renovate.json'))
/** @type {AllConfig} */
let globalConfig = {
// for instance, to simulate how the Mend Developer Platform is configured
allowedUnsafeExecutions: [
'gradleWrapper',
],
}
/** @type {AllConfig} */
let config = {
...globalConfig,
// don't require repositories to have a `renovate.json` - i.e. if we're testing how a configuration will affect a new repo
onboarding: false,
// if there is config, ignore it, and use our local copy
requireConfig: 'ignored',
// add our repo config
...repoConfig,
/*
* These settings are only for repositories where you're running Renovate without dry-run, /and/ you're expecting to get many branches/PRs created
*
* NOTE that this ordering allows these to take precedence over repo config
*/
prHourlyLimit: 100,
// allow lots of branches/PRs to be created
branchConcurrentLimit: 100,
prConcurrentLimit: 100,
// and separate to these settings, we also want to allow all PRs to be created at a given time
}
// NOTE that this isn't inlined, because it can be handy to do conditional checks
module.exports = config
This provides a strong basis for testing, and allows me to keep my repo configuration as it should be.
I'll then invoke Renovate like so:
# whitespace + comments added for readability
env
# ensure that we have GitHub authentication at least, so we can fetch changelogs, and access GitHub-only dependencies
RENOVATE_GITHUB_COM_TOKEN=$(gh auth token)
# make sure we have the right level of information
LOG_LEVEL=debug
# also capture the logs in JSONL (newline-delimited JSON) format
RENOVATE_LOG_FILE=debug-$(date +%s).jsonl
# use our Open Telemetry support (https://docs.renovatebot.com/opentelemetry/) to get additional insight into time taken / the flow of function calls
OTEL_EXPORTED_OTLP_ENDPOINT=http://localhost:4318
# if I'm running from source code
## node lib/renovate.ts
# or, more likely, running for a given version:
pnpx renovate@$version
# a lot of the time, I'll use the local platform for ease, but often will run against a real Platform
--platform local
Depending on whether I'm running against a real platform or not, I'll wire in the --platform argument and any other necessary authentication.
Why does this work for me?
The below setup is useful because:
- I can run the Renovate CLI locally against a repo
- I can mostly run in
dryRunmode- I can either use a local directory (with the
platform=local) for speed - ... or I can use a GitHub/GitLab repo, for full confidence that i.e. the right PRs get raised
- I can either use a local directory (with the
- I can continue to tweak the repo config in the
renovate.json(or any of the other filenames)- We get some editor completion from the JSDoc type hints, via, but it's not as good as the JSON Schema's autocomplete
- I can tune the global self-hosted config to allow me to test what I need, as well as mould to the organisation I'm running in
- For instance at Elastic, I'd wire in some secrets for authenticating to private registries, authenticate via my
~/.docker/config.jsonand inherit from our "base" configuration (that every repo was required to use), as well as override a couple of global settings - Or with the Mend-hosted Renovate app, I can use the same configuration we're using there in my local copy
- For instance at Elastic, I'd wire in some secrets for authenticating to private registries, authenticate via my
Most importantly - it gives me fast feedback and fairly high confidence that my changes will work (even more so if use a real repo, instead of dryRun).
Surely there's an easier way?
Renovate works to fit within your workflow and how your dependencies are set up, rather than vice versa.
For most changes, you can "YOLO" the change and see what happens, and/or iterate over it - but as mentioned above, where it makes sense, I prefer to have maximal confidence in these changes.

This also gives you an opportunity to learn more about Renovate and how it works - but I appreciate it's not for everyone.
I will also say that at least Renovate does provide you the opportunity to test things out, compared to some of our competitors, where you do very much have to "see what happens" after you change some configuration.
Getting started
There are a few steps to get set up and running for running Renovate against a repo.
Are we using an existing repo?
Most of the time, I'll test against the actual repository that we're working with (and will create a feature branch locally to commit any renovate.json changes to).
If there are lots of dependencies or files I want to ignore, I'll use enabledManagers or includePaths to hone what Renovate will process.
But there are cases where you're working with a large monorepo, and it can be really beneficial to performance to reduce the overall size of the changes Renovate is processing, even when excluding them using enabledManagers and includePaths while using platform=local.
In these cases, I'll:
- create a new directory i.e.
tmp/renovate-testing - in that directory,
git init - add the
renovate.json(or as a symlink) - symlink file(s) that I need to test from the monorepo, in the same directory structure as they exist
git add .
By keeping the existing directory structure, we can continue to use the existing renovate.json's paths for Custom Managers, allowing us the maximum confidence in being able to use this with the real repo.
Using the Local Platform
Renovate's Local Platform is a great way to get faster feedback on changes, without needing a full repository pushed to a Platform (like GitHub or GitLab).
I'll prefer to use platform=local where possible when testing changes.
I've written about the benefits before and still recommend it, even if we still have some gaps.
Confirming Renovate version
One of the things I love about Renovate is how we're constantly shipping new releases π
When trying to test/reproduce an issue, it's worth making sure that you're locally running the version of Renovate that is being used by your deployment, rather than the latest version of Renovate at a given time.
(That is, unless you've been asked by one of the folks helping debug your issue in a Discussions)
You can see the version of Renovate being used as part of Renovate's logs:
INFO: Renovate started
{
"renovateVersion": "43.56.0"
}
...
INFO: Repository started
{
"renovateVersion": "43.56.0"
}
Once we've determined this, we can use pnpx renovate@43.56.0.
(pnpx because pnpm is what we use for Renovate, but npx works too)
How dry a dry run are we running?
Renovate's dryRun functionality is useful to be able to scope the work that Renovate will do, especially while testing.
Depending on work I'm doing, I'll use different dry run settings.
dryRun=extract
I'll use dryRun=extract if I'm only testing Custom Managers, so I can validate that the right dependencies are detected.
dryRun=lookup
In the case that I'm testing with Datasources, or need to see what will happen with how branches are going to be created (i.e. if changing how grouping works), then I'll need to use dryRun=lookup, or if I'm testing how Versioning works.
dryRun=full
I'll very infrequently use this mode, largely because we didn't yet have the ability to log the commit's changes, so there's no straightforward way to determine exactly what changes Renovate will make without running it.
If I'm needing to determine what changes will be made against a repo, I'll end up running Renovate against the repo in non-dry-run mode (the default) which will start processing the repo.
Then, I'll review the branches/PRs created, and make sure they're generated correctly.
Setting up my config.js
I'll then copy in my config.js (for a mix of global self-hosted and repo config):
/**
* @typedef {import('renovate/dist/config/types').AllConfig} AllConfig
*/
const fs = require('fs')
// NOTE that filename would need to be changed based on the repo's config filename, or adapted to handle JSONC or JSON5
let repoConfig = JSON.parse(fs.readFileSync('renovate.json'))
/** @type {AllConfig} */
let globalConfig = {
// for instance, to simulate how the Mend Developer Platform is configured
allowedUnsafeExecutions: [
'gradleWrapper',
],
}
/** @type {AllConfig} */
let config = {
...globalConfig,
// don't require repositories to have a `renovate.json` - i.e. if we're testing how a configuration will affect a new repo
onboarding: false,
// if there is config, ignore it, and use our local copy
requireConfig: 'ignored',
// add our repo config
...repoConfig,
/*
* These settings are only for repositories where you're running Renovate without dry-run, /and/ you're expecting to get many branches/PRs created
*
* NOTE that this ordering allows these to take precedence over repo config
*/
prHourlyLimit: 100,
// allow lots of branches/PRs to be created
branchConcurrentLimit: 100,
prConcurrentLimit: 100,
// and separate to these settings, we also want to allow all PRs to be created at a given time
}
// NOTE that this isn't inlined, because it can be handy to do conditional checks
module.exports = config
Running Renovate + analysing the debug logs
Once this is all set up, it's time to run Renovate against the repo:
# whitespace + comments added for readability
env
# ensure that we have GitHub authentication at least, so we can fetch changelogs, and access GitHub-only dependencies
RENOVATE_GITHUB_COM_TOKEN=$(gh auth token)
# make sure we have the right level of information
LOG_LEVEL=debug
# also capture the logs in JSONL (newline-delimited JSON) format
RENOVATE_LOG_FILE=debug-$(date +%s).jsonl
# use our Open Telemetry support (https://docs.renovatebot.com/opentelemetry/) to get additional insight into time taken / the flow of function calls
OTEL_EXPORTED_OTLP_ENDPOINT=http://localhost:4318
# if I'm running from source code
## node lib/renovate.ts
# or, more likely, running for a given version:
pnpx renovate@$version
# a lot of the time, I'll use the local platform for ease, but often will run against a real Platform
--platform local
For most use-cases, I'll read the debug logs as they appear on the console or in the Mend Developer Portal (if I'm testing with a real repo, but on our Cloud offering), searching for specific log lines I know are useful (more on that in a later post).
I'll also load them into a tool that I wrote for Renovate's debug logs in particular, which allows me to read them in a similar means to what you see on the Mend Developer Platform, but for any Renovate logs.
In future versions of renovate-pretty-log-tui, I'll also surface "interesting log lines" and provide a little more information that will be useful to get at-glance view for interesting metadata.
If I'm doing more significant changes around how packages are detected or what version(s) will be available as part of changes, I'll use renovate-packagedata-diff to diff the packageFiles with updates log line.
Alternatives
- Merge config changes and see what happens (more iterative, depends on code review + CI time
- Using Renovate's "reconfigure via PR" flow
- This gives you a summary of the changes via the PR - not quite the same level of detail as we're looking at with my approach above, but it's a great start, and is slightly more user-friendly
- Writing unit tests for Custom Managers and Custom Datasources
Feedback wanted
How does this sound? Is there anything you'd like to hear me go into, other than "interesting log lines"? Any improvements you'd suggest based on your own experiences
Would a "worked example" be useful to see the full process? (Perhaps even in a video form?)
Note that we're also looking for more structured feedback for specific things in Renovate that would be appreciated if you have anything to share!