/ Tags: DEVELOPER TIPS / Categories: TIPS

Keeping Dependencies Updated — bundle outdated, Dependabot, and When to Hold

There are two failure modes and teams pick one without deciding. Either nobody updates anything until a CVE forces a jump across four major versions in a week, or Dependabot opens eleven PRs every Monday and everyone merges them without reading, which works right up until the morning it doesn’t. The middle path needs about twenty minutes a week and a policy you can state in one paragraph.

Know What You Have


Example:

bundle outdated

Output:

Gem            Current  Latest   Requested  Groups
nokogiri       1.15.4   1.16.2   ~> 1.15    default
puma           6.4.0    6.4.2               default
rails          7.1.2    7.2.1    ~> 7.1     default
rspec-rails    6.0.3    6.1.1    ~> 6.0     development, test

The useful flags:

bundle outdated --strict          # only versions your Gemfile constraints allow
bundle outdated --patch           # patch-level updates only
bundle outdated --minor           # minor and patch
bundle outdated --groups          # group by Gemfile group

--strict is the one that tells you what you can actually update today without editing the Gemfile. Everything else in the list is a decision, not a chore.

Security first, always
gem install bundler-audit
bundle audit check --update

Output:

Name: rack
Version: 3.0.8
CVE: CVE-2025-XXXXX
Criticality: High
URL: https://github.com/rack/rack/security/advisories/...
Title: Denial of service in multipart parsing
Solution: upgrade to >= 3.0.11

This runs in seconds and belongs in CI on every build. It’s the one dependency check that isn’t optional — everything else is maintenance, this is exposure.

The Update Cadence That Works


Weekly, batched, low-risk: patch updates for everything, applied together, verified by the test suite.

bundle update --patch --conservative
bundle exec rspec

--conservative prevents Bundler from also bumping the dependencies of the gems you’re updating, which keeps the diff small and the blast radius understandable.

Monthly, individually: minor versions, one gem or one related group per PR.

bundle update rspec-rails rspec-core rspec-expectations rspec-mocks

Quarterly or on demand: major versions, each as its own project with a read of the upgrade guide.

The reason to batch patches and separate majors is bisection. When a batched patch update breaks something, git bisect over eleven gems is unpleasant; but patch releases rarely break things, so the risk is low. Major versions break things routinely, which is why they get their own PR with their own revert.

Reading a Diff Before Merging


The single habit that separates safe updating from gambling. For any gem you’re not familiar with, read what changed.

Example:

# What actually changed between versions
gem contents rack --version 3.0.8 > /tmp/old.txt

# Better: diff the released gems
gem install diffend   # or:
git clone https://github.com/rack/rack /tmp/rack
git -C /tmp/rack diff v3.0.8..v3.0.11 --stat

For most updates the changelog is enough:

bundle exec gem changelog rack    # if the gem publishes one via metadata

What you’re scanning for: behaviour changes not marked as breaking, new dependencies pulled in, and anything touching serialization, authentication, or SQL generation. Twenty seconds per gem, and it catches the “patch release that changed a default” case that semver alone won’t warn you about.

Configuring Dependabot Properly


The default configuration produces noise. A tuned one produces a manageable stream.

Example:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: bundler
    directory: "/"
    schedule:
      interval: weekly
      day: monday
      time: "09:00"
      timezone: "Asia/Kathmandu"
    open-pull-requests-limit: 5
    versioning-strategy: increase-if-necessary

    groups:
      rspec:
        patterns: ["rspec*"]
      rubocop:
        patterns: ["rubocop*"]
      patch-updates:
        update-types: ["patch"]

    ignore:
      - dependency-name: "rails"
        update-types: ["version-update:semver-major"]
      - dependency-name: "pg"
        update-types: ["version-update:semver-major"]

    labels: ["dependencies"]

Three settings carry most of the value:

  • groups turns eleven PRs into three. Grouping all patch updates into one PR is the biggest single reduction in noise.
  • ignore for majors on framework-level gems, because those need a planned upgrade rather than a surprise PR.
  • open-pull-requests-limit stops the backlog from becoming a wall people stop looking at.

Add security-only updates on a faster schedule if your ecosystem supports it — those you do want immediately.

When to Hold


Not updating is sometimes the right call, and it should be a recorded decision rather than neglect.

  • The gem is unmaintained and the new version is a community fork. Verify the fork’s provenance before switching; a hostile takeover of an abandoned gem is a real supply-chain attack pattern.
  • You’re mid-migration on something the gem touches. Two moving parts at once makes any failure ambiguous.
  • The new major requires a Ruby you’re not on. Sequence it: Ruby first, then the gem.
  • A transitive dependency conflict. bundle update x failing with a resolution error usually means something else is pinning. bundle exec gem dependency <gem> --reverse finds who.
  • Release is less than a week old and the gem is critical. Letting a release sit for a few days lets someone else find the regression. This isn’t paranoia — it’s why staged rollouts exist.

Record the hold where the next person will see it:

# Gemfile
# Pinned: 2.4 changes the default timezone handling for parsed dates.
# Migration tracked in PROJ-882. Revisit after Q3.
gem "chronic", "~> 2.3.0"

A pin with no comment becomes permanent. A pin with a reason and a ticket becomes work.

The Lockfile Is the Contract


Gemfile.lock pins exact versions for every gem including transitive ones. Two rules:

Commit it for applications, always. Not committing it means your production install can differ from what you tested.

Don’t commit it for gems you publish — a library should resolve against whatever the consuming application has.

And check the lockfile diff on every dependency PR:

git diff Gemfile.lock

An update to one gem that changes forty lines of the lockfile pulled in something you didn’t ask for. That’s not necessarily wrong, but it should be noticed.

Pro-Tip: Add bundle audit and bundle outdated --strict to CI as separate jobs with different failure semantics. bundle audit should fail the build — a known CVE in a running application is not a warning. bundle outdated should never fail the build; run it on a schedule and have it open an issue or post to a channel instead. Mixing them is why teams end up with a permanently-red pipeline that everyone learns to ignore, and once people are ignoring the dependency job, the security check stops working too. The distinction is simple: block on exposure, report on staleness.

Major Upgrades Deserve a Plan


For something like Rails, treat it as a project rather than a PR:

  1. Read the upgrade guide end to end before touching anything
  2. Update to the latest patch of the current major first
  3. Run with deprecation warnings as errors and fix them all — config.active_support.deprecation = :raise
  4. Bump the major, run rails app:update, and review every conflict individually
  5. Ship behind whatever staged rollout you have, and watch error rates for a week

Step three is the one people skip and the one that does most of the work. Deprecation warnings in version N are removals in version N+1; clearing them before the bump turns a hundred failures into ten.

Conclusion


The working policy is short: bundle audit in CI as a blocking check, weekly batched patch updates verified by the suite, monthly individual minors, majors as planned projects with the deprecation pass done first. Group Dependabot’s output so Monday produces three PRs instead of eleven, ignore major bumps on framework gems so they don’t arrive unannounced, and comment every pin with a reason and a ticket. The goal isn’t to be on the latest version of everything — it’s to never be more than one manageable step away from it.

FAQs


Q1: Should I run bundle update with no arguments?
Rarely. It updates everything to the newest versions your constraints allow, producing an enormous lockfile diff that’s impossible to review or bisect. Use --patch, --conservative, or name specific gems.

Q2: What’s the difference between bundle update --conservative and --strict?
--conservative avoids updating the dependencies of the gems you named. --strict refuses to go beyond your Gemfile’s version constraints. They’re often used together.

Q3: How do I find which gem is holding back an update?
bundle exec gem dependency <gem> --reverse lists what depends on it. bundle update <gem> will also print a resolution error naming the conflicting constraint.

Q4: Is Dependabot enough on its own?
For keeping current, mostly. It doesn’t replace bundle audit for advisories in transitive dependencies, and it can’t tell you that a patch release changed a default. Treat it as a scheduler, not a reviewer.

Q5: What about gems with no releases in three years?
Not automatically a problem — a small, complete gem may simply be finished. Check open issues and whether it still works on your Ruby version. If it’s both unmaintained and load-bearing, budget time to vendor or replace it before you’re forced to.

cdrrazan

Rajan Bhattarai

Full Stack Software Developer! 💻 🏡 Grad. Student, MCS. 🎓 Class of '23. GitKraken Ambassador 🇳🇵 2021/22. Works with Ruby / Rails. Photography when no coding. Also tweets a lot at TW / @cdrrazan!

Read More