A dependency upgrade is a behavior change you didn't write, applied to every call site at once. Treat it like any other brownfield change: know the blast radius before you bump, prove behavior with the existing suite, and never mix the upgrade with code changes that would hide which one broke the build.
-
Classify the bump. Patch (x.y.Z) → proceed with suite proof. Minor (x.Y.z) → read the release notes first. Major (X.y.z) → full protocol below. A lockfile-only refresh that pulls many transitive bumps counts as one upgrade unit, proven the same way.
-
Read the changelog/release notes for the traversed range — not just the target version. Breaking changes, deprecations, and behavior changes (defaults flipping, stricter validation) accumulate across every version you skip. No changelog available → diff the library's source tags or treat the bump as major.
-
Map the usage surface. Grep the import name across the repo and check specs/brownfield/code-graph.json ext: edges for the package (when present) — every importing file is blast radius. For each breaking change in the notes, check whether any usage site hits it. Usage you cannot see (config-file conventions, plugin auto-discovery, peer-dependency contracts) is found by running the suite, which is why step 5 is not optional.
-
Security context. Run the ecosystem audit (npm audit / pip-audit) before and after — an upgrade motivated by a CVE must show the advisory cleared; an unrelated upgrade must not introduce new HIGH/CRITICAL advisories via transitive bumps.
-
Prove it. Full test suite + typecheck + lint on the bumped lockfile. If checking-coverage-before-change artifacts exist, also run the coverage-map oracle tests for the files in the usage surface. A green suite on an uncovered usage surface proves little — say so in the report rather than claiming safety you don't have.
-
Required code changes go in a follow-up commit. When a major bump forces call-site rewrites: first commit = version bump + the minimal mechanical adaptations to compile/import (suite green), labeled as the upgrade; subsequent commits = any refactoring or improvement the new version enables. If the mechanical adaptations are themselves large, do the inverse — ship compatibility shims first, then the bump, then remove the shims. Canary a large mechanical rewrite first: when the usage surface from step 3 is more than ~10 files, apply the mechanical adaptation to a small trial batch (3 files, or the smallest coherent subset) and prove it with a typecheck + that batch's own tests before extending the same edit to the rest of the surface — a bad trial batch is cheap to diagnose, a bad full-surface rewrite is not.
-
Stuck between versions? When the target version needs changes you cannot safely make yet (an unpinnable usage surface), pin the current version explicitly with a dated comment stating what blocks the upgrade, and surface it in the report — an undocumented stale pin is how 3-year-old CVEs happen.