Verified CPM migration and dependency analysis - #3
Merged
Conversation
Writing Directory.Packages.props was the whole tool. It is now the first step of a workflow that can prove the migration was safe and keep the dependencies healthy afterwards. Core - Encoding-aware IO. Every project file is decoded with its encoding, BOM and line-ending style recorded, and re-encoded through the same record on write, so a CRLF UTF-8-with-BOM file comes back byte-identical apart from what actually changed. --encoding and --linewrap override when you want normalisation instead. - XML is edited surgically with LoadOptions.PreserveWhitespace rather than re-serialised, so comments, indentation and attribute order survive. - Discovery understands .sln, .slnx and .slnf, and C#, F# and VB projects, with regex directory exclusion on top of the usual bin/obj skips. - Migration planning happens entirely in memory and returns a plan holding the original and new content of every file. Dry-run prints it, --diff diffs it, apply writes it. There is no second code path that could disagree with the preview. - Backups before every write, including a record of files the operation created so a restore removes them again. List, restore, retention, prune. - Verification restores and snapshots the resolved package graph before and after the migration and diffs the two, rolling back when anything moved. This is the difference between "the files look right" and "the build produces the same binaries". - Analysis engine with 16 rules, per-rule severity overrides, package ignores, baselines and a 0-100 health score normalised for workspace size. Auto-fixes are planned as one batch and backed up like any other write. - Package updates run the test suite and roll back on failure. --bisect keeps the largest subset that still passes and names what it held back. - Feed lookups go through every source in nuget.config, so private feeds work, and cover versions, deprecation, licence and advisories. Property hoisting A property only moves into Directory.Build.props when every project that declares it uses the same value, and identity properties like AssemblyName are never touched. Requiring unanimity is what makes the hoist behaviour-preserving; conditioned PropertyGroups are left alone entirely. CLI Rebuilt on Spectre.Console.Cli. migrate, build, all, revert, analyze, update, verify, tree, status, doctor, init, explain, batch, backups and completions, with exit codes 0-9 so CI can branch on the outcome. packages, convert and packages-enhanced alias migrate. Reports render as terminal output, JSON, SARIF 2.1.0, Markdown or CSV. Build Multi-targets net8.0, net9.0 and net10.0. global.json now rolls forward to the latest installed major instead of pinning 9.0.0, which no longer resolves on a machine with only the .NET 10 SDK. 334 tests.
…hangelog The README described three commands and an "enhanced" mode. It now covers the full command surface, the configuration file, the exit codes and the CI recipes people actually need. - docs/migration-playbook.md walks the route from inline versions to a CI-gated workspace, with a safe stopping point at every step. - docs/architecture.md explains the two invariants worth knowing (planning never writes, format is data) and how to add a rule or a command. - schemas/centralconfig.schema.json backs the $schema reference that init writes, so editors complete and validate the config file. - QUICK_START_ENHANCED.md and VERSION_MANAGEMENT.md described behaviour that no longer exists; their useful parts moved into the docs above. - build.ps1 now also tests and packs, and build.sh gives the same thing to everyone not on Windows.
…workflow - The build job runs on Linux, Windows and macOS with the 8, 9 and 10 SDKs installed, since the tool multi-targets all three. - A self-check job runs the tool against its own repository and uploads SARIF, so a regression in the analyzers shows up as an annotation on the pull request that caused it. - release.yml publishes to NuGet on a v* tag or on demand, and creates the GitHub release from the tag. --skip-duplicate keeps a re-run from failing when a version is already live.
|
You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool. What Enabling Code Scanning Means:
For more information about GitHub Code Scanning, check out the documentation. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Writing
Directory.Packages.propswas the whole tool. This turns that into the first step of a workflow that can prove the migration was safe and keep the dependencies healthy afterwards.Why
Two things bothered me about the old behaviour.
The first is that a migration was unverifiable. We rewrote every project file and told the user it worked. Nobody could tell whether NuGet still resolved the same packages afterwards, and that is the only question that actually matters — a migration that changes what gets restored is not a refactor, it is a behaviour change wearing a refactor's clothes.
The second is that
XDocument.ToString()reformatted every file it touched. A one-line version removal produced a diff covering the whole project file, comments moved, and CRLF repos got LF back. That is enough friction to make people not run the tool.What changed
The migration is provable.
verifyrestores and snapshots the full resolved package graph, migrates, restores and snapshots again, then diffs the two. If any package resolves differently it reports the drift and rolls back. Pure additions pass by default, because that is what enabling transitive pinning legitimately does;--strictfails on those too. Exit code9means drift.I tested it against a two-project solution where the library was on Newtonsoft.Json 13.0.1 and the app on 13.0.3. Unifying on 13.0.3 genuinely changes what the library resolves, and
verifycaught it and rolled back — which is exactly the case that would otherwise ship silently.Edits are format-preserving. Every file is decoded with its encoding, BOM and line-ending style recorded, and re-encoded through the same record. XML is parsed with
LoadOptions.PreserveWhitespaceand edited surgically. A CRLF UTF-8-with-BOM project comes back byte-identical apart from the versions that were removed.--encodingand--linewrapare there when you want normalisation instead.Planning never writes.
PlanAsyncreturns the original and new content of every file. Dry-run prints it,--diffdiffs it, apply writes it. There is no second code path that could disagree with the preview, which is what makes--dry-runtrustworthy rather than approximate.Property hoisting requires unanimity. A property only moves into
Directory.Build.propswhen every project declaring it uses the same value, and identity properties likeAssemblyNameare never candidates. ConditionedPropertyGroups are left alone entirely. Anything short of unanimity changes the evaluated build for at least one project.Analysis, then maintenance. 16 rules covering vulnerabilities, deprecation, licence risk, version drift, floating versions, transitive conflicts, duplicate and orphaned entries. Severity overrides, package ignores and baselines so CI can gate on new findings only. Auto-fix for the mechanical ones.
updateruns the tests and rolls back on failure, or bisects for the largest subset that still passes and tells you which packages it held back.Command surface
migratebuildallrevertanalyzeupdateverifytreestatusdoctorinitexplainbatchbackups {list,restore,prune}completionspackages,convertandpackages-enhancedaliasmigrate. Exit codes are 0–9 so scripts can branch on the outcome.Safety
Every mutating command backs up what it is about to touch first, including recording files it creates so a restore removes them again.
backups restoreundoes anything the tool did.Build and CI
Multi-targets
net8.0,net9.0andnet10.0.global.jsonnow rolls forward to the latest installed major rather than pinning9.0.0, which does not resolve on a machine with only the .NET 10 SDK.CI builds on Linux, Windows and macOS. A self-check job runs the tool against this repository and uploads SARIF, so a regression in the analyzers surfaces as an annotation on the PR that caused it.
release.ymlpublishes to NuGet on av*tag or on demand.release.ymlneedsNUGET_API_KEYin repository secrets before the first tag.Notes for review
System.CommandLine(which was still on a 2022 beta) toSpectre.Console.Cli, which was already a dependency.-dstays--directory; dry-run is-n/--dry-run..centralconfig-backupsis in the default exclusion list. It has to be — without it the backup tree gets scanned as part of the workspace, and--fixwill happily rewrite the backups it is supposed to restore from.batchprefers solutions and only falls back to project directories when no solution exists anywhere under the root, so a mixed monorepo will skip solution-less projects. Worth revisiting if anyone hits it.PropertyDriftfindings are deliberately not auto-fixable. Hoisting also has to delete the property from every project, and doing half of that outside thebuildcommand would be worse than reporting it.334 tests, no warnings.