Third of seven posts on how I keep everything I run up to date without trusting fresh releases. It stands on its own: if your Renovate automerge is stuck, start here.
Automerge is configured, CI is green, and the pull request just sits there. A day later it is still there. Renovate reports no error, because from its point of view nothing has failed.
I hit this for six different reasons, each costing an evening. My update policy lives in one preset that forty repositories read (part two), so each time, updates stalled everywhere at once and nothing said so. Below, for each cause, is the symptom that gives it away and the config that fixes it; at the end, the order I check them in and how I now get alerted instead of noticing days later.
One question found all six: when automerge does nothing, ask what Renovate is waiting for, not which setting is wrong. Most of the time it was waiting for something that would never arrive.
1. 0 days means “never”, not “now”
Some rules in my policy should have no wait: security fixes, digest pins, images I build myself. The natural way to write that is "minimumReleaseAge": "0 days", and it does not work. 0 days normalises to no value, so Renovate posts no stability status check. On a repository with no other CI, the forge reports the commit as pending, with an empty list of checks. Renovate waits for a green branch that never goes green.
The fix is a floor, not a zero:
{
"matchUpdateTypes": ["digest", "pinDigest"],
"minimumReleaseAge": "1 minute",
"minimumReleaseAgeBehaviour": "timestamp-optional",
"automerge": true
}(For container images a later rule raises digest updates to two days, as in part one’s table.) One minute is enough for Renovate to post a check that then passes. Back then internalChecksAsSuccess (Renovate’s own checks counting as a green build on a repository without CI) turned that check into a green build. I have since switched it off and given every repository at least one real check, because it let Renovate merge a pull request in the same run that opened it, before CI had even started. I found this in the debug logs of two pull requests that had been green and unmerged for days.
2. A missing release date blocks for ever too
Since Renovate 42, minimumReleaseAgeBehaviour defaults to timestamp-required: a release whose publication date Renovate cannot find is held indefinitely. Rightly so, or an artifact proxy that strips dates would silently disable the age gate from part one. But most registries do not date digests, and many lockfile updates carry no date, so for those rules only, timestamp-optional is right. (The old spelling, relaxed, still works but has left the schema, so editors flag it.)
Harder: a whole registry without dates. GitHub’s container registry publishes none, nor do several vendor registries, and nine of my images sat in Pending Status Checks. timestamp-optional would unblock them by turning their gate off. Better to look the version up where the dates are. An image on GitHub’s registry is almost always built from a GitHub repository of the same name and tagged like its releases, so one rule with overrideDatasource covers most of them:
{
"matchDatasources": ["docker"],
"matchPackageNames": ["/^ghcr\\.io/[^/]+/[^/]+$/"],
"matchCurrentValue": "/^v\\d/",
"matchJsonata": ["$not($exists(currentDigest))"],
"overrideDatasource": "github-releases",
"overridePackageName": "{{replace '^ghcr\\.io/' '' packageName}}"
}Two lines come from failures. The path pattern allows exactly two levels, because a Helm chart stored deeper has no repository of that name. And matchJsonata skips digest-pinned images: after an override the digest comes from GitHub too, where it is a git commit SHA. Renovate wrote one into an image reference, and the build failed with invalid reference format.
Docker Hub loses dates too. Past the first thousand tags its API stops paging, and Renovate falls back to a dateless tag list for the whole image (Docker: error fetching data from DockerHub in the debug log). The Renovate image is one of those, and a minor update of it waited two months. Where the project also publishes GitHub releases (Renovate does), point the lookup there. For official images such as Python and Node, a custom datasource reads only the first page of tags, newest first, filtered by suffix (-alpine, -slim), which keeps the dates.
A date can also be wrong. A floating tag such as 3.12-alpine is rebuilt whenever its base image changes, and dated from the latest rebuild. Official images rebuild every week or two, so a 14-day gate on a floating tag never passes. The official images I follow by floating tag get 3 days, which still bounds the real age.
3. The preset you extend can override you
Renovate merges a repository’s config over its preset’s, not uniformly. Top-level objects deep-merge, so the repository wins. packageRules arrays concatenate: the repository’s rules come after the preset’s, and the last matching rule wins.
My infrastructure repository clamps every package into its deployment window with a wildcard rule:
"packageRules": [
{ "matchPackageNames": ["**"], "automergeSchedule": ["* 0-1 * * 2,3,4"] },
"..."
]Every exemption the preset writes as a package rule comes before that clamp and loses: an “at any time” there waits for Tuesday. (The preset no longer sets a window at all: my infrastructure repository is now the only one with a merge window, because it is the only one whose merges deploy anything.)
Security fixes survive, for a reason I learned late. Renovate turns each advisory into its own rule, appended after all the others, and applies vulnerabilityAlerts to it as a force that no package rule can override (the reason part two’s preset sets no automerge there). So "automergeSchedule": ["at any time"] inside the preset’s vulnerabilityAlerts already beats the clamp. My earlier workaround, a matchCategories: ["security"] rule at the end of the array, never matched: Renovate’s categories are ecosystems (docker, helm, terraform), there is no security category, and the validator accepts it anyway.
4. platformAutomerge changes who decides
platformAutomerge defaults to true: Renovate sets the forge’s native auto-merge flag, and the forge merges the moment the last check goes green. In my infrastructure repository I turn it off, so Renovate merges itself, only during its own runs and only once it judges the branch green. An empty list of checks never qualifies, which is why failure 1 bites at all.
The native flag would have merged those stuck pull requests, but whenever CI finished, which a deployment window exists to prevent. No window: take the platform flag. With one, turning it off is required, and failure 1 is the price.
My dotfiles and my GitLab app repositories have no window to protect. A dotfiles merge reaches a machine only when I apply it, and an app merge builds nothing at all (images are built when a release ships): a running app changes when my infrastructure repository bumps its version, inside that repository’s own window. So they keep the platform flag and merge whenever CI is green. My GitHub repositories have no window either, and since November they take the platform flag too. That needed one change first: their rulesets required only the pull request title check, so GitHub would have merged as soon as the title passed, with the tests still running. Each ruleset now requires the repository’s real CI jobs. The exception is this blog’s repository. It is private, GitHub’s free plan has no rulesets for private repositories, and without required checks the flag would merge before CI, so there Renovate still merges itself.
5. A correct config that still merges nothing
Here every setting was correct. Renovate was waiting for a run inside its merge window, and none was scheduled there.
automergeSchedule says when a merge is allowed, but Renovate merges only while it runs, and self-hosted, you decide when that is: two schedules in two files, with nothing connecting them. I moved my Renovate Kubernetes CronJob clear of a node reboot window. Right change, right reason, and it moved every run out of the merge window (both were still in local time then):
runs at [00:30, 01:30, 02:30, 13:30], window 03:00-06:00Automerge stopped with no error and no failing check, and I noticed days later. A pre-commit hook now parses both files and fails the commit unless a run falls inside the window with at least fifteen minutes to spare.
6. A full queue hides new updates
This one looks like a missing pull request: a major update of an image I build had been out since the evening before and was never proposed. Renovate was waiting for a free branch slot.
Renovate keeps at most branchConcurrentLimit update branches open. And with platformAutomerge off, a run merges at most two pull requests per repository: after an automerge Renovate retries the repository once, then stops until the next run (Automerged but already retried once in the debug log). So the merge window has a capacity:
merges per week, per repository = 2 × runs inside the window × nights the window is openMine had two runs on three nights, twelve merges a week, against about sixteen new updates. The backlog grew until it hit the limit of ten open branches, and new updates were skipped. The only traces were a debug-level Reached branch limit - skipping branch creation and a Rate-Limited entry on the Dependency Dashboard, which reads like routine throttling. Failure 2 made it worse: an update that can never pass the age gate holds its slot for good, as the Renovate update did for two months.
I added two runs inside the window (24 merges a week against about 16 arrivals) and raised branchConcurrentLimit and prConcurrentLimit from 10 to 20, so a bad week has room. A higher limit alone only delays the problem, because the queue still grows.
My GitLab repositories hit the same ceiling with fewer updates. Renovate runs once a day for them, so the window held six merges a week per repository (2 × 1 × 3), against mise alone, which releases almost daily for the dotfiles. When CI came back after a day down, eighteen green merge requests were queued, and at two per repository per night the busiest repositories would have needed two nights to clear. With nothing to deploy, they took the platform flag instead (failure 4): GitLab merges as soon as CI is green, with no limit per run.
Making the silence loud
Self-hosted, the debug log that explains all this vanishes with the pod. Each run now writes it to a volume (LOG_FILE, kept 30 days), and a daily job reads every log since its last check plus the Dependency Dashboard. It reports each new problem once, all in one digest, and again only if it comes back after clearing: a run missing, failed or at the branch limit; a repository problem on the dashboard; an update pending longer than my longest gate or rate-limited for over ten days; an automerge pull request still open after eight.
A second message covers the opposite case, pull requests that are waiting for me. Once a day the job lists every Renovate pull request on all three forges that is marked manual-review or has red CI, and sends it only when something new appears, plus a reminder on Mondays. It also flags a GitHub repository whose Dependency Dashboard still lists updates waiting for their weekly schedule after eight days. The hosted Renovate app visits quiet repositories irregularly, and a dashboard that has not changed while updates still wait means a whole weekly window passed without a visit. A day without a message means there is nothing for me to do.
Reading every log, not only the newest, matters: Renovate aborts a repository’s run on a network error to an external host and still exits 0. One run timed out on a lookup against gitlab.com and did nothing, and the next run’s log had already replaced it as the newest. The job now alerts on External host error causing abort too.
What to take from this
Run Renovate once with LOG_LEVEL=debug before changing any setting. Then check, in this order:
- Self-hosted: is Renovate running at all? My
CronJobpod once could not be scheduled: every run stayedPendinguntil its deadline, with no Renovate log at all.kubectl get jobsshowsDeadlineExceeded;kubectl get eventsshows why. - Is any “no wait” rule written as
0 days? Make it1 minute. - Does the release have a date? If not,
timestamp-optionalon that rule only. If the whole registry has none, useoverrideDatasource. Give floating tags a short gate. - Does a repository rule override the preset’s? The last matching rule wins. Security fixes are safe in
vulnerabilityAlerts; put other exemptions last. - Is
platformAutomergeoff? Then Renovate merges only during its runs, and needs a definite green status. - Self-hosted: does a run fall inside
automergeSchedule, at least fifteen minutes before it closes? Add a check that compares the two. - Can the window keep up? Two merges per run, times runs in the window, times nights, against weekly arrivals. Rate-Limited on the dashboard means the branch limit is already full.
- Will you hear about the next one? Keep debug logs past the pod’s lifetime and alert on updates pending longer than your longest gate.