Contents

Renovate Bot: Automated Dependency Updates That Don't Break Everything

Letting a robot file your dependency PRs without surrendering your sanity

Contents

Last spring I opened a side project I hadn’t touched in eighteen months and ran npm install. The lockfile resolved to 340 packages, 61 of which had known advisories, and the top-level framework was four major versions behind. Bumping it meant chasing breaking changes through a dependency tree I’d forgotten the shape of. I lost most of a Saturday, and the whole mess existed for one reason: I’d let the updates pile up until they became a project instead of a chore.

That’s the failure mode Renovate is built to kill. Instead of one enormous, terrifying upgrade every eighteen months, you get a steady drip of tiny, individually reviewable pull requests as new versions land. The maths is simple — a patch bump you review the day it ships is trivial; the same bump discovered two years and forty transitive changes later is an afternoon. Renovate keeps you on the trivial side of that line permanently, and once it’s tuned it costs you almost no attention at all.

What Renovate actually does

Advertisement

Renovate scans your repository, works out which package managers you use — npm, pip, Go modules, Cargo, Docker tags, GitHub Actions, Helm charts, Terraform providers, the lot — and opens pull requests when something has a newer version. Dependabot does a version of this too, and for a small repo its defaults are perfectly fine. Renovate’s selling point is configurability. You can group related updates into one PR, schedule them for a quiet window, auto-merge the boring ones, and pin or range exactly as you like. It is the difference between a bot that nags you and a bot that does most of the work and only interrupts you for the decisions that need a human.

It runs three ways: as a hosted GitHub App (free for public and most private repos), as a self-hosted CLI, or as a CI job you drive yourself. I self-host it because I run a Gitea instance alongside GitHub and want one tool covering both platforms with one configuration.

A configuration that won’t drown you in PRs

The mistake everyone makes first is enabling Renovate with defaults and waking up to forty open PRs. It’s the fastest way to make a team turn the bot straight back off. The fix is a renovate.json that batches sensibly and lets the safe stuff merge itself. Here is roughly what I run:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:recommended",
    ":dependencyDashboard",
    "schedule:weekly"
  ],
  "prConcurrentLimit": 5,
  "prHourlyLimit": 2,
  "packageRules": [
    {
      "description": "Auto-merge non-major dev dependencies once CI is green",
      "matchDepTypes": ["devDependencies"],
      "matchUpdateTypes": ["minor", "patch"],
      "automerge": true
    },
    {
      "description": "Group all linting tooling into one PR",
      "matchPackageNames": ["eslint", "prettier"],
      "matchPackagePatterns": ["^eslint-", "^@typescript-eslint/"],
      "groupName": "linters"
    },
    {
      "description": "Hold major updates for human review",
      "matchUpdateTypes": ["major"],
      "automerge": false,
      "labels": ["dependencies", "major"]
    }
  ]
}

The :dependencyDashboard preset is the part that sold me. It creates a single issue in your repo listing every pending and rate-limited update with checkboxes, so even when Renovate is throttling PRs you can see the whole landscape at a glance and tick the ones you want now. The prConcurrentLimit and prHourlyLimit settings keep CI from melting under a flood of parallel jobs and keep you from drowning in your own notifications. The schedule:weekly preset stacks the week’s updates into a Monday-morning batch rather than trickling them in at random hours.

Grouping is where you claw back the most sanity. Linters, type packages, and anything that ships as a family (@babel/*, @types/*, a monorepo’s sibling packages) should travel together — bumping them one at a time produces a dozen PRs that all have to land in the same order anyway. One groupName collapses that into a single reviewable change.

Scheduling, and why timing matters

Advertisement

Left unconstrained, Renovate files a PR the instant an upstream release lands, at whatever hour that happens to be. That’s fine for a solo project and miserable for a team, because updates arrive in an unpredictable dribble that nobody batches attention around. The schedule field fixes this. The presets — schedule:weekly, schedule:monthly, schedule:earlyMondays, schedule:nonOfficeHours — map to sensible cron windows, and you can write your own in natural language: "schedule": ["after 9pm on sunday"].

There’s a deeper reason to delay updates beyond tidiness: a release that’s a day old has had a day for the ecosystem to find its regressions. The stabilityDays setting (or the minimumReleaseAge in newer configs) tells Renovate to ignore a version until it’s been public for N days, so you skip the version that gets yanked six hours after publishing. I run three days on runtime dependencies. It costs almost nothing and has saved me from a handful of “0.0.1 was a mistake, here’s 0.0.2” churns that would otherwise have generated a PR, a CI run, and a follow-up PR within the same afternoon.

For a monorepo the calculus shifts again. There, grouping isn’t a nicety — it’s the difference between one coherent PR and forty that all have to merge in the same commit anyway. Group by workspace or by dependency family, and lean hard on the dependency dashboard so the whole picture stays legible in one issue rather than scattered across your PR list.

Self-hosting it on a schedule

If you don’t want the hosted app touching your code, run the CLI yourself. I do it as a cron job in a container. The token needs repo and pull-request scope; nothing more:

1
2
3
4
5
6
7
docker run --rm \
  -e RENOVATE_TOKEN="$GITHUB_PAT" \
  -e LOG_LEVEL=info \
  renovate/renovate:latest \
  --platform=github \
  --autodiscover=true \
  --autodiscover-filter="myorg/*"

--autodiscover walks every repo the token can see; the filter keeps it to one namespace. Drop that into a nightly systemd timer or a Kubernetes CronJob and you have your own private Renovate bot. If you’re self-hosting the scheduler on a cluster, the same CronJob-plus-resource-limits discipline from Kubernetes resource requests and limits applies — give the job a sane memory limit, because a large monorepo scan is not cheap.

The logs are verbose but genuinely readable. When an update is skipped, Renovate tells you exactly which rule or version constraint blocked it, which is more than I can say for most automation. That log is your first debugging tool when a bump you expected doesn’t appear.

Auto-merge without playing Russian roulette

Auto-merge is the feature people are nervous about, and rightly so. The guardrail is your test suite: Renovate only merges when required status checks pass, so the honesty here is brutal — auto-merge is exactly as safe as your CI is thorough. If you have no tests, every auto-merged patch is a coin flip. If you have a real suite that exercises the paths your dependencies touch, auto-merging patch and minor dev-dependency bumps is genuinely fine, and it removes the dozens of trivial “bump lodash 4.17.20 to 4.17.21” PRs that nobody should be reading by hand.

My rule of thumb: auto-merge devDependencies and lockfile maintenance, group runtime libraries for a quick human glance, and force every major bump through review. Major versions are where the breaking changes hide, and Renovate helpfully links the release notes and changelog right in the PR body so you can read exactly what you’re walking into before you click merge. For anything that touches the build pipeline itself — Actions, base image tags — I keep auto-merge off regardless of update type, because a broken CI runner is harder to notice than a broken test.

Troubleshooting the usual failures

A handful of problems account for nearly every “why isn’t Renovate working” message I’ve had to answer.

No PRs appear at all. Almost always the token. Check that the PAT has repo and pull-request write scope, and that Renovate actually discovered the repo — run once with LOG_LEVEL=debug and search the output for the repo name. If it’s not in the discovery list, your --autodiscover-filter is wrong or the token can’t see it.

An update you expected never shows up. Renovate isn’t broken; a rule or constraint blocked it. Grep the log for the package name — it will print the reason, usually a matchUpdateTypes you forgot excludes it, a range constraint in your manifest that already satisfies the new version, or the dependency dashboard rate-limiting it. The dashboard issue lists everything being held back with a checkbox to force it through.

Auto-merge silently doesn’t merge. The required status checks aren’t configured as required in branch protection, so Renovate has nothing to gate on and refuses to merge blind. Set the checks to required in the branch protection rules; Renovate only auto-merges when the platform reports the branch as mergeable.

A flood of PRs on first run. Expected. Add "prConcurrentLimit" and lean on the dependency dashboard’s “open all” checkbox to pace yourself, or start with "schedule:weekly" so the initial wave arrives in one batch rather than all at once.

Lockfile-only changes conflict repeatedly. Enable lockFileMaintenance on a schedule so Renovate refreshes the whole lockfile in one dedicated PR instead of fighting itself across many. This pairs well with a reproducible build; the same repeatability discipline shows up in running GitHub Actions locally with act, where you want the CI that gates these merges to behave identically on your laptop and in the cloud.

Is it worth it?

If your project has more than a handful of dependencies and any kind of CI, yes, unreservedly. Renovate converts dependency maintenance from a dreaded quarterly slog into ambient background noise you mostly ignore. The setup cost is an hour of writing packageRules and a few days of tuning the noise down to a comfortable hum, and after that it pays rent forever.

One habit worth building early: treat your Renovate config as shared infrastructure. If you run more than a couple of repos, extract your packageRules into a preset — a renovate-config repo with a single JSON file — and have every project extends it. Then a tuning change (a new group, a stricter minimumReleaseAge, an auto-merge rule you’ve decided you trust) lands everywhere at once instead of being copy-pasted and drifting out of sync. It’s the same reasoning that makes shared CI templates worth the setup: the marginal cost of a good default should be paid once, not per repo. Renovate’s own extensive preset library is proof the approach scales — config:recommended is just someone else’s shared preset that you’re extending, and you can layer your house rules on top of it.

It is overkill for a tiny script with three pinned dependencies — Dependabot’s defaults would do, and you’d never touch the config. And if you have no test suite, fix that first, because otherwise auto-merge has just automated your own breakages on a schedule. But for any real codebase, this is one of the highest-leverage bits of automation you can add. The robot files the boring PRs; you keep the interesting decisions. That trade has held up well for me across half a dozen repos and a couple of years, and I wouldn’t run a project without it now.

Advertisement
Advertisement
Smarc
Written by Smarc

Founder and editor of vo.rs. A lifelong tinkerer who self-hosts far more than is sensible, hardens Linux boxes for fun, and prods the latest AI tools to see what they can really do. The how-to guides here are the notes Smarc wishes had existed the first time round.