Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
79 changes: 79 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,82 @@ jobs:
path: |
dist/selftest
dist/kit

manager:
name: Manager (${{ matrix.os }})
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- windows-latest
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
working-directory: manager
env:
TOOL_VERSION: 0.0.0-ci.${{ github.run_number }}
OUT: ../dist/manager
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0

- name: Install .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: |
8.0.x
10.0.x

- name: Build
run: dotnet build DragoAnt.MSBuildKit.Manager.slnx -c Release -nologo

- name: Test
run: |
mkdir -p "$OUT"
dotnet test --solution DragoAnt.MSBuildKit.Manager.slnx -c Release --no-build \
--coverage --coverage-output-format cobertura --report-trx --report-xunit-junit \
--results-directory "$OUT/TestResults" 2>&1 | tee "$OUT/test.log"

- name: Prove the tests ran on Microsoft.Testing.Platform
run: |
grep -q 'Test run summary:' "$OUT/test.log" || { echo "dotnet test did not run on Microsoft.Testing.Platform (no MTP summary in the log)"; exit 1; }
echo "dotnet test ran on Microsoft.Testing.Platform (manager/global.json test.runner)" >> "$GITHUB_STEP_SUMMARY"

- name: Coverage gate (70 %)
run: |
files=$(find "$OUT/TestResults" -name '*.cobertura.xml')
[ -n "$files" ] || { echo "no cobertura report"; exit 1; }
for f in $files; do
rate=$(tr -d '\r' < "$f" | sed -n 's/.*<coverage line-rate="\([0-9.]*\)".*/\1/p' | head -n 1)
awk -v r="$rate" -v f="$f" 'BEGIN { printf "line coverage %.1f %% in %s\n", r * 100, f; exit !(r >= 0.70) }'
done

- name: Pack the tool
run: dotnet pack src/DragoAnt.MSBuildKit.Manager/DragoAnt.MSBuildKit.Manager.csproj -c Release -nologo -o "$OUT/feed" -p:Version="$TOOL_VERSION"

- name: Install and run the packed tool
run: |
dotnet tool install --tool-path "$OUT/tools" --add-source "$OUT/feed" DragoAnt.MSBuildKit.Manager --version "$TOOL_VERSION"
"$OUT/tools/mskit-manager" --help
"$OUT/tools/mskit-manager" status --json > "$OUT/status.json"
cat "$OUT/status.json"
tr -d '\r' < "$OUT/status.json" | grep -q "\"version\": \"$TOOL_VERSION\"" || { echo "status --json did not report $TOOL_VERSION"; exit 1; }

- name: Test results
if: always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)
uses: dorny/test-reporter@a43b3a5f7366b97d083190328d2c652e1a8b6aa2 # v3.0.0
with:
name: Manager tests (${{ matrix.os }})
path: dist/manager/TestResults/*.trx
reporter: dotnet-trx

- name: Upload artifacts
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: manager-${{ matrix.os }}
path: dist/manager
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project are documented here. The format follows [Kee

## [Unreleased]

### Added

- `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.

## [0.2.1] - 2026-10-05

### Added
Expand Down
9 changes: 9 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Issues and pull requests are welcome.
- `samples/MinimalLibrary` is a consumer with a library and a test project; its `.toolkit/` is installed by the self-test and not committed.
- `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.

## Build and test

Expand All @@ -19,6 +20,14 @@ 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.

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

```sh
cd manager && dotnet test --solution DragoAnt.MSBuildKit.Manager.slnx
```

CI's `manager` job also packs the tool, installs it into a tool path and runs `mskit-manager --help` and `status --json`.

## Changing the kit

- A new property defaults with `Condition="'$(Name)'==''"`, so a consumer's value always wins, and gets a row in the README.
Expand Down
6 changes: 6 additions & 0 deletions manager/.toolkit/kit.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"repository": "DragoAnt/MSBuildKit",
"version": "0.0.0-local",
"sha256": "",
"parts": []
}
20 changes: 20 additions & 0 deletions manager/.toolkit/kit.parts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# DragoAnt.MSBuildKit parts: <name> <default|optional> [required parts...]
# The folder of a part is msbuild/DragoAnt.MSBuildKit.<name> ("Trunk" is msbuild/DragoAnt.MSBuildKit).
Core default
Trunk default
Vcs.GitHub default
TfmConstants default
Packaging default
Testing default
Testing.XUnit.v3 default Testing
Locals.Compile default
Locals.DirectorySecrets default
Locals.Secrets default
PrivateAssets default
Project.RoslynComponent optional
Project.CodeAnalyzer optional Project.RoslynComponent
Project.CodeFixer optional Project.RoslynComponent Project.CodeAnalyzer
Project.SourceGenerator optional Project.RoslynComponent
PackageAsProj optional
ProjMetadata optional
EF optional
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project>

<!-- MSKIT_CORE001: the project name matched more than one auto-detect regex. -->
<Target Name="ValidateAutoDetectedProjectTypeMutex"
BeforeTargets="CoreCompile;Build"
Condition="@(MSKit_AutoDetectedProjectType-&gt;Count()) &gt; 1">
<PropertyGroup>
<_MSKit_AutoDetectedTypes>@(MSKit_AutoDetectedProjectType, ', ')</_MSKit_AutoDetectedTypes>
</PropertyGroup>
<Error Code="MSKIT_CORE001"
File="$(MSBuildProjectFullPath)"
Text="Project '$(MSBuildProjectName)' auto-detected as MULTIPLE project types: $(_MSKit_AutoDetectedTypes).%0AA project can be exactly ONE type.%0AFix ONE of:%0A 1. Rename the project so its name matches only ONE auto-detect regex%0A 2. Override the conflicting MSKit_*ProjectNameRegex in Directory.Build.props to exclude this project name%0A 3. Set the corresponding MSKit_Disable*AutoDetect=true in the csproj and import the explicit endpoint instead (e.g. MSKit_DisableCodeAnalyzerAutoDetect=true + Import &quot;$(CodeAnalyzerCommonPropsPath)&quot;)" />
</Target>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project>

<!-- MSKIT_ROSLYN001..003: a project name matched a Roslyn auto-detect regex, but the kit part
that wires that project type is not installed. -->

<Target Name="ValidateCodeAnalyzerSatelliteAttached"
BeforeTargets="CoreCompile;Build"
Condition="'$(MSKit_IsCodeAnalyzerAutoDetected)'=='True'
AND '$(MSKit_CodeAnalyzerSatelliteAttached)'!='true'">
<Error Code="MSKIT_ROSLYN001"
File="$(MSBuildProjectFullPath)"
Text="Project '$(MSBuildProjectName)' name matches the CodeAnalyzer auto-detect regex '$(_MSKit_CodeAnalyzerProjectNameRegex_Shared)' but the kit part 'DragoAnt.MSBuildKit.Project.CodeAnalyzer' is not installed.%0AFix: pwsh .toolkit/update.ps1 -Add Project.CodeAnalyzer (or: sh .toolkit/update.sh --add Project.CodeAnalyzer)%0AOr, if this project is not a Roslyn analyzer, rename it or set MSKit_DisableCodeAnalyzerAutoDetect=true in the csproj." />
</Target>

<Target Name="ValidateCodeFixerSatelliteAttached"
BeforeTargets="CoreCompile;Build"
Condition="'$(MSKit_IsCodeFixerAutoDetected)'=='True'
AND '$(MSKit_CodeFixerSatelliteAttached)'!='true'">
<Error Code="MSKIT_ROSLYN002"
File="$(MSBuildProjectFullPath)"
Text="Project '$(MSBuildProjectName)' name matches the CodeFixer auto-detect regex '$(_MSKit_CodeFixerProjectNameRegex_Shared)' but the kit part 'DragoAnt.MSBuildKit.Project.CodeFixer' is not installed.%0AFix: pwsh .toolkit/update.ps1 -Add Project.CodeFixer (or: sh .toolkit/update.sh --add Project.CodeFixer)%0AOr, if this project is not a Roslyn code-fix provider, rename it or set MSKit_DisableCodeFixerAutoDetect=true in the csproj." />
</Target>

<Target Name="ValidateSourceGeneratorSatelliteAttached"
BeforeTargets="CoreCompile;Build"
Condition="'$(MSKit_IsSourceGeneratorAutoDetected)'=='True'
AND '$(MSKit_SourceGeneratorSatelliteAttached)'!='true'">
<Error Code="MSKIT_ROSLYN003"
File="$(MSBuildProjectFullPath)"
Text="Project '$(MSBuildProjectName)' name matches the SourceGenerator auto-detect regex '$(_MSKit_SourceGeneratorProjectNameRegex_Shared)' but the kit part 'DragoAnt.MSBuildKit.Project.SourceGenerator' is not installed.%0AFix: pwsh .toolkit/update.ps1 -Add Project.SourceGenerator (or: sh .toolkit/update.sh --add Project.SourceGenerator)%0AOr, if this project is not a Roslyn source generator, rename it or set MSKit_DisableSourceGeneratorAutoDetect=true in the csproj." />
</Target>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project>

<PropertyGroup>
<MSKit_Diagnostic Condition="'$(MSKit_Diagnostic)'==''">false</MSKit_Diagnostic>
<!-- True on a developer machine; the Vcs.GitHub part flips it to False on GitHub Actions. -->
<MSKit_IsDevEnv Condition="'$(MSKit_IsDevEnv)'==''">True</MSKit_IsDevEnv>

<!-- A Version that exists before the kit loads (-p:Version=... or Directory.Build.props
above the import) is the caller's decision: the version engine then renders nothing. -->
<MSKit_ExplicitVersion Condition="'$(MSKit_ExplicitVersion)'==''">$(Version)</MSKit_ExplicitVersion>

<MSKit_ImplicitPackageVersions Condition="'$(MSKit_ImplicitPackageVersions)'==''">True</MSKit_ImplicitPackageVersions>

<MSKit_Templates Condition="'$(MSKit_Templates)'==''">$(MSBuildThisFileDirectory)../../.local/</MSKit_Templates>

<MSKit_SlnFileDirectory Condition="'$(MSKit_SlnFileDirectory)'=='' AND '$(SlnxFilePath)'!=''">$([System.IO.Path]::GetDirectoryName('$(SlnxFilePath)').Replace('\', '/').TrimEnd('/'))/</MSKit_SlnFileDirectory>
<MSKit_SlnFileDirectory Condition="'$(MSKit_SlnFileDirectory)'==''">$(MSBuildThisFileDirectory)../../../</MSKit_SlnFileDirectory>
<MSKit_SlnFileDirectory>$([System.IO.Path]::GetFullPath('$(MSKit_SlnFileDirectory)').Replace('\', '/').TrimEnd('/'))/</MSKit_SlnFileDirectory>

<MSKit_SlnFileName Condition="'$(MSKit_SlnFileName)'=='' AND '$(SlnxFilePath)'!=''">$([System.IO.Path]::GetFileNameWithoutExtension('$(SlnxFilePath)'))</MSKit_SlnFileName>

<MSKit_ToolkitMSBuildDir Condition="'$(MSKit_ToolkitMSBuildDir)'==''">$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)../').Replace('\', '/').TrimEnd('/'))/</MSKit_ToolkitMSBuildDir>
<MSKit_ToolkitDir Condition="'$(MSKit_ToolkitDir)'==''">$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)../../').Replace('\', '/').TrimEnd('/'))/</MSKit_ToolkitDir>

<_ProjectObjDir>$(BaseIntermediateOutputPath)</_ProjectObjDir>
<_ProjectObjDir Condition="'$(_ProjectObjDir)'==''">obj</_ProjectObjDir>
<MSKit_ProjectObjDir Condition="$([System.IO.Path]::IsPathRooted('$(_ProjectObjDir)'))">$(_ProjectObjDir)</MSKit_ProjectObjDir>
<MSKit_ProjectObjDir Condition="'$(MSKit_ProjectObjDir)'==''">$([System.IO.Path]::GetFullPath('$(MSBuildProjectDirectory)/$(_ProjectObjDir)'))</MSKit_ProjectObjDir>
<MSKit_ProjectObjDir>$([System.IO.Path]::GetFullPath('$(MSKit_ProjectObjDir)').TrimEnd('/').Replace('\', '/'))</MSKit_ProjectObjDir>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project>

<PropertyGroup>
<MSKit_LocalSecretsBaseDirectory Condition="'$(MSKit_LocalSecretsBaseDirectory)'=='' AND $([MSBuild]::IsOsPlatform('Windows'))">$([System.Environment]::GetFolderPath(SpecialFolder.ApplicationData))\Microsoft\UserSecrets\</MSKit_LocalSecretsBaseDirectory>
<MSKit_LocalSecretsBaseDirectory Condition="'$(MSKit_LocalSecretsBaseDirectory)'=='' AND ($([MSBuild]::IsOsPlatform('Linux')) OR $([MSBuild]::IsOsPlatform('OSX')))">$([System.Environment]::GetFolderPath(SpecialFolder.UserProfile))/.microsoft/usersecrets/</MSKit_LocalSecretsBaseDirectory>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<Project>

<!-- UserSecretsId is set in the csproj, after every props file, so this lives in targets. -->
<PropertyGroup Condition="'$(UserSecretsId)'!=''">
<LocalSecretsDir Condition="'$(LocalSecretsDir)'==''">$(MSKit_LocalSecretsBaseDirectory)$(UserSecretsId)</LocalSecretsDir>
</PropertyGroup>

</Project>
29 changes: 29 additions & 0 deletions manager/.toolkit/msbuild/DragoAnt.MSBuildKit.Core/git.root.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project>

<!-- GitHub Actions checks out tags and pull requests detached, so .git/HEAD holds a SHA there:
the CI ref name wins over the local HEAD whenever Actions is running. -->
<PropertyGroup Condition="'$(MSKit_Branch)'=='' AND '$(GITHUB_ACTIONS)'=='true' AND '$(GITHUB_HEAD_REF)'!=''">
<MSKit_Branch>$(GITHUB_HEAD_REF)</MSKit_Branch>
</PropertyGroup>
<PropertyGroup Condition="'$(MSKit_Branch)'=='' AND '$(GITHUB_ACTIONS)'=='true' AND '$(GITHUB_REF_NAME)'!=''">
<MSKit_Branch>$(GITHUB_REF_NAME)</MSKit_Branch>
</PropertyGroup>

<PropertyGroup>
<MSKit_GitRoot Condition="'$(MSKit_GitRoot)'==''">$([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)', '.git'))</MSKit_GitRoot>
<MSKit_GitRoot Condition="'$(MSKit_GitRoot)'==''">$([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)', '.git/HEAD'))</MSKit_GitRoot>
</PropertyGroup>

<PropertyGroup Condition="'$(MSKit_GitRoot)'!=''">
<_MSKit_GitHeadFile Condition="Exists('$(MSKit_GitRoot)/.git/HEAD')">$(MSKit_GitRoot)/.git/HEAD</_MSKit_GitHeadFile>
<!-- In a git worktree .git is a file pointing at the worktree's own git dir. -->
<_MSKit_GitDirPointer Condition="'$(_MSKit_GitHeadFile)'=='' AND Exists('$(MSKit_GitRoot)/.git')">$([System.IO.File]::ReadAllText('$(MSKit_GitRoot)/.git').Trim().Replace('gitdir: ', ''))</_MSKit_GitDirPointer>
<_MSKit_GitDirPointer Condition="'$(_MSKit_GitDirPointer)'!='' AND !$([System.IO.Path]::IsPathRooted('$(_MSKit_GitDirPointer)'))">$(MSKit_GitRoot)/$(_MSKit_GitDirPointer)</_MSKit_GitDirPointer>
<_MSKit_GitHeadFile Condition="'$(_MSKit_GitDirPointer)'!='' AND Exists('$(_MSKit_GitDirPointer)/HEAD')">$(_MSKit_GitDirPointer)/HEAD</_MSKit_GitHeadFile>
</PropertyGroup>

<PropertyGroup Condition="'$(MSKit_Branch)'=='' AND '$(_MSKit_GitHeadFile)'!=''">
<MSKit_Branch>$([System.IO.File]::ReadAllText('$(_MSKit_GitHeadFile)').Trim().Replace('ref: refs/heads/', ''))</MSKit_Branch>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project>

<PropertyGroup>
<MSKit_Branch Condition="'$(MSKit_Branch)'=='' AND '$(GITHUB_REF_NAME)'!=''">$(GITHUB_REF_NAME)</MSKit_Branch>
<MSKit_Branch Condition="'$(MSKit_Branch)'==''">unknown-branch</MSKit_Branch>
<MSKit_CommitSha Condition="'$(MSKit_CommitSha)'==''"></MSKit_CommitSha>
<MSKit_IsStableBranchRegex Condition="'$(MSKit_IsStableBranchRegex)'==''">^(main|master|release/.+)$</MSKit_IsStableBranchRegex>
<MSKit_IsStableBranch>false</MSKit_IsStableBranch>
<MSKit_IsStableBranch Condition="$([System.Text.RegularExpressions.Regex]::IsMatch($(MSKit_Branch), $(MSKit_IsStableBranchRegex), RegexOptions.IgnoreCase))">true</MSKit_IsStableBranch>
</PropertyGroup>

</Project>
10 changes: 10 additions & 0 deletions manager/.toolkit/msbuild/DragoAnt.MSBuildKit.Core/init.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project>

<Import Project="$(MSBuildThisFileDirectory)core.common.props" />
<Import Project="$(MSBuildThisFileDirectory)git.root.props" />
<Import Project="$(MSBuildThisFileDirectory)git.routine.props" />
<Import Project="$(MSBuildThisFileDirectory)tfm.routine.props" />
<Import Project="$(MSBuildThisFileDirectory)core.locals.props" />
<Import Project="$(MSBuildThisFileDirectory)project-types.autodetect.props" />

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Project>

<Import Project="$(MSBuildThisFileDirectory)core.locals.targets" />

</Project>
Loading
Loading