Skip to content

Upgrade Planning

How to turn an outdated-dependencies report into an ordered, risk-batched upgrade plan. Used by dependency-upgrade Phases 1–2.

Score each candidate bump on two axes:

DistanceNominal meaningReal-world caution
Patch (1.2.3 → 1.2.4)Bug fixes onlyUsually safe in batches — but a patch can still change behavior your code accidentally depended on
Minor (1.2.x → 1.5.0)Additive featuresSafe-ish in batches; watch for deprecation warnings that foreshadow the next major
Major (1.x → 2.0)Breaking changesAlways its own stage; read the migration guide first
0.x anythingNo stability contractTreat every 0.x bump as a potential major
CategoryExamplesHandling
Framework / runtimeReact, Django, Rails, the language toolchainOwn stage, first in order, migration guide mandatory
Build & test toolchainBundler, compiler, test runner, linterOwn stage; failures here masquerade as app failures
Widely-imported libraryHTTP client, ORM, date libraryOwn stage for majors; grep the codebase for its API surface
Leaf utilityA formatter used in two filesBatchable, even across majors if the changelog is trivial
Type/lint-only@types/*, lint pluginsBatchable; failures are compile-time and obvious
  1. All-patch/minor leaf updates → one batch, one commit, one gate run.
  2. Each major → its own stage.
  3. Each framework/toolchain bump (any distance) → its own stage.
  4. A package and its plugin family (e.g. a framework and its official adapters) move together in one stage — they are one compatibility surface.
  5. Cap batch size at what you can attribute a failure to; ~10 trivial bumps is fine, 40 is an unreviewable diff.

Order stages so each one lands on a foundation that already supports it:

  1. Language / runtime version (if in scope)
  2. Build and test toolchain
  3. Framework majors, with their plugin families
  4. Widely-imported library majors
  5. The batched leaf/minor/patch remainder
  6. Type/lint-only cleanup

If a bump requires another bump (peer-dependency constraints), the dependency graph — not this list — wins; note the forced coupling in the plan.

For each staged bump, crossing versions a.b.c → x.y.z:

  • Read the notes for every release between the two, not just x.y.z. In monorepos, read the specific package’s changelog, not the umbrella one.
  • Extract three lists: breaking changes (must act), deprecations (act now or note for next time), behavior changes (verify the affected paths).
  • Map each breaking change to concrete call sites with a codebase search before bumping. An empty result set is evidence of safety; a long one might justify deferring the bump into its own plan.
  • Prefer the project’s official migration guide and codemods over manual edits — they encode edge cases the changelog omits.
  • No changelog at all? That is itself a risk signal — inspect the source diff between tags, or defer the bump.
  1. Read the failure against the changelog’s breaking-change list. A named, expected breakage → fix forward mechanically.
  2. An unexplained failure → revert the stage cleanly and investigate separately. Debugging forward on an unverified bump conflates two unknowns.
  3. A failure revealing the app depended on undocumented behavior → the fix may be genuine debug-and-fix work; the bump waits until the app is correct.
  4. Record deferred bumps and their blockers in the report — an upgrade plan whose failures evaporate silently will re-litigate them next quarter.