upgrade-deps
Upgrade a Node repo's dependencies in three rounds (patch → minor → major) using npm-check-updates, respecting the project's release-age cooldown. Researches each update's changelog in parallel, bumps
SKILL.mdskills/upgrade-deps/SKILL.md
GitHubUpgrade deps
Get a repo's dependencies current with the least possible human attention. The user's attention is the scarce resource: spend it only on packages that genuinely need a decision. Everything that Just Works™ gets bumped, tested, committed, and mentioned in a single line at the end.
The user can override the plan at any point ("skip majors today", "leave
zod alone", "just try them all"). Their instruction beats this document.
Setup (once per run)
- Clean tree required. If
git status --porcelainshows changes, stop and ask. Never stash or discard anything. - Package manager. Detect from the lockfile (
pnpm-lock.yaml,yarn.lock,package-lock.json,bun.lock*). Pass it to ncu as-p <pm>, and use it for installs. - Monorepo? If the root
package.jsonhasworkspaces, or there's apnpm-workspace.yaml, add--workspaces(root stays included by default). Bump a package to the same version everywhere it appears. - Cooldown. Only if the project configures one:
- pnpm:
minimumReleaseAge(minutes) inpnpm-workspace.yaml→--cooldown <N>m - yarn:
npmMinimalAgeGatein.yarnrc.yml→ convert to ncu's units (d/h/m) - npm/bun: check
.npmrc/bunfig.tomlfor a release-age setting If none is configured, use no cooldown. If a setting exists but its unit is ambiguous, ask rather than guess.
- pnpm:
- Verification command(s). From the root
package.jsonscripts (viaturbo runif the repo uses turbo):testandtest:types, whichever exist. No build, no lint. If the repo uses syncpack (dependency or config file present), addsyncpack lint(or whatever syncpack script the repo defines). - Baseline. Run verification once before touching anything. Record pre-existing failures so they're never blamed on an upgrade. If the baseline is red, tell the user in one line and ask whether to proceed.
Keep a session-local reject list: packages the user decided not to
upgrade. Pass them as --reject to every later ncu call so they don't
resurface.
A round
Rounds run in order: -t patch, then -t minor, then -t latest (majors).
Each round:
1. List
npx npm-check-updates -t <target> -p <pm> [--workspaces] [--cooldown X] [--reject a,b] --jsonUpgraded
Nothing to upgrade → say so in one line, move to the next round.
2. Research (parallel)
First grep the repo for each package's usage (imports, config files, CLI invocations in scripts) so research is grounded in what we use.
Then spawn one subagent per package (general-purpose), all in a single message. Each gets: package name, from → to, and our usage sites. Each must:
- Read the changelog for every version in the range: GitHub releases
(
gh release view),CHANGELOG.mdin the repo (npm view <pkg> repository.url), or the npm page. - Only if the changelog is vague or missing and our usage is non-trivial:
inspect
npm diff --diff=<pkg>@<from> --diff=<pkg>@<to>. - Return exactly:
verdict:trivialorattentionrisk(if attention), meaning how much harm it can do to the product if it's shipped or ignored:critical: security fix we're exposed to, or possible data loss/corruptionhigh: silent behavior change in code we use, or breaks runtime codemedium: breaks tests/types/tooling (loud, so caught before it ships), or needs a decisionlow: cosmetic, opt-in, or tooling-only consistency issues
reason(if attention): one line, citing our usage site if relevant
trivial = no change touches API surface we use, or only additive/fix
changes. Anything ambiguous about code we use is attention. Never pad a
trivial verdict with an explanation.
3. Bump the trivial ones
Upgrade all trivial packages together (ncu -u --filter a,b,c ...), install,
verify.
- Green → commit via
gcm --yolo "<round>-level dependency upgrades". - Red → bisect: revert half the batch (restore
package.jsonfiles + lockfile fromHEAD, re-apply the other half, reinstall), verify, narrow down until the culprit(s) are isolated. Commit the green remainder. Culprits move to the attention list, with their risk re-assessed in light of what broke.
4. Report
Sort attention items by risk, then by how much of our code they touch. Output, and nothing else:
Patch round: 3 need you.
| name | from | to | reason | risk |
|-------------|---------|---------|-------------------------------------------------------------|--------|
| zod | 3.23.8 | 3.23.11 | `z.string().email()` regex tightened; we validate signup | high |
| | | | emails with it (src/auth/schema.ts:14). Tests pass, but | |
| | | | existing users with odd emails could fail re-validation | |
| vitest | 2.1.3 | 2.1.5 | bumped → 4 tests in packages/core fail (fake timers) | medium |
| @types/node | 22.7.4 | 22.7.9 | syncpack mismatch: apps/web pins 22.7.4 exactly | low |
Pick one (default: zod), or "all" to batch-try + bisect.
Upgraded patch-level: react, react-dom, tsup, prettier, eslint-plugin-react, typescript-eslint (+9 more)
Rules:
- At most ~8 table rows (half a terminal screen). If more items exist,
the last row is
| …and msw, undici, hono (3 more) | | | next message | |. - Reasons are one line where possible; wrap only when the reason truly needs it. No mention of trivial packages beyond the final line.
- The final line lists everything committed this round. If it'd exceed one
line, name the first few and
(+N more). - Zero attention items → just the final line, then continue to the next round. (Still pause before majors if the user said so.)
5. Iterate
Work through items with the user, one at a time, their pick. Typical moves:
- They ask questions → answer tersely, from changelog/diff evidence.
- They say "try it" → bump, verify, report the result in a line or two.
- They say "all" → bump every attention item, verify, bisect on red, report only what broke.
- A fix on our side is needed → make it, verify, commit the upgrade + fix
together via
gcm --yolo "<pkg> <from> → <to>". - They decline → add to the reject list, move on.
Formatter upgrades (any formatter: prettier, biome, dprint, oxfmt, prettier/biome plugins, …): never ask, always do this as three separate commits:
- the version bump
- the reformat the new version causes in files that were already formatted
- a reformat of files that already failed the format check before the bump
Skip any step that has no changes. Mention commits 2 and 3 in a line each only if something in them deserves a look (e.g. a rewrap inside a code block or formula). Otherwise they go on the final line like any other upgrade.
Each green upgrade is committed as it lands. Pause only when something needs the user. When the attention list is empty, the round is done → next round.
Ground rules
- Never push. Never rewrite history. Commits only via
gcm --yolo. - Never leave the tree half-upgraded when pausing for the user: either
committed or reverted to
HEAD. - Never claim a package is safe without having read its changelog (or diff).
No changelog found and non-trivial usage →
attention, reason "no changelog". - Majors are almost never
trivialunless our usage is clearly unaffected by every listed breaking change.