Skip to content
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ All notable changes to this project are documented here. The format follows [Kee

- `manager/`: the first build of `mskit-manager`, the `DragoAnt.MSBuildKit.Manager` .NET tool (`net8.0`, `net10.0`, `RollForward=Major`) that will install, update and migrate the kit. This build has one command, `status [--json]`, which prints the tool version; the logo goes to stderr, only on a terminal and never with `--no-logo`, so `--json` output always parses. Each run writes a log under `<system temp>/mskit-manager/logs/`, named after the command, newest 20 kept. Not published yet.

### Changed

- The documentation moved from the README into [docs/](./docs/README.md), one page per topic in reading order, with a [property reference](./docs/reference/properties.md) and a [code reference](./docs/reference/codes.md) that cover everything the kit sets, reads and reports. Corrected along the way: most packaging defaults apply to every project, not only packable ones; a Roslyn project imports its role's props itself; an update rewrites more than `.toolkit/msbuild/`; any tag build is a release build.

## [0.2.1] - 2026-10-05

### Added
Expand Down
9 changes: 6 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Issues and pull requests are welcome.
- `tests/run.sh` is the self-test; `tests/fixtures/PackageChecks` breaks every package rule on purpose.
- `tools/pack-kit.sh` builds the release zip and its SHA-256.
- `manager/` is the `mskit-manager` .NET tool: its own solution, central package versions and `global.json`, built against `manager/.toolkit/` — the working-tree kit installed with `sh kit/.toolkit/update.sh --source kit --root manager` and committed.
- `docs/` holds the user documentation, read in the order of [docs/README.md](./docs/README.md); `docs/reference/` lists every property and code. `tools/docs-check.sh` keeps them honest against the kit.

## Build and test

Expand All @@ -18,7 +19,7 @@ You need the .NET 10 SDK and the .NET 8 runtime, plus `sh` (Git Bash on Windows)
sh tests/run.sh
```

It installs the working-tree kit into the sample and the fixtures with `update.sh`, then checks the computed versions for local, branch, pull-request and tag builds, runs the sample's tests with coverage, inspects the packed nuspec, and asserts that every `MSKIT_PKG` check fires on the fixtures. CI runs the same script on Linux and Windows.
It installs the working-tree kit into the sample and the fixtures with `update.sh`, then checks the computed versions for local, branch, pull-request and tag builds, runs the sample's tests with coverage, inspects the packed nuspec, asserts that every `MSKIT_PKG` check fires on the fixtures, and runs `sh tools/docs-check.sh`. CI runs the same script on Linux and Windows.

The tool's tests run from `manager/`, so its `global.json` selects Microsoft.Testing.Platform:

Expand All @@ -30,8 +31,10 @@ CI's `manager` job also packs the tool, installs it into a tool path and runs `m

## Changing the kit

- A new property defaults with `Condition="'$(Name)'==''"`, so a consumer's value always wins, and gets a row in the README.
- A new check gets an `MSKIT_<AREA><nnn>` code, a message that says how to fix it, a fixture that triggers it and a line in `tests/run.sh`.
- A new property defaults with `Condition="'$(Name)'==''"`, so a consumer's value always wins, and gets a row in [docs/reference/properties.md](./docs/reference/properties.md) plus a mention on its topic page.
- A new check gets an `MSKIT_<AREA><nnn>` code (a shipped code is never renumbered or reused), a message that says how to fix it, a section in [docs/reference/codes.md](./docs/reference/codes.md) headed by the code id without the underscore, a fixture that triggers it and a line in `tests/run.sh`.
- `sh tools/docs-check.sh` fails on a property, item or code without its reference entry, on a name the docs mention that the kit lacks, and on a broken relative link; `--list properties|items|codes` prints the kit's inventory with the file and line of each.
- The README stays short: key features, install, links. Detail goes to the topic page in `docs/`.
- A new part needs a line in `kit/.toolkit/kit.parts` and its `init.props` / `init.targets` imports in the entry points.
- Keep scripts POSIX `sh` and PowerShell 7 equivalent; `update.sh` and `update.ps1` must produce the same `.toolkit/`.
- Add a line to `CHANGELOG.md` under `Unreleased`.
Expand Down
180 changes: 12 additions & 168 deletions README.md

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# MSBuildKit documentation

Read in this order; each page stands on its own, so jump to the one you need.

| # | Page | Read it to |
| --- | --- | --- |
| 1 | [Getting started](./getting-started.md) | install the kit, build, test and pack a first library |
| 2 | [Install and update](./install-and-update.md) | move to another version, add or remove parts, install a local build |
| 3 | [Parts](./parts.md) | see what each part does, which are installed by default and the order they load in |
| 4 | [Build defaults and checks](./build.md) | know the language defaults, the owner layer, CI detection, target-framework rules and reference audits |
| 5 | [Versioning](./versioning.md) | control how package versions are computed |
| 6 | [Local files and secrets](./local-files.md) | keep secrets and machine-only source files out of the repository |
| 7 | [Optional parts](./optional-parts.md) | debug a dependency from source, list a project's packages, run Entity Framework migrations |
| 8 | [Packaging](./packaging.md) | understand the nuget.org defaults and the `MSKIT_PKG` checks |
| 9 | [Package readme](./package-readme.md) | generate every package's readme from the repository README |
| 10 | [Testing](./testing.md) | set up test projects, assertions, mocking and coverage |
| 11 | [Roslyn components](./roslyn.md) | build analyzers, code fixes and source generators |
| 12 | [Customizing](./customizing.md) | change the owner, import your own files, override a default |
| 13 | [Troubleshooting](./troubleshooting.md) | fix a failing build or update |
| 14 | [Code reference](./reference/codes.md) | look up any warning or error the kit reports |
| 15 | [Property reference](./reference/properties.md) | look up any property the kit sets or reads |
| 16 | [Migrating from MSBuild.Routine](./migrating-from-msbuild-routine.md) | move a repository off the older submodule |

Contributing to the kit itself: [CONTRIBUTING.md](../CONTRIBUTING.md).
114 changes: 114 additions & 0 deletions docs/build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Build defaults and checks

What the Core, Trunk, TfmConstants and PrivateAssets parts set for every project, and the checks that keep a repository consistent. Every default yields to a value you set ([load order](./parts.md#load-order)); every property is in the [property reference](./reference/properties.md).

## Language and product defaults

| Property | Default |
| --- | --- |
| [`Nullable`](https://learn.microsoft.com/dotnet/csharp/language-reference/compiler-options/language#nullable), [`ImplicitUsings`](https://learn.microsoft.com/dotnet/core/project-sdk/overview#implicit-using-directives) | `enable` |
| [`LangVersion`](https://learn.microsoft.com/dotnet/csharp/language-reference/configure-language-version) | `latest` |
| [`EnforceCodeStyleInBuild`](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/overview#code-style-analysis) | `True` |
| `GeneratePackageOnBuild` | `False` |
| `AssemblyTitle` | the project name |
| `Authors`, `LegalTrademarks` | `$(ManufacturerName)` |
| `Company` | `$(FullManufacturerName)` |
| `Copyright` | `Copyright (c) <current UTC year> $(FullManufacturerName)` — no "All rights reserved", which contradicts an open-source licence |

The owner layer adds `IsPackable=True`, `TreatWarningsAsErrors=True`, `NoWarn` `CS1591;xUnit1051` and keeps NuGet vulnerability warnings `NU1901`-`NU1904` as warnings ([Customizing](./customizing.md#the-owner-layer)).

## Developer machine or CI

`MSKit_IsDevEnv` is `True` unless the Vcs.GitHub part sees `GITHUB_RUN_ID`, which GitHub Actions sets for every job. It decides:

| | Developer machine | CI |
| --- | --- | --- |
| Version | `9999.0.0` | from the tag, pull request or run ([Versioning](./versioning.md)) |
| `MSKIT_PKG` checks | warnings | errors ([Packaging](./packaging.md)) |
| [`ContinuousIntegrationBuild`](https://learn.microsoft.com/dotnet/core/project-sdk/msbuild-props#continuousintegrationbuild) | unset (PDBs keep real paths) | `true` |
| Local secrets | created from templates | deleted ([Local files](./local-files.md)) |
| `TreatWarningsAsErrors` drift check | on | off: CI passes its own `-p:TreatWarningsAsErrors` |

Set `MSKit_IsDevEnv` yourself to build "as CI" locally (`-p:MSKit_IsDevEnv=False`).

## Roots, branch and commit

| Property | Value |
| --- | --- |
| `MSKit_SlnFileDirectory` | the folder of the `.slnx` being built (`SlnxFilePath`), else the folder that holds `.toolkit/`; ends with `/` |
| `MSKit_SlnFileName` | the solution name without extension, when building a `.slnx` |
| `MSKit_GitRoot` | the nearest folder above the project with a `.git` (worktrees included) |
| `MSKit_ToolkitDir`, `MSKit_ToolkitMSBuildDir` | `.toolkit/` and `.toolkit/msbuild/`, absolute, ending with `/` |
| `MSKit_ProjectObjDir` | the project's `obj` folder, absolute |
| `MSKit_Branch` | on GitHub Actions `GITHUB_HEAD_REF` (pull requests) or `GITHUB_REF_NAME`; else read from `.git/HEAD`; else `unknown-branch` |
| `MSKit_IsStableBranch` | `true` when `MSKit_Branch` matches `MSKit_IsStableBranchRegex` (case-insensitive): the owner layer's `^(main\|release/.+)$`, the kit's own default `^(main\|master\|release/.+)$` |
| `MSKit_CommitSha` | `GITHUB_SHA` on CI, empty on a developer machine |

The kit imports these files from `MSKit_SlnFileDirectory` when they exist: `Directory.Version.props`, `Directory.GlobalUsings.props` / `.targets`, `Directory.Packages.Metadata.targets`, `Directory.PackageAsProj.targets` ([Customizing](./customizing.md#extension-files)).

## Target frameworks declared once

Declare `TargetFramework` or `TargetFrameworks` once, in `Directory.Build.props` above the kit import, and leave it out of the projects. A project that needs the other shape declares only that one; the kit clears the shared value of the shape the project does not use, so a library can multi-target while one tool project targets a single framework. The props phase reads the two files as text for this, because the SDK fixes cross-targeting before the targets phase.

| Code | When |
| --- | --- |
| [`MSKIT_SHARED006`](./reference/codes.md#mskitshared006), [`MSKIT_SHARED007`](./reference/codes.md#mskitshared007) | error: the csproj repeats the shared `TargetFramework` / `TargetFrameworks` value |
| [`MSKIT_SHARED008`](./reference/codes.md#mskitshared008), [`MSKIT_SHARED009`](./reference/codes.md#mskitshared009) | warning: the csproj sets a different value of the same shape; `MSKit_SkipAudit_TargetFrameworkOverride=True` accepts it |
| [`MSKIT_SHARED010`](./reference/codes.md#mskitshared010) | error: the csproj declares both shapes |

`MSKit_GuardXmlPeekRoutine=False` and `MSKit_GuardXmlPeekAudit=False` turn off a cheap text pre-check and always parse the csproj; leave them alone unless a declaration is missed.

## Target framework constants

The TfmConstants part sets these to `True` for conditions in items, targets and `Directory.Build.targets`:

| Property | `True` when `TargetFramework` is |
| --- | --- |
| `IsNET7` … `IsNET14` | `net7.0` … `net14.0` |
| `IsNET7_OR_GREATER` … `IsNET14_OR_GREATER` | that version or later, up to `net14.0` |
| `IsNETSTANDARD20`, `IsNETSTANDARD21`, `IsNETSTANDARD` | `netstandard2.0`, `netstandard2.1`, either |
| `IsNETFRAMEWORK` | `net48` (only that one) |
| `IsNETFRAMEWORK_OR_STANDARD` | `IsNETFRAMEWORK` or `IsNETSTANDARD` |

`TargetFrameworkVersionMajor` holds `7` … `14`. They are evaluated twice: in the props phase, where only a multi-targeted inner build knows its framework, and again in the targets phase, after the csproj body, where a single `TargetFramework` is known too. A `PropertyGroup` in the csproj itself runs between the two and cannot rely on them for a single-framework project.

```xml
<ItemGroup Condition="'$(IsNET8_OR_GREATER)'=='True'">
<PackageReference Include="System.IO.Hashing" />
</ItemGroup>
```

## Global usings

Every project except analyzers and source generators gets `global using` for `System.Diagnostics.CodeAnalysis` and `System.Runtime.CompilerServices`, and `global using static` for `System.Runtime.CompilerServices.MethodImplOptions` and (not on `netstandard2.0`) `System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes`. `MSKit_IncludeCodeAnalysisGlobalUsings=False` turns them off. Add your own in `Directory.GlobalUsings.props` / `.targets` next to the solution.

A project with `ExcludeFromCodeCoverage=true` gets the [`[ExcludeFromCodeCoverage]`](https://learn.microsoft.com/dotnet/api/system.diagnostics.codeanalysis.excludefromcodecoverageattribute) assembly attribute; test projects set it by default.

## Central package versions

The parts that add package references also provide their versions: as `PackageVersion` items with [central package management](https://learn.microsoft.com/nuget/consume-packages/central-package-management), else on the `PackageReference` itself when it has no version. Each version is a property you can override, `MSKit_PackageVersion_<Package>` (lists: [Testing](./testing.md#package-versions), [Roslyn](./roslyn.md#package-versions)); `MSKit_ImplicitPackageVersions=False` turns them all off. A `PackageVersion` of yours for the same id fails restore with [`MSKIT_DUP001`](./reference/codes.md#mskitdup001) (bypass: `MSKit_SkipAudit_ImplicitPackageDuplicates=True`).

`PrivateAssets=all` is set on references to `Fody`, `ConfigureAwait.Fody`, `IgnoresAccessChecksToGenerator`, `Grpc.Tools`, `Microsoft.EntityFrameworkCore.Design` and `Microsoft.EntityFrameworkCore.Tools`, so these build-time tools never become package dependencies. Add your own `Update` items in `Directory.Packages.Metadata.targets`.

## Reference checks

**Banned and discouraged packages.** Each `MSKit_RestrictPackageReference` item names a package:

```xml
<ItemGroup>
<MSKit_RestrictPackageReference Include="Moq" Type="Error" Message="Use NSubstitute for test doubles." />
<MSKit_RestrictPackageReference Include="Newtonsoft.Json" Type="Warning" Message="Prefer System.Text.Json." />
</ItemGroup>
```

A reference to a `Type="Error"` package fails with [`MSKIT_RES001`](./reference/codes.md#mskitres001), a `Type="Warning"` one warns with [`MSKIT_RES002`](./reference/codes.md#mskitres002). `SkipGlobalRestriction="True"` on one `PackageReference` allows it (an error becomes a warning). The owner layer bans `Moq`.

**Allow-list mode.** With `MSKit_RestrictProjectReferences=True` (or `MSKit_RestrictPackageReferences`, or `MSKit_RestrictReferences` for both) every reference must carry `Allowed="True"`, else [`MSKIT_RES003`](./reference/codes.md#mskitres003) / [`MSKIT_RES004`](./reference/codes.md#mskitres004). Off by default.

**Prerelease dependencies on a stable branch.** On a stable branch a reference to a prerelease version of a package whose id starts with `MSKit_PrereleasePackagePrefix` (owner layer: `DragoAnt.`) warns with [`MSKIT_PRE001`](./reference/codes.md#mskitpre001); `MSKit_PrereleasePackageCheckAsWarning=false` makes it an error.

**`TreatWarningsAsErrors` drift.** On a developer machine a csproj that changes the shared `TreatWarningsAsErrors` fails with [`MSKIT_SHARED020`](./reference/codes.md#mskitshared020); `MSKit_SkipAudit_TreatWarningsAsErrors=True` allows it.

## Private references

The PrivateAssets part sets `PrivateAssets="All"` on every `ProjectReference` of a project with `MSKit_ProjectReferenceAsPrivateAssets=True`, and on every `PackageReference` with `MSKit_PackageReferenceAsPrivateAssets=True`, so they do not flow to projects and packages that depend on it.
66 changes: 66 additions & 0 deletions docs/customizing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Customizing

Three ways to change what the kit does without editing `.toolkit/msbuild/` (an update overwrites it): set a property, add one of the extension files the kit looks for, or hook your own file in around the kit. To use the kit for another owner, change the owner layer.

## Overriding a default

Almost every default is written as `<X Condition="'$(X)'==''">`, so the first value set wins ([load order](./parts.md#load-order)):

| Where you set it | Effect |
| --- | --- |
| `Directory.Build.props`, above the kit import | every project, before the kit's defaults — the place for repository-wide choices such as `MSKit_VersionStrategy` or `MSKit_TestsAssertions` |
| `Directory.Build.props`, below the kit import | every project, after the kit's props-phase defaults — for values the kit sets unconditionally |
| the csproj | one project; it runs after all props, so it wins except for what the kit decides in the props phase (test-project detection, Roslyn role detection, `TargetFramework` shape) |
| `-p:Name=Value` | one build; a global property beats everything |

## The owner layer

`.toolkit/msbuild/init.company.props` is loaded before every part and holds the owner's defaults; `init.company.targets` is its targets-phase twin (empty for DragoAnt).

| Setting | DragoAnt value | Note |
| --- | --- | --- |
| `ManufacturerName`, `FullManufacturerName` | `DragoAnt` | **unconditional**: set them below the kit import to change them; they feed `Authors`, `Company`, `Copyright` |
| `PackageLicenseExpression` | `MIT`, unless `PackageLicenseFile` is set | |
| `PackageIconPath` | `.toolkit/res/package.icon.png` | |
| `MSKit_PrereleasePackagePrefix` | `DragoAnt.` | [prerelease check](./build.md#reference-checks) |
| `MSKit_IsStableBranchRegex` | `^(main\|release/.+)$` | |
| `MSKit_VersionStrategy` | `ReleaseTag` | [Versioning](./versioning.md) |
| `IsPackable` | `True` | test projects and code fixes opt out |
| `TreatWarningsAsErrors` | `True` | |
| `NoWarn` | adds `CS1591;xUnit1051` | missing XML docs on public members; `CancellationToken` overloads in tests. Remove `CS1591` below the kit import to gate on XML docs |
| `WarningsNotAsErrors` | adds `NU1901;NU1902;NU1903;NU1904` | a newly disclosed vulnerability stays a warning, so the fix can be scheduled |
| `MSKit_TestingFramework`, `MSKit_TestsAssertions` | `xunit.v3`, `AwesomeAssertions` | [Testing](./testing.md) |
| `MSKit_RestrictPackageReference` | `Moq` as an error | [reference checks](./build.md#reference-checks) |

**Another owner:** fork the kit, change `kit/.toolkit/msbuild/init.company.props` (and `.targets`) and `kit/.toolkit/res/package.icon.png`, publish releases from the fork and install with `update.sh --repo <owner>/<fork>` (recorded in `kit.json`, so later updates come from the fork). No part names an owner.

## Extension files

The kit imports these from the solution folder (`MSKit_SlnFileDirectory`) when they exist:

| File | Phase | Use |
| --- | --- | --- |
| `Directory.Version.props` | props | `VersionPrefix` ([Versioning](./versioning.md)) |
| `Directory.GlobalUsings.props`, `Directory.GlobalUsings.targets` | props, targets | your own `<Using>` items |
| `Directory.Packages.Metadata.targets` | targets | `PackageReference Update` items with metadata such as `PrivateAssets` |
| `Directory.PackageAsProj.targets` | targets | the package-to-project switches ([PackageAsProj](./optional-parts.md#packageasproj)) |

## Hooks around the kit

Name your own files in `Directory.Build.props` above the kit import; each is imported only when it exists:

| Property | Imported |
| --- | --- |
| `MSKit_BeforeInitProps` | before the owner layer and every part's props |
| `MSKit_AfterInitProps` | after every part's props, the version engine included |
| `MSKit_BeforeInitTargets` | before every part's targets |
| `MSKit_AfterInitTargets` | after every part's targets and audits |

```xml
<PropertyGroup>
<MSKit_AfterInitTargets>$(MSBuildThisFileDirectory)build/after-kit.targets</MSKit_AfterInitTargets>
</PropertyGroup>
<Import Project="$(MSBuildThisFileDirectory).toolkit/msbuild/init.props" />
```

`MSKit_BeforeInitTargets` and `MSKit_AfterInitTargets` are read in the targets phase, so they can also be set in the csproj.
Loading
Loading