Dependency Upgrade Runbook
A five-phase runbook for upgrading project dependencies safely, covering preparation, the upgrade itself, validation, rollback, and post-deploy monitoring.
Overview
Outdated dependencies expose projects to security vulnerabilities, compatibility bugs, and capabilities they’re missing. This runbook gives you a repeatable, five-phase process for upgrading them with minimal risk. The same checklist works for a single CVE patch or a quarterly maintenance window.
When to Use
- A critical security patch lands for a direct or transitive dependency and you need it in production today, not next sprint.
- A major version upgrade is needed for long-term support or compatibility with the rest of your stack.
- You run scheduled dependency refreshes each sprint or quarter and want the same procedure every time.
- A junior engineer is running their first upgrade and needs the checklist, not tribal knowledge.
The Runbook process at a glance
The Runbook
# Dependency Upgrade Runbook
## 1. Preparation
- [ ] Identify the dependency and target version
- [ ] Review the changelog / release notes for breaking changes
- [ ] Check open issues in the dependency repository for upgrade-related bugs
- [ ] Create a dedicated branch: `deps/upgrade-<name>-<version>`
- [ ] Ensure CI is green on the current main branch
## 2. Upgrade
- [ ] Update the version in `package.json`, `requirements.txt`, `pom.xml`, etc.
- [ ] Run the dependency installation command
- [ ] Check for peer dependency warnings or conflicts
- [ ] Run automated tests (unit, integration, lint)
- [ ] Run smoke tests against a local or staging environment
## 3. Validation
- [ ] Review test coverage reports for regressions
- [ ] Check application logs for new warnings or errors
- [ ] Verify critical user paths manually if behavior changed
- [ ] Run security scan (`npm audit`, `safety check`, OWASP dependency check)
## 4. Rollback Plan
- [ ] Tag the last known good commit before merge
- [ ] Document any manual data or config changes required
- [ ] Confirm rollback can be executed within 15 minutes
## 5. Merge & Monitor
- [ ] Open a pull request with changelog summary
- [ ] Deploy to staging and let it soak for 24 hours
- [ ] Deploy to production during low-traffic window
- [ ] Monitor error rates and latency for 48 hours post-deploy
How the Five Phases Work
The runbook splits upgrades into phases that each catch a different failure mode. Preparation kills surprises: most upgrade incidents trace back to a changelog nobody read. The upgrade itself stays isolated in a branch so the diff is reviewable. Validation mixes automated checks with a manual pass on whatever behavior actually changed. The rollback plan exists before you need it. “Revert fast” is a decision made while calm, not during an incident. Merge and monitor closes the loop: staging soak plus 48 hours of watching error rates catches the regressions tests miss.
Each phase also has a cost if you skip it. Skip preparation and you discover breaking changes in production. Skip the isolated branch and the upgrade tangos with whatever else was in flight that week. Skip validation and “tests pass” quietly covers the one path nobody exercised. Skip the rollback plan and the revert becomes an archaeology project. Skip monitoring and you learn about the regression from a user report instead of a dashboard.
Choosing the Target Version
“Latest” isn’t always the right target. Pick the version by intent:
- Security fix. Take the smallest release that patches the CVE, even if it means a backport branch like
2.4.xinstead of3.x. Big jumps under incident pressure multiply risk. - Routine maintenance. Latest stable is fine, but prefer a version with a few weeks in the wild over a release published yesterday. Let someone else find the
.0bugs. - Major migration. Target the newest LTS line, not the bleeding edge. For frameworks, check the official support window first: an upgrade that lands you on a version going end-of-life in three months buys you a second emergency.
Two checks before committing to a number. First, runtime requirements: many majors quietly raise the minimum Node, Python, or JVM version, and that upgrade turns into two upgrades. Second, the ecosystem lag: type definitions, plugins, and wrappers (@types/*, webpack plugins, ORM integrations) often trail a major release by weeks. If the wrappers you use haven’t caught up, waiting is cheaper than patching around them.
Upgrade Commands by Ecosystem
The five phases are the same everywhere; only the commands change. A quick map for the common toolchains:
| Ecosystem | See what’s outdated | Upgrade one package | Lock file | Vulnerability audit |
|---|---|---|---|---|
| npm | npm outdated | npm install pkg@x.y.z | package-lock.json | npm audit |
| yarn / pnpm | yarn outdated / pnpm outdated | yarn up pkg / pnpm update pkg | yarn.lock / pnpm-lock.yaml | yarn npm audit / pnpm audit |
| pip + pip-tools | pip list --outdated | pip-compile --upgrade-package pkg | requirements.txt | pip-audit |
| poetry | poetry show --outdated | poetry update pkg | poetry.lock | pip-audit on exported reqs |
| Maven | mvn versions:display-dependency-updates | mvn versions:use-latest-versions | (versions in pom.xml) | dependency-check (OWASP) |
| Go modules | go list -u -m all | go get -u pkg@vX.Y.Z | go.sum | govulncheck ./... |
| Cargo | cargo outdated | cargo update -p pkg | Cargo.lock | cargo audit |
| Bundler | bundle outdated | bundle update gem | Gemfile.lock | bundler-audit |
Whatever the ecosystem, the workflow in phase 2 is identical: bump the manifest, regenerate the lock file, run the tests, then read the lock diff before committing. The lock diff is where surprise transitive upgrades show up.
Variants
| Context | Approach | Notes |
|---|---|---|
| Security patch | Fast-track branch | Skip soak time only for CVEs with active exploits |
| Major version | Feature-flag rollout | Isolate new behavior behind flags during transition |
| Monorepo | Batch upgrades | Upgrade shared libraries first, then consumers |
| Transitive-only CVE | Force-pin via overrides / resolutions | Document why the pin exists and when to remove it |
Best Practices
- Upgrade one major dependency at a time. Two majors in one PR makes failures impossible to attribute.
- Pin exact versions and commit lock files.
package-lock.jsonandpoetry.lockmake builds reproducible and diffs reviewable. - Use
npm ciin CI, notnpm install. It installs exactly from the lock file and fails when the file drifts frompackage.json:
# GitHub Actions
- name: Install dependencies
run: npm ci
- Automate the boring 90%. Dependabot or Renovate can auto-merge patch and minor bumps; keep humans for majors:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
groups:
patch-and-minor:
update-types: ["patch", "minor"]
- Keep a deprecation calendar. Track end-of-life dates for frameworks and runtimes so upgrades are planned, not forced by a CVE.
Common Mistakes
- Upgrading several majors at once. When tests fail, you can’t tell which bump broke things.
- Ignoring peer dependency warnings. They become runtime errors in production.
- Skipping the rollback plan. Improvising a revert under pressure extends downtime.
- Not reviewing transitive changes.
git diff package-lock.jsonoften shows surprise updates that deserve scrutiny. - Upgrading devDependencies without a full build. Webpack, babel, or eslint can break the production bundle even when tests pass:
npm run build && npm run test
- Dismissing deprecation warnings. Warnings in one minor version become errors in the next major. Capture and track them:
npm test 2>&1 | grep -i "deprecat" > deprecation-warnings.txt
A Worked Example: Upgrading React 18 to 19
A concrete pass through the runbook makes the phases tangible. Say the upgrade is React 18 to 19 on an app with a week of runway.
Preparation. The React 19 changelog calls out removed APIs (defaultProps on function components, string refs) and changed behavior around useEffect cleanup timing and ref as a prop. The project’s issue tracker flags a known problem with a popular form library on 19.0.0, fixed in 19.0.2, so the target is 19.0.2, not 19.0.0. The app’s Node version already meets the requirement. Branch: deps/upgrade-react-19.
Upgrade. Bump react and react-dom together (they ship in lockstep), plus @types/react and @types/react-dom. Run the official codemod for ref as a prop. npm install reports a peer warning from an old version of a charting library that caps React at 18, so that library gets upgraded in the same branch because a peer conflict is a failure in phase 2, not a separate project.
Validation. Unit and integration suites pass. The manual pass exercises the three paths the changelog marked as risky: a form with uncontrolled inputs, a modal using createPortal, and a component that relied on defaultProps. The codemod rewrote the last one automatically.
Rollback. The pre-upgrade tag exists; the rollback is git checkout <tag> -- package.json package-lock.json && npm ci. Tested once on a throwaway branch: four minutes.
Merge & monitor. PR links the changelog summary and lists the codemod diff separately so reviewers can skim generated changes. Staging soaks a day, production deploys Tuesday morning, error rate and INP stay flat through the 48-hour window.
Handling Breaking Changes
Breaking changes are where upgrades go to die. The defense is boring but effective: read the migration guide before touching the manifest, not after the build breaks. Most mature projects ship one (MIGRATION.md, upgrade docs, or release notes with a “Breaking” section). If the project only publishes a raw changelog, look for renamed APIs, dropped options, changed defaults, and removed deprecations you were relying on.
For majors, the mechanical work often has tooling:
- Codemods: many ecosystems ship AST transforms:
npx @org/codemod, Angular’sng update, React’s official codemods, or OpenRewrite recipes for Java. Run them on a clean branch and review the diff like any other code change. - Adapter layers: when you call the old API in forty places, wrap it. Write a thin module matching the old surface that delegates to the new API, migrate call sites behind it, then drop the adapter. This converts one risky flag-day into many small diffs.
- Dual-running: for infrastructure-level deps (HTTP clients, serializers, ORM), run old and new side by side behind a flag when the library allows it, and cut over per environment.
A warning about test coverage: if your suite is thin, an upgrade is the wrong time to discover it. Before the major bump, add contract tests around the code paths that touch the dependency: serialization boundaries, retry behavior, error shapes. Those tests are the difference between “tests pass” and “it works”.
Coordinating Upgrades Across Repos
Inside one repo the runbook is enough. Across repos, like a shared library plus its consumers, the ordering matters more than the mechanics:
- Upgrade the producer first. Publish the new shared-library version to a registry (or a prerelease tag) before touching consumers.
- Declare a compatibility window. If the library can support both old and new consumers for a release or two, say so explicitly. It decouples team schedules and removes the pressure for a coordinated flag-day.
- Migrate a canary consumer. Pick the lowest-risk service, run the full five-phase runbook on it, and let it soak in production. Its failure modes teach you what the other twenty consumers will hit.
- Batch the followers. Once the canary is clean, upgrade the remaining consumers in one maintenance window per team, not one per service.
Contract tests between producer and consumers catch the subtle breaks (a changed error shape, a reordered enum) that unit tests in either repo can’t see.
Advanced: Automating Upgrades
Renovate configuration with safe auto-merge
{
"extends": ["config:base"],
"schedule": ["before 6am on Monday"],
"packageRules": [
{
"updateTypes": ["patch", "minor"],
"automerge": true,
"groupName": "patch and minor updates"
},
{
"updateTypes": ["major"],
"automerge": false,
"labels": ["major-upgrade", "needs-review"],
"dependencyDashboardApproval": true
}
],
"vulnerabilityAlerts": {
"enabled": true,
"labels": ["security"],
"schedule": ["at any time"]
}
}
Rollback script for npm upgrades
#!/bin/bash
set -euo pipefail
BRANCH=$(git rev-parse --abbrev-ref HEAD)
TAG="pre-upgrade-$(date +%Y%m%d-%H%M%S)"
git tag "$TAG"
echo "Created safety tag: $TAG"
# If the upgrade fails, restore manifests from the tag:
# git checkout "$TAG" -- package.json package-lock.json && npm ci
echo "To rollback: git checkout $TAG -- package.json package-lock.json && npm ci"
Pinned Python upgrades with pip-tools
#!/bin/bash
set -euo pipefail
# requirements.in holds loose constraints; requirements.txt is the locked output
echo "package-name==2.0.0" >> requirements.in
pip-compile --upgrade-package package-name --output-file requirements.txt requirements.in
pip install -r requirements.txt --dry-run # catch conflicting transitive deps
pytest tests/ -x
git add requirements.in requirements.txt
git commit -m "deps: upgrade package-name to 2.0.0"
Audit report and SBOM for compliance
#!/bin/bash
set -euo pipefail
npx @cyclonedx/cyclonedx-npm --output-file sbom.json
npm audit --json > audit-report.json
node -e "
const audit = require('./audit-report.json');
const vulns = audit.vulnerabilities || {};
const high = Object.entries(vulns).filter(([,v]) => v.severity === 'high' || v.severity === 'critical');
if (high.length > 0) {
high.forEach(([name, info]) => console.log(name + ': ' + info.severity));
process.exit(1);
}
"
When NOT to Upgrade
A runbook is also permission to wait. Legitimate reasons to defer:
- A release freeze is days away. Upgrading right before a code freeze puts the riskiest change into the least-tested window.
- The dependency is scheduled for removal. If you’re migrating off the library next quarter, patching it today is work you’ll throw away; document the exposure and monitor instead.
- The upgrade forces a bigger one. A package whose new version demands a newer runtime or framework converts a small task into a project. Schedule the project; don’t smuggle it into maintenance.
- Nothing is broken and nothing is exposed. A dependency pinned to an old-but-patched version, with no CVEs and no capabilities the project misses, can wait for the next cycle. Deferring on purpose is a decision; forgetting isn’t.
Whatever the reason, write it down next to the dependency. An undocumented “we’ll do it later” is how dependencies end up three majors behind.
Metrics Worth Tracking
You can’t improve a process you don’t measure. Four numbers tell you whether upgrades are working:
- Upgrade cadence: time between dependency refreshes. If patches sit for months, “routine” upgrades turn into risky jumps.
- Mean time to patch (MTTP): how long a published CVE takes to reach production. For critical CVEs with active exploits, measure it in days, not weeks.
- Freshness: percentage of direct dependencies within one major of latest. Watch the trend, not the absolute number.
- Failed-upgrade rate: how often a bump gets rolled back. A rising rate usually means phases 1 and 3 are being skipped, not that upgrades are risky by nature.
Track them in whatever you already use for engineering metrics; a spreadsheet works fine. The point is spotting the drift before a CVE forces the conversation.
Further Reading
- Runbook Template — the generic runbook structure this one specializes.
- Changelog Template — document what each upgrade changes for your own consumers.
- On-Call Runbook Template — for when the upgrade goes wrong at 3 a.m.
Frequently Asked Questions
How often should I upgrade dependencies?
Patch versions weekly or automatically via Dependabot/Renovate. Minor versions monthly. Major versions quarterly, or sooner when they gate security fixes or LTS support. The worst cadence is "when something breaks"; it turns every upgrade into an emergency.
What if a transitive dependency has a CVE?
You don't have to wait for the direct dependency to release a fix. Use npm audit fix, overrides in package.json, pip-audit for Python, or Maven dependency management to force a patched transitive version.
Should I commit lock files?
Always. Lock files give you reproducible builds across machines and CI, and they turn dependency upgrades into reviewable diffs. npm ci, pip install -r requirements.txt, and similar commands all assume a committed lock file.
How do I handle an unmaintained dependency?
Evaluate maintained alternatives first: a migration is usually cheaper than carrying a dead dependency. If nothing fits, fork it (license permitting) or vendor it into your repo, then add it to the deprecation calendar with a replacement deadline. Scan the last published version so you at least know your exposure.
What's the difference between ~ and ^ in semver?
^1.2.3 allows anything that doesn't change the leftmost non-zero digit (so 1.x.x), while ~1.2.3 allows patch-level only (1.2.x). Caret is fine for most deps; use tilde or exact pins for packages whose stability you depend on.
When does a monorepo need special tooling?
Nx, Turborepo, or Lerna earn their complexity when packages share dependencies and upgrades must land together. They add workspace hoisting, caching, and batch upgrade commands. For a handful of packages, a single package.json and normal tooling is simpler and just as safe.
How do I test an upgrade when test coverage is thin?
Lean on the environment, not the suite. Deploy to staging first and run a scripted smoke pass over the critical user paths: login, the top three API calls, the one batch job that matters. If you can, do a canary deploy to a small slice of production traffic and watch error rates for a few hours before full rollout. And capture the baseline before the upgrade: you can't spot a 2% latency regression without knowing last week's number.
Should I upgrade to a new major version right away?
Usually not. Unless it patches a CVE you're exposed to, give a fresh major a few weeks: the .0 release collects the real-world bug reports, and the .1 or .2 that follows is where most teams should land. Use the waiting time to watch the issue tracker for upgrade-specific reports and to let @types, plugins, and wrappers catch up.
Related Resources
Runbook Template
A reusable template for operational runbooks: incident response, deployment procedures, and routine tasks.
DocChangelog Template
A structured changelog template following Keep a Changelog conventions for tracking project releases.
DocOn-Call Runbook Template
A template documenting common alerts and step-by-step response procedures for on-call engineers.
DocSecurity Incident Response Template
A template for security incident response covering detection, classification, containment, eradication, recovery, communication, and post-incident review.
DocBug Report Template (Markdown + Examples)
A structured bug report template to help teams reproduce, triage, and resolve defects faster — with a filled example and a GitHub issue form variant.
DocCapacity Planning Template
A reusable template for planning system capacity, estimating growth, and preventing performance bottlenecks before they happen.