Test a major version upgrade by splitting it into small steps that each ship and pass tests on their own: first adopt the latest release of the current major, then fix deprecation warnings, then hide the library behind an adapter, then switch the version, then roll out in stages. One giant pull request is the riskiest way to do it.
Why majors are different
Semantic Versioning says a major version increments when incompatible API changes are made. Unlike patch and minor releases, the maintainer is allowed to remove or change things your code uses. That is why the same checklist that works for a patch bump is too thin here.
The sequence
- Read the migration notes first. List removed APIs, changed defaults and new minimum runtime versions. Search your code for each.
- Upgrade to the newest release of your current major. Maintainers often add deprecation warnings and bridge APIs there. This step is usually a minor bump with low risk.
- Turn warnings into failures in CI. Fix deprecations one module at a time, shipping each fix. These changes work on both the old and the new major where possible.
- Add an adapter. Route calls to the library through a thin wrapper of your own, so the change is concentrated in one place. This is the first stage of branch by abstraction, which Fowler describes as making a large change gradually while still releasing regularly.
- Write characterization tests for the wrapper. See characterization tests before upgrading legacy code.
- Switch the version in a branch or flagged build. Run the full suite. Fix only what breaks.
- Smoke test, then stage the rollout. Compare the new build with a control.
- Remove shims. Delete the adapter layers and temporary flags you added once everything runs on the new major.
Scope the risk with a table
| Change type in the new major | Where to look | Test |
|---|---|---|
| Removed or renamed API | Compiler errors, search | Unit tests |
| Changed default value | Config, docs | Integration tests with explicit values |
| New minimum runtime | Build files, base image | Smoke test on the new runtime |
| Behavior change without API change | Release notes | Characterization tests |
| Dropped platform support | Install and CI matrices | CI on each supported target |
The hardest items are the behavior changes with no compile error. They are why the characterization and integration tests matter more than the compiler.
Handling a blocked upgrade
Sometimes a dependency of yours holds you on the old major. Options include upgrading the blocker first, using a package manager override for a tested range, carrying a patch, or replacing the blocker. Each carries cost and should be recorded with a review date. For the vocabulary, see bump, pin, override, replace, fork and remove defined.
Estimating the work
Use your own history. If the last two majors of a similar library took three weeks each, plan for that and not for a day. See estimate a dependency upgrade from your own past upgrades.
When a security fix forces the issue
If the sole fixed version is on a new major, the steps are the same but the clock is shorter. Consider a temporary mitigation, such as disabling the vulnerable feature or blocking the input at the edge, while you finish the steps. Safeguard's breaking-change-aware remediation approach is covered in breaking change aware remediation, and its fix pull requests are off by default with every category on Manual.
Quick check
Should a major upgrade be one pull request?
Rarely. Small prerequisite pull requests that work on both versions reduce risk and make the final switch small enough to review.
What if the new major has no migration guide?
Treat that as a risk. Compare the public API between versions, read the changelog, and lean harder on characterization tests and a slow rollout.
Sources
Last reviewed: 19 October 2026.