DEV Community

yureki_lab
yureki_lab

Posted on

How I Use Claude Code to Tackle Dependency Upgrades Without Losing a Weekend

TL;DR

For years, "upgrade dependencies" meant blocking off a Saturday, praying nothing exploded, and reading changelogs until my eyes glazed over. I rebuilt that workflow around Claude Code (currently running v2.x) doing the grunt work — one package at a time, changelog-first, test-gated, with automatic rollback on failure. My last quarterly upgrade round took 85 minutes instead of the usual 6+ hours, and I shipped zero regressions. Here's the loop, the guardrails that make it safe, and what I got wrong the first three times I tried it.

The Problem

I maintain a mid-sized Node.js/TypeScript backend — nothing exotic, maybe 60 direct dependencies. Every quarter, npm outdated would spit out a wall of packages, and I'd do what most of us do: ignore it until a security advisory forced my hand, then upgrade everything at once in a panic.

That "big bang" approach has an obvious failure mode. When you bump 15 packages in one commit and something breaks, you have no idea which one did it. I've spent entire evenings bisecting a broken test suite across a pile of simultaneous version bumps, only to find the culprit was a patch-level bump to a logging library that changed its default output format.

The worst case I hit was a batch of 22 packages bumped in a single PR, merged late on a Friday because the CI run happened to pass. Two days later, a background job started silently dropping retries — not failing loudly, just quietly not retrying. It took me most of a Monday to trace it back to a minor version bump in a queue library that had changed its default backoff behavior. Nothing in the diff screamed "this is the one," because it was buried among 21 other unrelated bumps. That single incident is what finally pushed me to stop batching.

The real cost wasn't the upgrading — it was the psychological tax. Dependency upgrades became a thing I dreaded and postponed, which meant I was running months-old, unpatched packages in production most of the time. That's the actual risk: not that upgrades are hard, but that dread makes you defer them until you're forced into it under worse conditions — an active CVE, a broken build from a transitive dependency, whatever forces your hand.

I wanted a process boring enough that I'd actually run it monthly instead of quarterly.

How I Solved It

The core idea: never bump more than one package per test run, and let the agent do the boring parts — reading changelogs, writing the bump, running the suite, and reverting cleanly if anything fails.

Here's the loop:

flowchart TD
    A[List outdated packages] --> B[Pick next package]
    B --> C[Fetch changelog / release notes]
    C --> D{Breaking changes?}
    D -->|No| E[Bump version, install]
    D -->|Yes| F[Draft migration notes for this package]
    F --> E
    E --> G[Run full test suite]
    G -->|Pass| H[Commit with changelog summary]
    G -->|Fail| I[Revert package.json + lockfile]
    I --> J[Log failure reason, skip package]
    H --> B
    J --> B
Enter fullscreen mode Exit fullscreen mode

In practice, this is a Claude Code session with a fairly tight prompt. I don't let it run fully unattended for this one — dependency upgrades touch production, so I keep the permission mode conservative and review each commit before pushing. The value isn't "unattended automation," it's "the boring 80% happens without me typing anything."

A simplified version of the per-package step looks like this:

#!/usr/bin/env bash
# one-package-at-a-time.sh -- run per outdated package
set -euo pipefail

PKG=$1
TARGET=$(npm view "$PKG" version)

echo "Upgrading $PKG to $TARGET..."
npm install "$PKG@$TARGET"

if npm test --silent; then
  git add package.json package-lock.json
  git commit -m "chore: bump $PKG to $TARGET"
  echo "OK: $PKG"
else
  git checkout -- package.json package-lock.json
  npm install --silent
  echo "FAILED: $PKG -- reverted, needs manual look"
fi
Enter fullscreen mode Exit fullscreen mode

I hand Claude Code the list of outdated packages and ask it to:

  1. Sort them by risk (patch, then minor, then major)
  2. For each one, pull the changelog (GitHub releases or CHANGELOG.md) and summarize breaking changes in plain English
  3. Run the script above, one package at a time
  4. If a bump fails, read the test output, decide whether it's a real incompatibility or a flaky test, and only escalate real ones to me

That last point matters. In my first attempt, I had it stop and ask me after every single failure, which defeated the purpose since I was back to babysitting. Now it retries once, in case of a flaky integration test, and only surfaces genuine failures with a one-paragraph diagnosis: which package, which error, and its best guess at the cause.

For major version bumps specifically, I don't let it auto-commit even on green tests. Major bumps get a migration summary written into the PR description instead, and I review those by hand. Patch and minor bumps, gated on a full green test run, go through on their own.

Handling the messy cases

Two things broke my first version of this loop before I patched them in:

  • Lockfile churn from transitive deps. Bumping one direct dependency sometimes drags a dozen transitive packages along with it in the lockfile diff, which makes the "one package per commit" story a little dishonest. I now have Claude Code note in the commit message which transitive packages moved and why, so a future git blame doesn't leave me guessing.
  • Peer dependency conflicts. Some upgrades fail at npm install before tests even run, because a peer dependency range doesn't allow the new version yet. Instead of forcing it with --legacy-peer-deps and hoping, the agent now checks whether a compatible peer bump exists and proposes bumping both together as a single logical unit — still one test run, just two packages that are coupled by definition.

Neither of these came up until I'd run the loop for a few weeks. The first pass through any new workflow like this tends to handle the happy path fine; the edge cases show up once you've automated away the tedious part and started trusting the output a little too much.

Lessons Learned

1. One package per test run is the entire trick. It sounds almost too simple, but isolating the variable is what turns "something broke, good luck" into "package X broke, here's exactly why." I resisted this at first because it felt slower — it's not, because you're not debugging a tangle of simultaneous changes afterward.

2. Changelog-reading is where the agent earns its keep, not the install step. The mechanical bump was never the hard part. Reading a wall of GitHub release notes for 15 packages and figuring out which ones actually affect your code is the part that used to eat hours. Having that summarized first meant I could triage risk before touching anything.

3. Treat major version bumps as a different workflow entirely, not "the same loop but scarier." I learned this after a major bump to an ORM passed all my tests, shipped, and then broke a code path my test suite didn't cover. Green tests on a major bump are necessary, not sufficient. Now those always get a human pass over the actual diff, no exceptions.

4. Flaky test retries need a hard cap. My second attempt let the agent retry failing tests indefinitely "just to be sure," which occasionally masked a real regression behind three retries before it happened to pass. One retry, then treat any subsequent failure as real. Better a false alarm than a silent miss.

5. The dread was the real bug, not the dependencies. The measurable win (6 hours to 85 minutes) matters less than the behavioral change: I now run this monthly instead of quarterly, because it's no longer something I put off. Reducing the psychological cost of a maintenance task can matter more than reducing its raw time cost.

6. Commit messages are documentation, not just a log. Having the agent write "bumped X to Y" was never useful on its own — what I actually needed six months later, when trying to figure out why a behavior changed, was the one-line changelog summary sitting right there in the commit. I now treat the commit message as the actual deliverable of each step, not the version bump itself.

What's Next

I'm extending the same "isolate the variable, gate on tests, escalate only real failures" pattern to a task I've been avoiding even longer: rotating and re-verifying third-party API integrations that don't have great test coverage. The changelog-summarization step turned out to be reusable there too — most integration breakage is announced somewhere, you just have to actually read it.

I'm also experimenting with letting the agent draft the PR description directly from the changelog summaries it already collected, so the human review step (which I'm keeping, especially for major bumps) has better context to work from instead of a bare "bump lodash to 4.17.22."

Wrap-up

If you're sitting on a pile of npm outdated dread right now, the one-package-per-commit rule alone is worth stealing even without an agent involved — it's what actually makes the debugging tractable. Pairing it with Claude Code just means you're not the one reading forty changelogs to get there.

If this was useful, follow me here on Dev.to — I'm writing up more of these "how I actually run an AI coding agent day to day" posts as I go. Curious what maintenance tasks you've been putting off — drop them in the comments, I might tackle one next.

Top comments (0)