Grouping dependency updates to reduce PR noise Jump to heading
The complaint is always the same: nobody reads the update pull requests any more. The cause is rarely the number of dependencies and almost always the grouping, because an ungrouped bot converts one upstream release into one review decision, and most of those decisions carry no information. Grouping restores the signal by making each pull request a decision somebody would actually make. This recipe covers how to group without hiding the changes that matter, within automated dependency updates.
When to use this approach Jump to heading
- More than about five update pull requests are open at any time.
- Reviewers merge them without reading, or do not merge them at all.
- Several updates routinely conflict on the lockfile.
- CI spend on update branches is a noticeable fraction of the total.
- If updates are already merging within a day or two, grouping buys little; look at auto-merge scope instead.
Step 1 β Count what you have before changing the rules Jump to heading
# Update pull requests opened in the last 90 days, by week
gh pr list --label dependencies --state all --limit 300 \
--json createdAt --jq 'group_by(.createdAt[0:7])[] | {month: .[0].createdAt[0:7], count: length}' # And how many were merged without a review comment β the "nobody reads them" signal
gh pr list --label dependencies --state merged --limit 100 \
--json number,comments --jq '[.[] | select(.comments == 0)] | length' These two numbers frame everything that follows. A high open rate with near-zero comments means grouping will help; a low rate with slow merges means the problem is review capacity, not volume.
Step 2 β Group by what a failure would mean Jump to heading
Grouping by ecosystem is easy and mostly useless: it puts a linter and a database driver in one pull request because both are npm packages. Group instead by the consequence of getting it wrong.
{
"packageRules": [
{
"matchDepTypes": ["devDependencies"],
"matchUpdateTypes": ["minor", "patch"],
"groupName": "dev tooling",
"schedule": ["before 6am on monday"]
},
{
"matchDepTypes": ["dependencies"],
"matchUpdateTypes": ["patch"],
"groupName": "runtime patches",
"minimumReleaseAge": "3 days"
},
{
"matchDepTypes": ["dependencies"],
"matchUpdateTypes": ["minor"],
"groupName": "runtime minors"
},
{
"matchUpdateTypes": ["major"],
"groupName": null
}
]
} # Verification: a dry run should show four branch names, not forty
npx --yes renovate --dry-run=full --platform=local 2>&1 | grep -o 'branchName[^,]*' | sort -u "groupName": null on majors is the rule people forget. Without it, a later catch-all rule sweeps major versions into a group, and a breaking change arrives inside a pull request titled βchore: update dependenciesβ.
Step 3 β Keep security updates ungrouped Jump to heading
An advisory-driven update has a different urgency and a different reviewer. Grouping it with routine churn delays it behind unrelated decisions.
{
"vulnerabilityAlerts": {
"groupName": null,
"labels": ["security", "priority"],
"schedule": ["at any time"],
"prPriority": 10
}
} # Verification: security updates bypass the schedule and stand alone
gh pr list --label security --json number,title,createdAt --jq '.[] | "\(.createdAt[0:10]) \(.title)"' Step 4 β Size the group against review capacity Jump to heading
A group of sixty package bumps is one pull request and zero real review. The useful size is the number of lines a reviewer will read, which in practice is small.
# How large are the grouped pull requests actually getting?
gh pr list --label dependencies --json number,additions,deletions \
--jq '.[] | {pr: .number, changed: (.additions + .deletions)}' | head {
"packageRules": [
{
"matchDepTypes": ["devDependencies"],
"groupName": "dev tooling",
"separateMinorPatch": true,
"prBodyColumns": ["Package", "Type", "Update", "Change", "Age", "Adoption"]
}
]
} What changed: the pull request body now carries the per-package detail, so the reviewer can scan a table instead of reading a lockfile diff. The lockfile itself should be marked generated so it collapses in review β the mechanics are in keeping generated files out of review diffs.
Step 5 β Verify that grouping did not hide anything Jump to heading
The risk of grouping is that a meaningful change rides along inside a batch. Two checks catch it.
# 1. No major version inside a grouped branch
git log origin/main..HEAD --format='' -p -- package.json \
| grep -E '^\+.*"\^?[0-9]+' | head
# 2. The set of changed packages matches the pull request body
git diff origin/main...HEAD -- package.json | grep -E '^[-+]\s+"' | sort Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Does grouping make failures harder to diagnose? Jump to heading
Slightly, and the cure is bisection rather than smaller groups. If a grouped update breaks the build, reverting the whole branch restores service immediately, and git bisect over the individual bumps finds the culprit quickly β the technique in automating git bisect with a test script.
Should patch and minor updates share a group? Jump to heading
Usually not for runtime dependencies. A patch is a bug fix by convention and a minor adds behaviour, so they deserve different soak periods and different auto-merge policies. For development tooling the distinction rarely matters and one group is simpler.
What if a group never goes green? Jump to heading
One member is broken and holding the rest hostage. Configure the bot to split a failing group, or pin the offender with an explicit ignore rule and a reason, so the remaining updates can flow.
Related Jump to heading
- Automated Dependency Updates β the parent topic and the three levers.
- Auto-Merging Patch Updates Safely β what to do with the groups once they are small.
- Configuring Renovate for a Monorepo β grouping when one bump touches twelve manifests.