All notable changes to this project are documented in this file.
- change
nuget: every package shows its source next to its version,(7.0.0, nuget.org): the source it was restored from (recorded in its.nupkg.metadata) or, if it is not restored yet, the sources it would come from (nuget.configwithpackageSourceMapping, or the project'sRestoreSources/RestoreAdditionalProjectSources). When sources other than nuget.org are involved, a legend maps source names to URLs. The summary counts packages per source, and--markdownhas aSourcecolumn. - [BUG]
clean --delete -o:--deletedeletes immediately and--output-filewrites a script to run later, but the two together silently deleted and wrote no script. The combination is now rejected; pass one of them, or-ito pick and write the script (an explicit-imakes the picker write the script even next to--delete). - [BUG]
clean --non-current: the flag never reachedobj, sobld clean --non-current --objdeleted even the current target'sobj/<Configuration>/<tfm>. It now marks only the obj TFM directories no project targets, keeps the current output and the restore artifacts (so it implies--keep-assets), and leaves publish, package andTestResultsalone, since they have no TFM to be stale by. - [BUG]
clean/stats: a project withAppendTargetFrameworkToOutputPath=falseand aRuntimeIdentifierbuilds intobin/<Configuration>/<rid>/without a TFM segment, which the output check reported asoutput layout not recognized. The RID directory under a configuration directory is now recognized and cleaned like the configuration directory;--non-currenttreats it as current. - [BUG]
clean: after deleting a TFM directory the now-empty parents (bin/Debug,obj/Debug) were left behind. They are removed up to, not including, thebin/obj(orartifacts/<kind>/<project>) root and never above the project directory; the generated scripts get matchingrmdirlines (rmdir /q ... 2>nulon Windows,rmdir -- ... 2>/dev/null || :on Linux/macOS) that remove a parent only while it is empty. - [BUG]
clean/stats: the size and file count followed symlinks and junctions out ofbin/obj, counting a share's contents and risking a cycle, and the picker, the script preview and the stats table used three slightly different copies that could disagree on an unreadable directory. One measurement that skips reparse points is shared by all three. - [BUG]
tfm: the dry run listed projects whoseTargetFrameworkcomes fromDirectory.Build.props, an import or a conditionalPropertyGroupas migrations, and--applythen failed each of them with "No unambiguous " and exit 1. Such projects are now listed under "Not migratable by bld" with the reason, in the dry run and with--applyalike, and do not count as failed migrations; the exit code is 1 only when a rewritable project was not written or nothing at all could be rewritten. - [BUG]
tfm: without network the end-of-life check came back empty, so no framework was flagged or dropped and the run looked clean. A list built into the release (as of 2026-10-02) is used instead, with a warning naming that date; live data still wins. - [BUG]
tfm: the automatic--fromdetection searched with a depth of 4 regardless of--depth, because it built its own options. It now uses the parsed options; theCleaningOptionsdefaults match the command-line defaults (depth 3,objoff). - [BUG]
nuget: aPackageReferencewithVersionOverrideunder central package management was reported at the central version. The override is what restore uses and is reported now. - [BUG]
nuget: version constraints in the rules file were compared asSystem.Versionwith a hand-made prerelease adjustment that put2.0.0-betasomewhere in 1.x, so>=2.0.0-betanever matched the beta it named and four-part versions compared wrongly. Constraints use NuGet version ordering now; the file format is unchanged and finally documented. - [BUG]
nuget: an exception escaping the package extractor (categorizer, assets reader) was printed but did not reach the error sink, so the run exited 0. - [BUG]
cpm --overwrite: newPackageVersionentries went into the firstItemGroupthat already had some, even when that group carried aCondition(a per-framework pin block), so they were only active for that framework. They go into the first unconditioned group now. - [BUG]
build-props --list: the import tree nested files by path length, so twoDirectory.Build.propswithout an ancestor relationship (a sibling directory, an explicit import) rendered as parent and child. The tree follows directory ancestry. - change
nuget: a package matched by the rules file's# blacklist(and not by# whitelist) is its own category, "Blacklisted", shown first in the per-project, aggregated and transitive views instead of staying "Microsoft Official" because of its prefix. - [BUG]
outdated: a saved package policy could loosen a stricter--max-bumpgiven for the run (--max-bump patchwith aminorpolicy produced a minor bump). The effective cap is now the more restrictive of the policy rule and--max-bump;--max-bump-forstill overrides both. - [BUG]
outdated: Ctrl+C during--applywas reported as a failed file write and the run went on to exit 1; the project, props andPackageDownloadwriters no longer swallow the cancellation, so the run exits 130. - [BUG]
outdated: the NuGet request throttle counted the wait for a slot against the request timeout, so a long queue in front of a slow feed timed requests out before they were sent. The timeout starts when the request is sent. - [BUG]
outdated: non-CPM projects that pin the same package at different versions were shown with only the lowest pin. Every distinct pin is listed in thecurrentcell (9.0.0 (1), 8.0.1 (3)) and each project is updated from its own pin. - [BUG]
containerize --migrate:USER $APP_UID, the line every .NET 8+ template Dockerfile carries, was written verbatim asContainerUser=$APP_UID(an image with a non-existent user) andAPP_UIDwas listed as an unresolved build arg.$APP_UID/${APP_UID}are the SDK's defaultappuser: nothing is written and a note says so. - [BUG]
containerize --migrate: a plain dry run (no--apply,-ior--run-as-root) asked "Keep running as root?" for a Dockerfile withoutUSERalthough it writes nothing. Only a run that writes asks; the dry run notes that--applywould. - [BUG]
containerize: heredocs inRUN/COPY/ADD(<<EOF,<<-EOF,<<"EOF") were read line by line, so anEXPOSEinside the body became aContainerPort. The body up to the terminator is skipped; the instruction itself is handled as before. - [BUG]
containerize --validate:ContainerRepositorynames with__or repeated-(my__app,my--app) and names with a registry host and port (registry:5000/team/my-app) were reported as invalid although Docker accepts them. - change
containerize: the-pshort alias of--projectsis gone (it collided with--package/-pinoutdated),--forceis now--allow-unsupported(--forcestill works as an alias), the never-used--concurrencyoption is no longer offered,--markdownwith--interactiveis rejected instead of silently printing text, and--apply,--delete-dockerfile,--allow-unsupportedor--run-as-rootwithout--migrate(or--validatefor--apply) warn that they have no effect. - [BUG]
cleanpicker: at a very narrow width a path under a Windows drive root (C:\) was shortened toC:\...in, cutting the directory name; the leaf is kept and the root dropped instead. - change
clean:--yes/-yare aliases of--force, so "do not ask" is spelled the same as inoutdated undo. - change: the NuGet
User-Agentisbld/<version>instead ofNugetMetadata/1.0.0. - change: the
statshelp texts no longer say "clean"; the README documents--confirmas the working option it is, lists the exit codes (0, 1, 130), the shared dry-run/--apply/--interactivemodel, the extra picker keys (k/j/h/l,q), and thatoutdated undotakes only--rootand--log. - build: the CI workflow runs the tests on Windows as well as Linux and cancels superseded runs. The unreachable Visual Studio MSBuild registration path (
VSService, never wired to an option) is removed; Visual Studio still contributes its targets throughVSToolsPath.
-
[BUG]
clean --non-current: a project whose current output sits directly inbin/<Configuration>(legacy projects,AppendTargetFrameworkToOutputPath=false) had that whole directory marked, deleting the output--non-currentpromises to keep. Only TFM directories below it that no project targets are marked now. -
[BUG]
clean -oon Windows: the script relied onsetlocal disabledelayedexpansion, which does not stop cmd from expanding%VAR%, so a path containing%TEMP%deleted another directory.%is written as%%. -
[BUG]
cpm --apply: a package conditional in one project and unconditional in another was centralized and kept its inlineVersionin the unconditional one, and any conditional reference kept its inlineVersiontoo - both NU1008, restore failed. Per-framework pins, property references ($(FooVersion)), ranges and floating versions now becomeVersionOverride; only literal unconditional versions move toDirectory.Packages.props. -
[BUG]
cpm: aDirectory.Packages.propsinherited from a parent directory was ignored and a new one created next to the solution, which shadowed it and left the packages it managed without a version (NU1010). The inherited file is the one merged into (with--overwrite). -
[BUG]
outdated --apply: conditionalPackageVersionentries inDirectory.Packages.propswere rewritten to the one version proposed for the package, e.g. moving a net48 pin to a version without net48. They are skipped with a warning, as conditionalPackageReferenceitems already were. -
[BUG]
tfm: platform target frameworks (net8.0-windows,-android,-ios) could not be migrated, and the dry run and--applydisagreed about them. They migrate now and keep their platform:--to net10.0turnsnet8.0-windowsintonet10.0-windows. Switching platforms is not treated as a migration. -
[BUG]
containerize: the directory scan found only files named exactlyDockerfile;Dockerfile.prodandapi.Dockerfilewere skipped (Dockerfile.dockerignoreis still not one). When several Dockerfiles build one project, the plainDockerfileis the one migrated. -
[BUG]
containerize --migrate: a relativeWORKDIRreplaced the previous one instead of resolving against it (/appthenbinis/app/bin); a first relative one is noted, since it depends on the base image. -
[BUG]
outdated: Ctrl+C was ignored during project evaluation and the NuGet lookups, and a cancelled lookup was reported as a failed fetch. Cancellation now stops the run; every command exits with 130 and "Cancelled." on Ctrl+C. -
[BUG]
clean -oon Linux/macOS: the script was created0644(only an overwritten one kept an execute bit it already had), so./clean.shfailed with "Permission denied" more often than not. It is now created executable (0755minus the umask; an existing file gains the execute bits), starts with#!/bin/sh, keeps going past anrmthat fails and exits non-zero at the end instead of reporting success, usesrm -rf --/rm -f --, and bld prints how to run it. -
change:
--markdownis only offered by the commands that print tables (stats,nuget,containerize,outdated,tfm,build-props);clean,cpmandoutdated undoaccepted it and ignored it, and now reject it. -
extend
cleancommand:--keep-private-packageskeeps a cloned repo restorable after losing access to its private feeds. Before anything is cleaned, every package the projects restored (perproject.assets.json, transitive ones included) from a source other than nuget.org - as recorded in the package's.nupkg.metadata- is copied, unless the file carries nuget.org's repository signature and so came through a mirror unchanged, from the packages folder into.nuget-private/<source key>/, andnuget.offline.configis written next to the root: nuget.org as configured, each private source's key pointing at its folder, no credentials, package source mapping carried over.dotnet nuget locals all --clearthen loses nothing thatdotnet restore --configfile nuget.offline.configcannot bring back.--private-packages-dirpicks another folder. -
change
clean: the picker is the default.bld cleannow opens it and deletes what is checked after one confirmation; the deletion script is only written with--output-file/-o, whose path is now optional (a bare-owritesclean.cmd/clean.sh). An explicit--deletedeletes without the picker as before, and an explicit-ibrings the picker back next to--deleteor-o. Without a terminal,cleanfails and names--deleteand-oinstead of writing a script nobody asked for.
- change
containerize --migrate: a customENTRYPOINTis written asContainerAppCommanditems withContainerAppCommandInstruction=EntrypointandCMDasContainerDefaultArgs, instead of theContainerEntrypointitems the SDK deprecated in .NET 8. A variant of the image the SDK would pick anyway (aspnet:8.0-alpine,8.0-noble-chiseled) becomesContainerFamilyrather than a pinnedContainerBaseImage, so the variant stays and the version follows the target framework; Windows tags stay pinned. A Dockerfile withoutUSERran as root while the SDK defaults to the non-rootappuser on .NET 8+ images: the command now asks per Dockerfile whether to keep root and writesContainerUser=rooton yes;--run-as-rootanswers yes for all, and without a terminal the SDK default is kept and noted. The "already has container settings" check covers every SDK property (ContainerRegistry,ContainerImageTag(s),ContainerRuntimeIdentifier(s),ContainerImageFormat,ContainerArchiveOutputPath,LocalRegistry, ...). - extend
containerizecommand:--validatechecks the SDK container settings every project carries and, with--apply, rewrites the ones with an exact equivalent: aContainerBaseImagethat is the image the SDK computes anyway is removed, a family variant of it (runtime-deps:10.0-azurelinux3.0-distroless-extra) becomesContainerFamily(azurelinux3.0-distroless-extra), the obsoleteContainerImageNamebecomesContainerRepository, deprecatedContainerEntrypointitems becomeContainerAppCommandwithContainerAppCommandInstruction=Entrypointwhen the instruction isNone, and aContainerWorkingDirectoryof/appgoes. Reported without a fix: elements with theContainerprefix the SDK does not read, aContainerFamilynext to aContainerBaseImage, a pin of another repository or version than the SDK would pick, invalidContainerAppCommandInstruction/ContainerImageFormat/LocalRegistryvalues, tags and repository names the registry would reject,ContainerImageTagnext toContainerImageTags, badContainerPortitems,ContainerRuntimeIdentifiersoutsideRuntimeIdentifiers, and a pin carrying a platform suffix (aspnet:8.0-amd64):ContainerFamilycannot carry the platform, so dropping the pin would hand the architecture to theRuntimeIdentifierand let the image follow the building machine. Conditioned settings are reported, never rewritten. - extend
containerizecommand:--interactive/-iwith--migrateor--validateasks instead of--apply. Per Dockerfile: migrate anyway when the SDK cannot express part of it (default no), write the settings (default yes), delete the Dockerfile and the Visual Studio container-tools settings (default no;--delete-dockerfileanswers yes for all). Per fixable validation finding: fix it (default yes). Answers are written at once; without a terminal the command fails with a hint. - [BUG]
containerize --projects: the scan readContainerImage, which is not an SDK property, so a project identified byContainerRepository,ContainerFamily,ContainerRegistryorContainerImageTag(s)alone was not listed. Those are read now (ContainerImageNameas the obsolete alias ofContainerRepository) and shown. - [BUG]
clean/stats: a project with aRuntimeIdentifierbuilds intobin/<Configuration>/<tfm>/<rid>/, which the output check did not know, so the project was skipped withoutput layout not recognized. The RID directory is now recognized and the project'sbin/<Configuration>/<tfm>cleaned like any other;--non-currenttreats its framework as current. - change
clean --interactive/-i: the picker now deletes. It used to write a deletion script unless--deletewas also given, which made the obvious command a dry run. Afterenterit asks once for the whole selection (Delete 12 directories (340.2 MiB)?, default no) and then deletes it;--forceskips that question, an explicit--confirmstill adds the per-directory ones, and--output-file/-owrites the script for the selection instead.
- extend
cleancommand:--interactive/-ishows everything the run would delete as a list grouped by project, with the same keys as theoutdatedpicker.b/o/p/g/ttoggle bin, obj, publish, package and test results for every project on the top line or for one project on its line, space toggles a directory, headers show each category as checked, unchecked or partly checked with the selected and total size.--obj,--publishand--test-resultsonly decide what starts out checked. With--deletethe picker replaces the per-directory confirmation unless--confirmis given. Every row shows the fully qualified path that would be deleted, cut in the middle when the terminal is too narrow; the same directory marked with and without a trailing separator is one row, not two; and what the picker hands back is checked against what the run marked, so a path the run never marked aborts it instead of being deleted. - extend
cleanandstats:--test-resultsalso cleansTestResults/next to each project (VSTestResultsDirectorywhen set) and next to its solution. - [BUG]
clean/stats--publish: aPackageOutputPathoutside the build output (a local NuGet feed,artifacts/package/<config>/) was marked as a whole, with every other project's packages in it. Package output is now cleaned per file: only<PackageId>.<version>[.symbols].nupkg/.snupkgdirectly in that directory, only for projects that pack, and the directory itself is never touched. The name only nominates a file; the id in the package's own.nuspecdecides, soFoocannot deleteFoo.1'sFoo.1.2.0.nupkg, and a file that does not read as a package is left alone. The scripts get onedel/rmline per file,--deleteasks per file at theDirectoryconfirm level, and the picker lists each file with its own size.
- [BUG]
outdated --interactiveandoutdated undo -i: the picker showed at most 30 rows, leaving the rest of a tall terminal empty. It now uses the whole height, less the header lines as they wrap at the terminal width.
- extend
outdatedcommand:bld outdated undo. Every--apply,--interactiveandtfm --update-packagesrun that changes a file records what it wrote (file, package, value before and after, per element) underBLD_HOME/history, andundotakes the newest run back after showing a table and asking once — or part of it with-p/--exclude, or from a grouped picker with-i. Reverted edits leave the record, so a secondundopops the run before;--listand--run <n>reach older runs. A value that no longer reads as the run left it is skipped and named (with the later run that wrote it, when there is one), never overwritten.--yesskips the question,--verify-restorerestores afterwards. - extend
tfmcommand:--update-packagesnow runsoutdated --applyon the same input after the frameworks are written, so packages are checked for compatibility with the new target framework, capped by the new--max-bumpoption and the saved package policies, checked for dependency conflicts, and updated inDirectory.Packages.propsunder central package management. It used to bump every inlinePackageReferenceto the latest stable version without any of that, and updated nothing under central package management. - [BUG] exit codes:
nugetandbuild-propsreturned 0 after reporting errors; they now return 1 when a solution or project could not be analyzed (build-propsalso when a project could not be evaluated).cpmandtfmcollected solution and project load errors and never showed them; they are now printed and fail the command. - build:
TreatWarningsAsErrorsis set (the previousWarningsAsErrorsproperty takes a list of warning ids, so warnings never failed the build); a reponuget.configpins the restore to nuget.org. A CI workflow runs the test suite on every push and pull request, and the publish workflow runs it before packing and refuses a release tag that does not match the project version. - extend
outdatedcommand:--interactiveshows the packages grouped, so a whole family can be toggled in one keystroke, and left/right move a package between the versions the feeds offer for it — highest patch, highest minor, highest major — or cap a whole family at a bump class from its group line, which shows the class (a package with nothing at or below the cap is unchecked until the cap admits it again); packages are indented under their group line.--group-by <prefix|bump|none>(defaultprefix),--group-depthand--group-mincontrol the grouping; passing--group-byexplicitly also groups the report table. Every row names its bump class (patch/minor/major, colored),--preselect <all|no-major|patch|none>picks what starts out checked, versions the bump cap held back are listed unchecked instead of being unreachable, and esc cancels without writing. [BUG]--interactivewithout a terminal threw out of Spectre; it now fails with an exit code and a hint. - extend
outdatedcommand: one lookup per package now collects the highest compatible version per bump class instead of stopping at the first usable one, so--max-bumppicks from a set rather than constraining the fetch. [BUG] the version in theheldcolumn was never checked for target framework compatibility; every offered version is now checked, and registration pages that end below the pinned version are no longer fetched. - extend
outdatedcommand:--max-bump-for "<pattern>=<level>"overrides--max-bumpper package pattern, and--interactiveprints the equivalent prompt-free command line after the selection. - extend
outdatedcommand: the dependency check also runs in reverse. A package that stays where it is and declares a range on one being updated now holds that update back when the new version falls outside the range (A 9.0.1 breaks Q 3.1.0, which requires A [8.0.0, 9.0.0));--interactiveoffers to include the blocking package's update instead. The manifests come from the registration data already fetched, so it costs no extra request. [BUG] a range declared for one target framework was dropped when another framework group declared an open-ended range on the same dependency, so an upper bound could go unchecked. - extend
outdatedcommand: package policies.policy.jsonunderBLD_HOME(or the local application data folder) holds persistent per-pattern caps (MassTransit*at mostminor) that every run applies;--max-bump-foroverrides a policy,--ignore-policyskips them. In--interactive,psaves a "no major" rule for the package under the cursor (the family's pattern on a prefix group line) and caps the row, or removes the rule it has. The held-back summary names the source of each cap. - [BUG]
outdated --interactive: the picker was not drawn until the first key was pressed; only the "Interactive update selection" rule was visible. - speed up
outdated: ProjectReferences are read from the same MSBuild evaluation as the PackageReferences (they used to cost a second, sequential evaluation of every project configuration), only the Release configuration is evaluated, evaluations reuse pooledProjectCollections and one shared evaluation context so the SDK imports are parsed once per worker and the SDK is resolved once per run instead of once per project, the SDK's default item globs are not expanded (nothing read depends on them), every package source is queried at once, all registration pages that can hold a candidate are fetched at once, and lookups run at least 16 wide regardless of--concurrency.-v Infoprints how long evaluation and metadata fetching took. - extend
outdatedcommand:--eval-cacheskips the MSBuild evaluation of a project configuration when the project file and every non-SDK import are byte-identical to the last evaluation (SHA-256, keyed by global properties, MSBuild version and whichDirectory.Build.*files exist up the tree). Entries live underBLD_HOMEor the local application data folder. Opt-in: properties set through environment variables are not detected.
- [BUG]
clean/stats: projects using the SDK artifacts layout (UseArtifactsOutput=true) were only cleaned when single-targeted; multi-targeted output underartifacts/bin/<project>/<config>_<tfm>[_<rid>]/was skipped without any message. Both are now recognized,--non-currentapplies to the TFM segment, and anOutDirthat matches no known layout is reported as a warning. - extend
clean/stats:--publishalso cleans publish output (PublishDir) and pack output (PackageOutputPath), includingartifacts/publish/<project>/andartifacts/package/in the artifacts layout. Off by default. - extend
tfmcommand: warns when the governingglobal.jsonpins an SDK that cannot build the target framework;--update-global-jsonsetssdk.versionto the highest installed SDK of the target's major on--apply. - extend
nugetandoutdated:GlobalPackageReferenceandPackageDownloaditems are listed and checked. [BUG] aGlobalPackageReferencewas attributed to the SDK's NuGet.targets instead ofDirectory.Packages.props, sooutdated --applyreported the update but wrote nothing; it is now updated in place.PackageDownloadversions are updated in their bracketed form and skip the TFM check. - extend
nugetcommand:--transitivelists the packages resolved through other packages (fromproject.assets.json), categorized and blacklist-checked like direct references, with the packages that pull them in. - extend
outdatedcommand: package sources come from thenuget.confighierarchy (enabled sources,packageSourceMapping,packageSourceCredentials) instead of only api.nuget.org, so packages on private feeds are checked and updated.--sourcerestricts or overrides the sources,--ignore-source-mappingqueries every source.tfm --update-packagesuses the same sources. - extend
outdatedcommand:--max-bump <major|minor|patch>caps how far a package may move from the version it is pinned at now. Versions above the cap are reported in a newheldcolumn instead of being applied. - extend
outdatedcommand:--package/-pand--excludeselect a subset of packages by wildcard pattern. - extend
outdatedcommand:--applyand dry runs now check the packages they would update against the versions their dependencies will end up at, and hold back any package whose declared range would be violated.--allow-conflictsupdates anyway. The check covers direct references only. - extend
outdatedcommand:--verify-restorerunsdotnet restoreafter--applyand fails the command on NuGet errors. - extend
containerizecommand:--migrateturns each Dockerfile into SDK container properties on the project it builds (ContainerBaseImage,ContainerPort,ContainerEnvironmentVariable,ContainerLabel,ContainerWorkingDirectory,ContainerUser,ContainerEntrypoint/ContainerDefaultArgswithContainerAppCommandInstruction). The runtime stage and the stages it derives from are folded like Docker does,ARG/ENVvalues are substituted, and settings that equal the SDK's defaults (base image for the target framework,/app,dotnet App.dll) are left out so they keep following the project. Instructions the SDK cannot express (RUN,VOLUME,HEALTHCHECK, files copied from the build context) are listed and block the Dockerfile unless--force. Dry run by default;--applywrites with the file's layout preserved,--delete-dockerfilealso removes the Dockerfile and the Visual Studio container-tools properties and package.--markdowntabulates the plan. - bump
System.CommandLineto 2.0.12 andMicrosoft.SourceLink.GitHubto 10.0.401.
- remove net8.0 target
- extend
outdatedcommand: --orphaned and --interactive - fix System.Text.Json load error by preloading
- Added
build-propscommand to the root command set. - [BUG] .slnf file handling changed. Actually applies the fiter from the .slnf file now (it did not before).