Dependency updates: keeping packages current without breaking things

A sustainable routine for updating NuGet and npm packages: lockfiles, automated pull requests, vulnerability scanning, a tested pipeline and abandoned packages.

A young developer in a green jumper talks through something at his monitor with a grey-haired colleague in a navy cardigan, plants lining the windowsill of a bright charity office behind them

A typical business application of any age pulls in hundreds of packages, most of them indirectly. You chose a dozen. Those dozen chose the rest. Nobody on the team could list them all, and a good proportion were last updated when the application was first built.

Old packages are a common way into a system. Once a weakness in a library is published, anyone can look for applications still using the old version, without needing to find a flaw in your own code. It is also the usual way an upgrade turns into a project: leave everything alone for three years and the day the platform goes out of support you face every breaking change at once.

The alternative is dull and effective: small, frequent, tested updates. This walkthrough covers a routine a small team can sustain, using .NET (NuGet) and JavaScript (npm) as examples. Tool details are as of November 2026.

Start by being able to see and repeat your dependencies

Before automating anything, make the current state reproducible.

  • Lockfiles. npm records exact resolved versions in package-lock.json, and builds in your pipeline should install from it (npm ci) so that every build gets the same packages. NuGet supports an equivalent through lock files, if you enable them.
  • Know your direct dependencies. Keep the list you chose short and deliberate. In .NET solutions with several projects, central package management lets you set versions in one file, so a project cannot quietly drift onto a different version of the same package.
  • Pin, but not forever. Pinning gives predictability. It becomes a liability only if nobody ever moves the pin, which is why the rest of this article is about moving it regularly.

Let a tool propose the updates

Do not rely on someone remembering. Automated update tools watch your repository and open a pull request when a newer version exists, so an update becomes a small piece of review work rather than a project.

  • Dependabot is built into GitHub and supports NuGet and npm among others.
  • Renovate is an open-source alternative, either self-hosted or through the Mend-hosted app, and works across several code hosts.

Both can group updates and run on a schedule. A starting configuration for Dependabot, illustrative and untested against your repository, might look like this:

# .github/dependabot.yml (illustrative)
version: 2
updates:
  - package-ecosystem: "nuget"
    directory: "/"
    schedule:
      interval: "monthly"
    groups:
      nuget-updates:
        patterns:
          - "*"
        update-types:
          - "minor"
          - "patch"
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "monthly"
    groups:
      npm-updates:
        patterns:
          - "*"
        update-types:
          - "minor"
          - "patch"

Grouping matters more than it looks. One pull request a month with fifteen small updates is reviewable. Fifteen separate pull requests, each triggering a build and a review, is how teams learn to ignore the bot. Without update-types, a group would sweep major versions in too; limited to minor and patch as here, each major update still arrives as its own pull request.

Scan for known vulnerabilities in the pipeline

Updating on a monthly schedule is not fast enough for a serious vulnerability, so scan on every build too. Both ecosystems have this built in.

NuGet. Since the .NET 8 SDK, NuGet audit is on by default and reports known vulnerabilities as build warnings (NU1901 to NU1904, from low to critical severity). Projects targeting net10.0 or later audit transitive packages by default as well; earlier targets audit only direct references unless you set NuGetAuditMode to all. To make the serious ones stop the build:

<PropertyGroup>
  <NuGetAuditMode>all</NuGetAuditMode>
  <WarningsAsErrors>$(WarningsAsErrors);NU1903;NU1904</WarningsAsErrors>
</PropertyGroup>

You can also check by hand with dotnet list package --vulnerable --include-transitive.

npm. npm audit reports known vulnerabilities, and its --audit-level flag sets the severity at which it returns a failing exit code:

npm audit --audit-level=high

Treat these as a safety net rather than a guarantee. They only know about published advisories, and they will sometimes flag something that does not affect how you use the library. Someone still needs to read the result.

A tested pipeline is what makes updates safe

An update tool without tests simply produces pull requests nobody trusts. The thing that lets you merge a package update on a Tuesday afternoon is a pipeline that builds, runs the automated tests and tells you within minutes whether anything broke.

If the application has few or no tests, that is the first job, and it is more tractable than it sounds: characterisation tests record what the code does today so you can change things underneath it with a safety net. Start with the parts that touch the packages you update most often, such as data access, serialisation and authentication.

Add a deployment to a test environment as a final gate, and keep the ability to roll back. Most update failures are caught by tests. The rest show up on a test environment, which is why you have one.

Cadence, grouping and who approves

Settle three things so the routine does not depend on goodwill:

  • Cadence. Monthly for routine updates, immediately for a critical advisory that affects you.
  • Grouping. Patch and minor updates together, major version updates on their own, because a major version is where breaking changes live.
  • Approval. One named person reviews and merges, with a second reader for anything touching authentication, payments or personal data. If an update fails the pipeline, it is not merged until someone has looked at why.

Our software care work follows the same pattern: updates go through the tested pipeline, and a named engineer approves anything before it reaches a live system.

When a package is abandoned

Sooner or later a package stops being maintained. Signs include no release for a couple of years, unanswered security issues and an archived repository. A package that is stable and small may be fine as it is. One that handles authentication, parsing untrusted input or cryptography is not.

Your options, roughly in order of effort: switch to a maintained alternative; fork it and own the maintenance, which is a real commitment; or replace the small part you use with your own code. Whichever you choose, write down the decision and the date you will look at it again.

The same logic applies to the framework itself. Moving regularly is far cheaper than a single jump at the end, as the .NET 8 to .NET 10 upgrade guide shows for teams that have left it late.

A sensible first step

Start by running the vulnerability check on your most important application (dotnet list package --vulnerable --include-transitive, or npm audit) and keep the output. It takes minutes, and it gives you an honest starting point. If the list is long, do not try to fix it all at once. Fix the critical and high items, then schedule the monthly routine for the rest.

Getting dependencies, tests and a repeatable build in place is part of onboarding a system into our software care service, and our software and integration team can help with the engineering behind it.