@fub4r/nuup
v0.2.0
Published
Upgrade the NuGet packages a repository references, by editing the files that declare them — no restore, no build
Maintainers
Readme
nuup
Upgrade the NuGet packages a repository references, by editing the files that declare them. No restore, no build, no project evaluation.
nu(get) up(grade).
$ nuup -f "MyCompany.*"
src/Api/Api.csproj
MyCompany.Core ........................... 1.4.0 -> 1.6.2
MyCompany.Messaging ...................... 2.0.1 -> 2.3.0
src/Web/Web.csproj
MyCompany.Messaging ...................... 2.0.1 -> 2.3.0
nuup: 2 to upgrade, 7 up to date, 1 not a plain version
nuup: nothing was written; pass --write to applyWhy this exists
dotnet outdated needs the project to restore. Across an estate that is all-or-nothing: one unreachable feed, one pinned SDK, one unsupported target framework, and the whole repository reports nothing — including the packages that would have resolved perfectly.
$ dotnet outdated # one private feed is down
error NU1301: Unable to load the service index for source https://nuget.corp.example/v3/index.json
Failed to restore App.csproj — exit code: 1
$ nuup # same repository, same moment
App.csproj: Serilog 3.1.1 -> 3.4.0
nuup: 1 to upgrade, 1 could not be checked
nuup: 1 package was not checked because a source failed; it is not known to be up to datenuup reads the project files directly and asks the feeds which versions exist. A repository that will never restore on your machine is still answerable.
It is not Renovate or Dependabot. Those are the right answer for continuous, per-repository, bot-driven updates. This is for the other thing: a campaign across an estate you have just cloned, where you want to build and verify before anything leaves your machine.
Requirements
- The .NET SDK, version 9 or newer —
nuupasksdotnet package searchwhich versions exist git, so a repository's ignore rules can be honoured- Node.js 22 or newer
Install
npm install -g @fub4r/nuupThe whole parameter surface
nuup
-f, --filter <glob> (repeatable)
-vl, --version-lock <major|minor|none|"<6.0.0"|"<=5.9.9">
--prerelease
-w, --write
-j, --json
-h, --helpnuup edits files and stops. It never restores, builds, commits, pushes or opens a pull request — verifying the change is yours.
Upgrades are printed as a table, grouped under the projects that reference each package, with the versions aligned so the new column reads straight down.
--filter <glob>
Package names, anchored and case-insensitive: MyCompany.* matches MyCompany.Core but not Contrib.MyCompany.Core. Repeatable, and repeats are OR-ed:
nuup -f "MyCompany.*" -f "Serilog*"Omitted, every package is considered.
--version-lock <lock>
How far up to go. Two shapes, because there are two ways to say "not past here".
A keyword pins a part of whatever version each project is already on, so it means something different for each of them. On 3.1.1, with 3.1.2, 3.4.0 and 4.4.0 available:
| lock | picks | |
|---|---|---|
| major | 3.4.0 | the major stays; minor and patch move |
| minor | 3.1.2 | major and minor stay; only the patch moves |
| none | 4.4.0 | nothing pinned, major bumps included — the default |
A ceiling names one version and means the same thing everywhere. On 3.1.1, with 4.2.0, 5.0.0, 5.9.1, 6.0.0 and 6.1.0 available:
| lock | picks | |
|---|---|---|
| "<6.0.0" | 5.9.1 | the highest 5.x there is |
| "<5.0.0" | 4.2.0 | the highest 4.x there is |
| "<=5.4.9" | 5.4.9 | up to and including the version named |
A ceiling will cross majors on the way up — 3.1.1 to 5.9.1 is two major bumps — because the ceiling is what you asked for, not the major. That is the point of it: get as current as you can without going into 6.
Quote it.
<is a redirection operator in every shell.nuup -vl <6.0.0never reaches nuup at all — bash reports6.0.0: No such file or directory, and cmd and PowerShell fail their own way. Write-vl "<6.0.0".
A partial version works ("<6" is the same as "<6.0.0"), and a floor is refused: an upgrade never goes below where it started, so ">=5.0.0" would add nothing.
Nothing is locked unless you ask. A lock is a decision somebody makes for a reason, not one to inherit quietly — and an unqualified run that silently withheld major versions would report them as nothing to do.
When that default holds something back, it says so rather than calling it up to date — being on the newest 8.x while 10.x exists is not the same thing:
$ nuup -f "Microsoft.Extensions.*"
nuup: 0 to upgrade, 1 held by the version lock
nuup: Microsoft.Extensions.DependencyInjection could go to 10.0.12, held at 8.0.1 by --version-lock
``` When a lock you asked for holds something back, it says so rather than calling it up to date — being on the newest 8.x while 10.x exists is not the same thing.
There is no `patch`: locking the patch would pin all three parts and permit nothing. Passing it says so and suggests `--version-lock minor`, which is how you ask for patch-only updates.
### `--prerelease`
Consider prerelease versions. A package already **on** a prerelease is offered newer prereleases regardless — somebody chose that, and hiding the newer ones would answer a question they did not ask.
### `--write`
Apply the upgrades. Without it nothing is written: `nuup` prints what it would do and exits.
Only the version text is replaced, at the exact offset it sits at. Formatting, attribute order, comments, encoding and line endings come back byte-identical, so `git diff` shows the versions and nothing else.
## What it will not touch
- **Commented-out references.** A commented-out reference is not a reference.
- **MSBuild properties.** `Version="$(CoreVersion)"` is reported as `not a plain version` and left alone — the literal lives in a property, and rewriting it into a fixed version would change what the file means.
- **Ranges and floating versions.** `[1.0,2.0)` and `1.2.*` have no single meaning for "upgrade", so they are reported rather than guessed at.
- **Anything a repository ignores**, or that sits under `bin`, `obj`, `node_modules`, `.vs`, `packages` or `TestResults`.
## Central package management
A `<PackageReference Include="Serilog" />` under CPM carries no version, so it is not a site and nothing is written there. The `<PackageVersion>` in `Directory.Packages.props` is the site, and that is where the edit lands. `packages.config`, `Directory.Build.props` and `Directory.Build.targets` are all read the same way: `nuup` edits wherever a version is actually written.
**What you are shown is not that file.** Where to write is the tool’s problem; what you want to read is which of your projects is on the old version. So the report groups by the projects that *reference* the package, exactly as `nuls` does — even when the single edit happens somewhere else entirely:
```console
$ nuup -f xunit -vl none
src/Api/CentralOnly.csproj
xunit .................................... 2.4.1 -> 2.9.3
nuup: 1 to upgrade
$ nuup -f xunit -vl none --write && git diff --stat
Directory.Packages.props | 2 +-A version written inside a project speaks only for that project — two projects pinning the same package separately are two independent upgrades. A version in a shared file speaks for every project below it that references the package. One that nothing references at all is shown against the file declaring it, since there is no project to show it under.
Up to date is not the same as unchecked
If a source fails, the packages it serves come back with no versions. Reporting that as "up to date" is how a sweep silently skips every internal package in an estate and still exits 0. nuup tells them apart, says which source failed, and exits non-zero:
nuup: 3 to upgrade, 12 up to date, 4 could not be checked
nuup: corp-feed — Unable to load the service index for source https://nuget.corp.example/v3/index.json
nuup: 4 packages were not checked because a source failed; they are not known to be up to dateAcross many repositories
repwrk clone -o my-org -t my-team -b chore/bump-packages
repwrk foreach nuup -f "MyCompany.*" --write
repwrk foreach --parallel dotnet build
repwrk foreach git commit -am "Bump internal packages"nuls lists what an estate references; nuup changes it. They are separate tools on purpose, and neither depends on the other.
Colour
Colour is decided per stream, not per process. nuup puts data on stdout and commentary on stderr, and the two are redirected independently: nuup > plan.txt still wants a readable summary on the terminal, and escape codes in the file would be corruption.
A stream that is not a terminal never gets colour, so a pipe receives exactly the bytes it would have without any of this. NO_COLOR turns it off, FORCE_COLOR turns it on where nothing can be detected, and FORCE_COLOR=0 is the explicit off switch.
It marks a state rather than decorating one: the count that could not be checked is yellow, because that is the one worth spotting across forty repositories, and a failing source is red.
Exit codes
| Code | Meaning |
| --- | --- |
| 0 | Success |
| 1 | A source could not be reached, or a file could not be written |
| 2 | Usage error |
A run where some source failed exits 1 even when the upgrades it did find were applied, so a script cannot mistake a partial answer for a complete one.
Development
npm testVersion comparison, the surgical edits and the table are tested on their own; the feed is tested through an injected dotnet package search, and repository scanning against fixture trees. Nothing in the suite touches the network.
Licence
MIT
