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.

Update pull requests per week, before and after groupingUngrouped, a mid-sized project generates roughly one update pull request per weekday. Grouping development tooling and runtime patches collapses that to a handful, without changing how many dependency versions actually move.update pull requests opened per weekungrouped23grouped by ecosystem11grouped by consequence4the same version bumps arrive either way β€” only the number of decisions changes

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)"'
Routine updates against advisory-driven onesRoutine updates benefit from batching and a weekly window. An advisory-driven update needs to arrive immediately, stand alone so it can be read, and reach a reviewer who is expecting it.Routine updateAdvisory-driventimingweekly windowimmediatelygroupingbatchedalways alonereviewerwhoever is on rotasomeone expecting itauto-mergewhere coveredafter a human reads itone configuration block keeps the urgent case out of the batch

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
The check that keeps a batch honestThe pipeline compares the versions actually changed in the branch against the update types the group is allowed to contain. A major version found inside a minor-and-patch group fails the job, so nothing breaking can arrive unannounced.update branchpolicy jobmanifest diffreviewergrouped bump openedread changed versionsone major foundfail: split this outotherwise: green, read the tablegrouping is safe only when something verifies what is in the group

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.