Automated Dependency Updates Jump to heading

Dependency automation fails in one of two directions. Either nothing is configured and the repository accumulates two years of drift until a security advisory forces a weekend of upgrades, or a bot is switched on with defaults and opens forty pull requests a week until everyone mutes the notifications β€” at which point the outcome is identical to having no automation, with extra CI spend. The difference between the two failure modes and a working setup is almost entirely about batching, scheduling and where the trust boundary sits. This part of Git Automation & CI/CD Hook Engineering covers the design decisions that keep updates flowing.

Prerequisites Jump to heading

The Shape of the Problem Jump to heading

An update bot produces a stream of changes at a rate you choose. Each one costs a pipeline run, a review decision and a merge. Left at defaults, the rate is set by the ecosystem β€” and a mid-sized JavaScript project has enough transitive churn to generate several updates a day. The three levers that make the stream manageable are grouping, scheduling and automatic merging, and they interact: aggressive grouping makes auto-merge riskier, tight scheduling makes grouping more effective.

From an upstream release to a merged updateA new upstream version is detected on a schedule, grouped with related updates into one branch, tested by the pipeline, and either merged automatically or routed to a reviewer depending on the update class.Upstream releaseregistry publishesScheduled scanonce or twice a weekGrouped branchone PR, many bumpsPipelinefull suitelockfile rebuiltMerge or reviewby update classevery lever that reduces noise sits in the middle three boxes

Step 1 β€” Choose a Schedule Before Choosing a Tool Jump to heading

A schedule is what turns a stream into a batch. Scanning continuously produces pull requests at random times, each one alone; scanning twice a week produces two batches that can be grouped.

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": ["config:recommended"],
  "schedule": ["after 2am and before 6am on monday and thursday"],
  "timezone": "Europe/London",
  "prConcurrentLimit": 5,
  "prHourlyLimit": 2
}
# Verification: dry-run the configuration without opening anything
npx --yes renovate --dry-run=full --platform=local 2>&1 | tail -30

The two limits matter as much as the schedule. Without a concurrency cap, the first run after enabling the bot opens every outstanding update at once, which is the experience that gets dependency automation switched off again.

Step 2 β€” Group Updates by Blast Radius, Not by Ecosystem Jump to heading

The instinct is to group by package manager. The useful grouping is by what a failure would mean: development tooling that cannot affect production, patch updates to runtime dependencies, and major versions that need reading release notes.

Three groups, three review expectationsDevelopment tooling cannot reach production, so a green pipeline is sufficient evidence. Runtime patches need the suite to pass and a glance at the diff. Major versions need someone to read the release notes before merging.Dev toolinglinters, types, builderscannot reach productionauto-merge on greenRuntime patchsame major and minorsuite plus a glanceauto-merge with soakMajor versionbreaking by declarationread the notesalways a humangrouping by consequence is what makes automatic merging defensible
{
  "packageRules": [
    {
      "matchDepTypes": ["devDependencies"],
      "groupName": "dev tooling",
      "automerge": true
    },
    {
      "matchUpdateTypes": ["patch"],
      "matchDepTypes": ["dependencies"],
      "groupName": "runtime patches",
      "minimumReleaseAge": "3 days"
    },
    {
      "matchUpdateTypes": ["major"],
      "groupName": null,
      "automerge": false,
      "labels": ["needs-release-notes"]
    }
  ]
}
# Verification: which group would a given package land in?
npx --yes renovate --dry-run=full --platform=local 2>&1 | grep -i 'groupName\|automerge' | head

minimumReleaseAge deserves its own mention: it delays adoption by a few days so that a compromised or broken release has time to be pulled before your pipeline consumes it. It costs nothing and removes a whole class of supply-chain exposure β€” the same reasoning behind pinning GitHub Actions to a commit SHA.

Step 3 β€” Decide Where Automatic Merging Stops Jump to heading

Auto-merge is safe exactly to the extent that the pipeline would catch the failure. That is a statement about your test suite, not about the bot, and it should be written down as such.

# An honest test: would the suite catch a dependency that breaks at runtime?
npm install --no-save [email protected] 2>/dev/null
npm test >/dev/null 2>&1 && echo "suite passed β€” is that because nothing exercises it?" \
                         || echo "suite fails fast, as it should"
git checkout -- package-lock.json

SAFETY WARNING β€” automatic merging combined with a test suite that does not exercise a dependency is a silent supply-chain channel: a malicious or broken release merges to the default branch with nobody reading the diff. Restrict auto-merge to dependency classes your pipeline genuinely covers, require a release soak period, and keep the merge going through the same protected path as human changes.

Step 4 β€” Deal With Lockfile Conflicts Structurally Jump to heading

Bulk updates conflict with each other by construction, because every one of them rewrites the same lockfile. Resolving those conflicts by hand is wasted effort β€” the file is generated, and the correct resolution is always to regenerate it.

# gitattributes: stop Git trying to merge a generated file line by line
printf 'package-lock.json merge=ours linguist-generated=true\n' >> .gitattributes
git config merge.ours.driver true

# The real resolution: take the incoming branch and rebuild
git checkout --theirs package-lock.json && npm install --package-lock-only
# Verification: a rebuilt lockfile is reproducible from the manifest alone
rm -f package-lock.json && npm install --package-lock-only && git diff --stat package-lock.json

The details, including the equivalents for other ecosystems, are in keeping lockfiles conflict-free during bulk updates.

Step 5 β€” Measure the Backlog, Not the Activity Jump to heading

A dependency bot generates visible activity whether or not it is working. The number that matters is how long an update waits before merging.

# Age of open dependency pull requests, oldest first
gh pr list --label dependencies --json number,title,createdAt \
  --jq 'sort_by(.createdAt) | .[] | "\(.createdAt[0:10])  #\(.number)  \(.title)"' | head -20
Median days an update waits, by groupDevelopment tooling merges the same day because it merges itself. Runtime patches wait for a reviewer but move within a week. Major versions sit for a month, which is the signal that they need scheduled attention rather than a faster bot.median days from opened to mergeddev tooling0.3 druntime patches4 dmajor versions31 da growing right-hand bar means the queue needs a scheduled slot, not more automation

Configuration Reference Jump to heading

SettingTypical defaultEffectWhen to change
scheduleany timeWhen the bot may open pull requestsSet a window so updates batch
prConcurrentLimit10Maximum open update pull requestsLower it to a number the team can drain
minimumReleaseAgenoneDelay before adopting a new releaseSet 3–7 days for runtime dependencies
groupNameper packageWhich updates share one pull requestGroup by consequence, not ecosystem
automergefalseMerge without a human on greenEnable only where the suite covers the risk
rangeStrategyautoWhether to widen or pin version rangesPin for applications, widen for libraries
lockFileMaintenancedisabledPeriodic full lockfile refreshEnable monthly to stop transitive drift

Troubleshooting Jump to heading

SymptomLikely causeFix
Dozens of pull requests on day oneNo concurrency limit during onboardingSet a low limit, raise it as the backlog drains
Update pull requests conflict constantlyEvery branch rewrites the lockfileRegenerate rather than merge; see the gitattributes rule
Auto-merged update broke productionThe suite does not exercise that dependencyNarrow the auto-merge scope; add coverage
The bot keeps reopening a closed updateClosing is read as β€œnot now”, not β€œnever”Add an explicit ignore rule with a reason
Major versions never get mergedNo scheduled slot for reading release notesBook one, and label them so they are findable
Pipeline cost tripledOne run per update, none groupedGroup, and cancel superseded runs

Rolling It Out Without a Flood Jump to heading

Switching a bot on for a repository that has never had one produces the backlog all at once. The order below spreads that first wave over a fortnight and lets the team calibrate as they go, rather than deciding everything up front and discovering the consequences on Monday morning.

The reason the ordering matters is psychological as much as technical. A team that sees forty pull requests on day one concludes that dependency automation is noise and never revisits the judgement; a team that sees three, merges them without incident, and then sees five learns that the bot is trustworthy and that its output is worth reading. The configuration ends up identical either way β€” what differs is whether anyone is still paying attention by the time it is finished.

One more decision belongs in this phase: who owns the stream. Dependency updates are nobody’s feature and therefore nobody’s job, which is how a backlog of ninety open update pull requests accumulates in a repository where everything else moves quickly. Naming an owner β€” a rota, a slot in a weekly routine, an explicit line in someone’s responsibilities β€” is the difference between automation that keeps the repository current and automation that generates a queue for a future emergency. The measurement in the previous step is what that owner uses; a rising median wait is the signal that the capacity assumption has stopped holding.

Frequently Asked Questions Jump to heading

Is automatic merging of dependency updates responsible? Jump to heading

It is responsible for the classes where your pipeline is the reviewer β€” build tooling, type definitions, linters β€” and irresponsible where it is not. The question is never β€œdo we trust the bot”, it is β€œwould our tests fail if this update were wrong”. Answer that per group and the policy writes itself.

Should applications pin exact versions? Jump to heading

Applications should pin and rely on the lockfile; libraries should express ranges so consumers can resolve one copy. Pinning in a library forces version conflicts onto everyone downstream, and widening in an application makes builds non-reproducible for no benefit.

How do we handle a dependency we deliberately will not upgrade? Jump to heading

Record the decision where the bot can read it: an ignore rule with a comment explaining why and what would change the answer. A closed pull request is not a decision, and the bot will correctly reopen it next week.

What about transitive dependencies nothing updates? Jump to heading

That is what periodic lockfile maintenance is for. Direct dependency bumps do not refresh the rest of the tree, so a monthly full refresh is the only thing that stops transitive versions ageing quietly for years.

Should the bot open pull requests against release branches too? Jump to heading

Only if you actively maintain those branches, and then with a much narrower policy: security fixes and nothing else. A release branch exists to change as little as possible, so routine updates there defeat its purpose while adding merge work to every backport. Configure the branch list explicitly rather than letting the bot discover branches, or a stale release line from two years ago will start receiving weekly pull requests nobody wants.