Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
0653899
Add convert-to-cpm skill for NuGet Central Package Management conversion
jeffhandley Feb 15, 2026
27e84f7
Refactor SKILL.md with references, baseline build, and package compar…
jeffhandley Feb 15, 2026
fc2ff0f
Replace third-party packages with Microsoft equivalents, add baseline…
jeffhandley Feb 15, 2026
fd6313f
Address review feedback on terminology, placement, and reference accu…
jeffhandley Feb 15, 2026
402af8c
Address review findings: sort PackageVersion, add packages.config sce…
jeffhandley Feb 15, 2026
ad5485b
Fix test scenario accuracy: sln/slnx mix, package counts, ImplicitUsings
jeffhandley Feb 15, 2026
90bb89c
Add skill evaluation section to test README
jeffhandley Feb 15, 2026
5911ea1
Fix Inventory.sln references and make evaluation opt-in
jeffhandley Feb 15, 2026
ec6b8c6
Condense SKILL.md with progressive disclosure
jeffhandley Feb 15, 2026
003a434
Fix Enterprise.sln references to Enterprise.slnx in with-skill.md
jeffhandley Feb 15, 2026
abae173
Improve convert-to-cpm skill: preserve whitespace, save summary repor…
jeffhandley Feb 16, 2026
2dbd1f3
Indicate that the binlog viewer is for the user; not the tool/skill
jeffhandley Feb 20, 2026
79b4427
Move convert-to-cpm skill and tests to src/dotnet-msbuild layout
jeffhandley Feb 21, 2026
5fec844
Convert tests from with/without-skill format to eval.yaml
jeffhandley Feb 21, 2026
4d7d14a
Consolidate eval.yaml files into single root eval.yaml
jeffhandley Feb 21, 2026
d3270b4
Merge branch 'main' into jeffhandley/convert-to-cpm
jeffhandley Mar 2, 2026
a899154
Reorganize convert-to-cpm into plugins/tests folders
jeffhandley Mar 3, 2026
76dd717
Add post-conversion reporting rubrics to CPM eval scenarios
jeffhandley Mar 4, 2026
56dd1d5
Improve post-conversion reporting, conflict resolution, and skill act…
jeffhandley Mar 4, 2026
12a5dee
Address PR feedback from kartheekp-ms
jeffhandley Mar 4, 2026
d2914be
Merge branch 'main' into jeffhandley/convert-to-cpm
jeffhandley Mar 4, 2026
636ce91
Improve convert-to-cpm skill activation, artifact preservation, and e…
jeffhandley Mar 5, 2026
f006833
Extend timeouts and add CODEOWNERS entry
jeffhandley Mar 6, 2026
f6a9b95
Merge branch 'main' into jeffhandley/convert-to-cpm
jeffhandley Mar 6, 2026
a5f4d3c
Merge branch 'main' into jeffhandley/convert-to-cpm
jeffhandley Mar 7, 2026
9c9bc1f
Move convert-to-cpm skill into a new dotnet-nuget plugin
jeffhandley Mar 7, 2026
dab18d4
Fix erroneous plugin msbuild/nuget plugin split change. Add dotnet-nu…
jeffhandley Mar 9, 2026
bb07e7a
Merge branch 'main' into jeffhandley/convert-to-cpm
ViktorHofer Mar 9, 2026
b46dba1
Add NuGet package management skills to README
ViktorHofer Mar 9, 2026
338e1e1
Merge branch 'main' into jeffhandley/convert-to-cpm
jeffhandley Mar 12, 2026
5324fb7
Merge branch 'jeffhandley/convert-to-cpm' of https://github.com/dotne…
jeffhandley Mar 12, 2026
49c19ea
Address PR feedback: clarify CPM sub-folder semantics, remove securit…
jeffhandley Mar 12, 2026
c0f6e48
Extend eval timeouts for scenarios hitting time limits
jeffhandley Mar 12, 2026
110e2b8
Merge branch 'main' into jeffhandley/convert-to-cpm
jeffhandley Mar 12, 2026
8c71103
Merge branch 'main' into jeffhandley/convert-to-cpm
jeffhandley Mar 17, 2026
d76e394
Add missing CODEOWNERS for dotnet-nuget plugin. Reorder convert-to-cp…
jeffhandley Mar 18, 2026
0e93231
Remove containing skill mentions from reference files; fix ordering
jeffhandley Mar 18, 2026
c050835
Replace output_matches with deterministic file assertions in eval.yaml
jeffhandley Mar 18, 2026
fe1ad3b
Reorganize skill description with USE FOR / DO NOT USE FOR markers
jeffhandley Mar 18, 2026
341d7c6
Fix issues for external link references
jeffhandley Mar 18, 2026
1b5e61f
Increase timeouts
jeffhandley Mar 18, 2026
a919a64
Handle packages.config non-activation in convert-to-cpm skill
jeffhandley Mar 18, 2026
65a663d
Remove leading blank lines from .sln test files
jeffhandley Mar 18, 2026
c2fe3c8
Use imperative 'Read' instead of 'See' for reference file links
jeffhandley Mar 18, 2026
286772e
Merge branch 'main' into jeffhandley/convert-to-cpm
jeffhandley Mar 18, 2026
3c6d391
Simplify dotnet-nuget CODEOWNERS
jeffhandley Mar 18, 2026
1351841
Simplify packages.config guard
jeffhandley Mar 18, 2026
b5dc93d
Add minimal Program.cs entrypoints to Web/Worker test fixtures
jeffhandley Mar 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions src/dotnet-msbuild/skills/convert-to-cpm/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
---
name: convert-to-cpm
description: Convert .NET projects and solutions to use NuGet Central Package Management (CPM). Use when the user wants to centralize package versions into a Directory.Packages.props file, remove Version attributes from PackageReference items, and adopt CPM across a repository, solution, or single project.
---

# Convert to Central Package Management

Migrate .NET projects from per-project package versioning to NuGet Central Package Management (CPM). CPM centralizes all package versions into a single `Directory.Packages.props` file, making version governance and upgrades easier across multi-project repositories.

## When to Use

- The user wants to adopt Central Package Management for a .NET repository, solution, or project
- Package versions are scattered across many `.csproj`, `.fsproj`, or `.vbproj` files and the user wants a single source of truth
- The user mentions `Directory.Packages.props`, CPM, or centralizing NuGet versions

## When Not to Use

- The repository already has CPM fully enabled for all in-scope projects
- The user is working with `packages.config`-based projects (must first migrate to `PackageReference`)
- The user wants to manage versions via a custom MSBuild property file without using CPM

## Inputs

| Input | Required | Description |
|-------|----------|-------------|
| Scope | Yes | A project file, solution file, or directory containing .NET projects to convert |
| Version conflict strategy | No | How to resolve cases where the same package has different versions across projects (default: use highest version) |

## Workflow

### Step 1: Determine scope

- **Single project**: User specifies a `.csproj`, `.fsproj`, or `.vbproj`.
- **Solution**: User specifies a `.sln` or `.slnx`. List projects with `dotnet sln list`.
- **Repository/directory**: No specific file given. Find all project files recursively from the first common ancestor directory of all .NET projects in scope.

If the scope is unclear, ask the user.

### Step 2: Establish baseline build

Before making any changes, verify the scope builds successfully and capture a baseline snapshot using the procedure in [baseline-comparison.md](references/baseline-comparison.md). If the baseline build fails, stop and inform the user — the scope must build cleanly before conversion.

### Step 3: Check for existing CPM

Search for any existing `Directory.Packages.props` in scope or ancestor directories. If CPM is already fully enabled, inform the user and stop. If a `Directory.Packages.props` exists without CPM enabled, ask whether to add the property to the existing file or create a new one.

### Step 4: Audit package references

Extract all `<PackageReference>` items and versions from in-scope project files. Also scan `<Import>` elements to discover shared `.props`/`.targets` files containing package references.

Check for complexities: version conflicts, MSBuild property-based versions, conditional references, security advisories, and existing `VersionOverride` usage. See [audit-complexities.md](references/audit-complexities.md) for the full checklist.

Present audit results to the user before proceeding, including:
- A table of each package, its version(s), and which projects use it
- Any version conflicts, security advisories, or complexities requiring decisions

### Step 5: Create or update Directory.Packages.props

Create the file with `dotnet new packagesprops` (.NET 8+) or manually. Add a `<PackageVersion>` entry for each unique package sorted alphabetically. For conditional versions or `VersionOverride` patterns, see [directory-packages-props.md](references/directory-packages-props.md).

### Step 6: Update project files

Remove the `Version` attribute from every `<PackageReference>` that now has a corresponding `<PackageVersion>`. Also update any shared `.props`/`.targets` files identified in step 4.

- Preserve all other attributes (`PrivateAssets`, `IncludeAssets`, `ExcludeAssets`, `GeneratePathProperty`, `Aliases`)
- Preserve conditional `<ItemGroup>` elements — only remove the `Version` attribute within them
- Retain each file's existing indentation style (spaces vs. tabs, indentation depth) and blank lines — do not reformat or reorganize unchanged lines
- Use `VersionOverride` (with user confirmation) when a project needs a different version than the central one

### Step 7: Handle MSBuild version properties

For `PackageReference` items that used MSBuild properties for versions, determine whether to inline the resolved value or keep the property reference in `Directory.Packages.props`. After validation succeeds in step 8, remove inlined version properties from `Directory.Build.props` or other files, verifying they have no remaining references. See [msbuild-property-handling.md](references/msbuild-property-handling.md) for the decision workflow, import order requirements, and cleanup procedure.

### Step 8: Restore and validate

Run a clean restore and build, producing post-conversion artifacts for comparison using the procedure in [baseline-comparison.md](references/baseline-comparison.md). If errors occur, see [validation-and-errors.md](references/validation-and-errors.md) for NuGet error codes and multi-TFM guidance.

### Step 9: Summary and package list comparison

Compare baseline and post-conversion package lists to produce a per-project version diff. See [baseline-comparison.md](references/baseline-comparison.md) for the comparison procedure and table format. Present changes and unchanged packages in separate tables so the user can verify the conversion.

Also present: number of projects converted, packages centralized, any skipped packages, and MSBuild properties kept or removed. Recommend running `dotnet test`.

Save the full summary (including comparison tables, conversion statistics, and recommendations) as a `convert-to-cpm.md` markdown file alongside the binlog and JSON artifacts. Inform the user that this file can be used as a pull request description. Also inform the user that binlog files and package list JSON files are available for manual inspection.

## Validation

- [ ] Baseline build succeeded before any changes were made
- [ ] `Directory.Packages.props` exists with `ManagePackageVersionsCentrally` set to `true`
- [ ] Every in-scope `PackageReference` either has no `Version` attribute or uses `VersionOverride`
- [ ] Every referenced package has a corresponding `PackageVersion` entry
- [ ] `dotnet restore` and `dotnet build` complete without errors from a clean state
- [ ] Package list comparison shows no unexpected version changes
- [ ] No orphaned version properties remain (unless intentionally kept)

## More Info

- [Central Package Management documentation](https://learn.microsoft.com/nuget/consume-packages/central-package-management)
- [Validation and common errors](references/validation-and-errors.md)
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Audit Complexities

When auditing `PackageReference` items across in-scope project files, watch for these complexities and flag them to the user:

## 1. Version set via MSBuild property

If a `PackageReference` uses a property for its version (e.g., `Version="$(SomePackageVersion)"`), trace the property definition. If the property is defined in a `Directory.Build.props`, `.props` import, or the project file itself, note it for the user. These require manual decisions about whether to replace the property with a literal version in `Directory.Packages.props` or to keep the property and use it within `Directory.Packages.props`.

See [msbuild-property-handling.md](msbuild-property-handling.md) for decision workflow.

## 2. Conditional PackageReference items

If a `PackageReference` is inside a conditional `<ItemGroup>` (e.g., `Condition="'$(TargetFramework)' == 'net8.0'"`), the version must still be centralized. The `PackageVersion` entry in `Directory.Packages.props` can use the same condition, or the project can use `VersionOverride` if the condition is project-specific.

## 3. Same package with different versions

If the same package ID appears with different versions across projects, record all versions. The default strategy is to use the highest version.

- **Major version difference**: Ask the user to confirm — may indicate intentional pinning.
- **Minor or patch difference**: Prefer the highest version but note the change — a patch-level difference may indicate a security fix.

## 4. Known security advisories

If a package version is known to have security vulnerabilities (e.g., from nuget.org advisory data or `dotnet list package --vulnerable` output), flag the vulnerable version and recommend upgrading at least to the minimum patched version. Do not silently keep a vulnerable version even if a project pins to it.

## 5. Packages without a Version attribute

These may already be managed by CPM from a parent directory or may be using a default version. Verify whether a `Directory.Packages.props` in an ancestor directory already provides the version.

## 6. PackageReference in imported .props/.targets files

Scan for `<Import>` elements in project files and `Directory.Build.props` to discover shared `.props` or `.targets` files that may contain `PackageReference` items. Search those imported files for package references — they need the same treatment but modifying shared build files has broader impact. Flag these to the user.

## 7. VersionOverride already in use

If any project already uses `VersionOverride`, note it — this suggests partial CPM adoption may already be in progress.
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Baseline Comparison

Verify the CPM conversion is version-neutral by comparing resolved package versions before and after conversion using `dotnet list package`. Binlogs are also captured as artifacts for manual inspection or troubleshooting.

## Capturing package lists

Use `dotnet list package` to snapshot resolved versions. Always build from a clean state first to ensure accurate resolution.

### Baseline (before conversion, step 2)

```bash
dotnet clean
dotnet build -bl:baseline.binlog
dotnet list package --format json > baseline-packages.json
```

### Post-conversion (after all changes, step 8)

```bash
dotnet clean
dotnet build -bl:after-cpm.binlog
dotnet list package --format json > after-cpm-packages.json
```

If `--format json` is not available (requires .NET 8 SDK+), use the default tabular output:

```bash
dotnet list package > baseline-packages.txt
```

For solution-scoped conversions, pass the solution file to all commands.

## Producing the comparison

Compare `baseline-packages.json` and `after-cpm-packages.json` per project. For each project, identify:

1. **Version changes**: Packages whose resolved version differs.
2. **Added packages**: Packages present after conversion but not in the baseline.
3. **Removed packages**: Packages present in the baseline but not after conversion.
4. **VersionOverride entries**: Packages that use `VersionOverride` (their version matches baseline but the mechanism changed).
5. **Transitive changes**: If `CentralPackageTransitivePinningEnabled` was set, note any transitive packages that are now pinned.

### Example comparison tables

Present changes and unchanged packages in separate tables. The **Changes** table highlights anything that differs from baseline — version bumps, security fixes, `VersionOverride` entries, and added/removed packages. The **Unchanged** table lists everything else for reference and confidence.

**Changes:**

```
| Project | Package | Before | After | Status |
|---------|---------|--------|-------|--------|
| Legacy.csproj | System.Text.Json | 8.0.4 | 8.0.5 | ⚠️ Security fix (CVE-2024-43485) |
| Core.csproj | System.Text.Json | 9.0.0 | 9.0.0 | VersionOverride |
| Shared.csproj | Azure.Identity | 1.10.0 | 1.10.0 | VersionOverride |
```

**Unchanged:**

```
| Project | Package | Version |
|---------|---------|---------|
| Api.csproj | System.Text.Json | 10.0.1 |
| Api.csproj | Azure.Storage.Blobs | 12.24.0 |
| Web.csproj | OpenTelemetry.Extensions.Hosting | 1.15.0 |
| Tests.csproj | xunit | 2.9.3 |
```

If there are no changes at all, state that the conversion is fully version-neutral and present only the unchanged table.

## Binlog artifacts

MSBuild binary logs (binlogs) capture the full structured build event stream, including every resolved package reference, property evaluation, and target execution. They are produced alongside the package list captures as supplementary artifacts. Inform the user they are available for manual review:

- `baseline.binlog` — Build state before CPM conversion
- `after-cpm.binlog` — Build state after CPM conversion

The user can open these `.binlog` files in the [MSBuild Structured Log Viewer](https://msbuildlog.com/) for detailed inspection of the full build tree, including target execution, property evaluation, and item resolution.

```bash
# Install the viewer on Windows
winget install KirillOsenkov.MSBuildStructuredLogViewer

# Or download from https://msbuildlog.com/
```

## When comparison reveals unexpected differences

If the post-conversion package list resolves different versions than expected (beyond intentional changes like security fixes or `VersionOverride`), investigate:

- Missing `<PackageVersion>` entries causing fallback behavior
- Conditional `<PackageVersion>` entries not matching the project's target framework
- Import order issues where a property referenced in `Directory.Packages.props` is not yet defined
- Transitive dependency resolution differences from version alignment
- Packages unexpectedly added or removed due to conditional ItemGroup changes

The binlogs can help diagnose these issues by showing the full MSBuild evaluation and package resolution. Flag any unexpected differences to the user before considering the conversion complete.
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Directory.Packages.props Creation

## Placement

- **Repository scope**: Place at the first common ancestor directory of all in-scope .NET projects. This may not be the repository root — many repos nest source code under `src/` or similar directories.
- **Solution scope**: Place in the solution directory.
- **Single project scope**: Default to the project directory. If the project is inside a repository with other projects that may be converted later, ask the user where to place it.

Only the nearest `Directory.Packages.props` is evaluated per project. When in doubt about placement, ask the user.

## Creating the file

Use the .NET CLI (available in .NET 8+):

```bash
dotnet new packagesprops
```

This generates a `Directory.Packages.props` with `ManagePackageVersionsCentrally` set to `true`. If the CLI template is not available, create the file manually:

```xml
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<!-- PackageVersion items will be added here -->
</ItemGroup>
</Project>
```

## Adding PackageVersion entries

Add a `<PackageVersion>` entry for each unique package, using the resolved version from the audit. Sort entries alphabetically by package ID:

```xml
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.0" />
<PackageVersion Include="System.Text.Json" Version="10.0.1" />
```

## Conditional versions

If the same package needs different versions for different target frameworks, use MSBuild conditions:

```xml
<PackageVersion Include="PackageA" Version="1.0.0" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
<PackageVersion Include="PackageA" Version="2.0.0" Condition="'$(TargetFramework)' == 'net8.0'" />
```

Ask the user before using conditional versions — it may be preferable to standardize on a single version.

## VersionOverride

If a project intentionally needs a different version than the centrally defined one, use `VersionOverride` in the project file instead of removing the `Version` attribute:

```xml
<PackageReference Include="System.Text.Json" VersionOverride="9.0.0" />
```

Ask the user before applying `VersionOverride` — in most cases, version alignment is preferred.
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# MSBuild Property Handling

This covers how to handle MSBuild properties that define package versions (e.g., `Version="$(DIVersion)"` or `Version="$(BlobsVersion)"`) during CPM conversion. This applies to workflow steps 7 (decisions) and 9 (cleanup).

## Step 7: Make property decisions

For each `PackageReference` that used an MSBuild property for its version:

### 1. Check if the property is used elsewhere

Search all project files, `.props`, and `.targets` files in scope for references to the property name:

```bash
# Unix/macOS
grep -r '$(PropertyName)' --include='*.csproj' --include='*.props' --include='*.targets' .

# Windows (PowerShell)
Get-ChildItem -Recurse -Include *.csproj,*.props,*.targets | Select-String '$(PropertyName)'
```

If it appears only in `PackageReference` version attributes, it is safe to remove after inlining.

### 2. Property only used for versioning (in scope)

If the property is defined in a file within scope (e.g., `Directory.Build.props`), ask the user whether to:

- **Inline**: Replace the property usage with a literal version in `Directory.Packages.props` and remove the property definition (deferred to step 9)
- **Keep**: Reference the property from `Directory.Packages.props` (e.g., `<PackageVersion Include="PackageA" Version="$(PackageAVersion)" />`)

### 3. Property used for other purposes

If the property is used beyond package versioning, do not remove it. Use the property's resolved value in `Directory.Packages.props` and inform the user.

### 4. Property defined outside scope

If the property is defined outside the conversion scope (e.g., in parent repository build infrastructure), flag it to the user and skip that package. Add a comment in `Directory.Packages.props`:

```xml
<!-- PackageA: version managed externally via $(PackageAVersion) in [file path] -->
```

## Import order

If keeping a property reference in `Directory.Packages.props` (e.g., `Version="$(PackageAVersion)"`), the property must be defined in a file that MSBuild evaluates before `Directory.Packages.props`. Properties in `Directory.Build.props` satisfy this requirement because MSBuild imports `Directory.Build.props` before `Directory.Packages.props`.

## Step 9: Clean up obsolete properties

After restore and build succeed (step 8), remove property definitions that the user chose to inline. Before removing any property, verify it has zero remaining references outside its own definition:

```bash
# Unix/macOS
grep -r '$(PropertyName)' --include='*.csproj' --include='*.props' --include='*.targets' .

# Windows (PowerShell)
Get-ChildItem -Recurse -Include *.csproj,*.props,*.targets | Select-String '$(PropertyName)'
```

Only remove a property if it has zero remaining references outside its own definition. Preserve all non-versioning properties in the same file (e.g., `OutputPath`, `LangVersion`).
Loading
Loading