.NET build automation tool with semantic versioning, changelog generation, and multi-platform publishing.
- Semantic Versioning: Automatic version calculation based on commit messages and public API diff analysis
- Changelog Generation: Auto-generated CHANGELOG.md from git history with multi-level commit filtering
- License Generation: Generates LICENSE.md and COPYRIGHT.md from embedded templates
- Multi-Platform Publishing: Build and publish for Windows, Linux, and macOS (x64, x86, arm64)
- NuGet Publishing: Publish to NuGet.org, GitHub Packages, and custom feeds
- GitHub Releases: Create releases with assets, SHA256 hashes, and release notes
- Winget Manifests: Generate Windows Package Manager manifests with auto-detection
- Organization Profiles: Generate a GitHub profile README listing every repository with version, activity, and build status badges
dotnet tool install -g ktsu.KtsuBuild.Tool
ktsubuild ci --workspace .
ktsubuild build --workspace .
ktsubuild version show --workspace .In CI, prefer --tool-path over -g: the install is self-contained and the command does
not depend on the runner having the global tools directory on PATH.
dotnet tool install ktsu.KtsuBuild.Tool --tool-path ./.ktsubuild
./.ktsubuild/ktsubuild ci --workspace .git clone --depth 1 https://github.com/ktsu-dev/KtsuBuild.git /tmp/KtsuBuild
# Run the full CI/CD pipeline
dotnet run --project /tmp/KtsuBuild/KtsuBuild.Tool -- ci --workspace .
# Build only
dotnet run --project /tmp/KtsuBuild/KtsuBuild.Tool -- build --workspace .
# Show version info
dotnet run --project /tmp/KtsuBuild/KtsuBuild.Tool -- version show --workspace .All commands support these options:
| Option | Short | Description | Default |
|---|---|---|---|
--workspace |
-w |
The workspace/repository path | Current directory |
--configuration |
-c |
Build configuration (Debug/Release) | Release |
--verbose |
-v |
Enable verbose output | false |
Run the full CI/CD pipeline: metadata update, build, test, pack, publish, and release.
ktsubuild ci [options]Options:
--dry-run: Preview actions without executing them--version-bump: Force a specific version bump type (auto, patch, minor, major)--no-test: Skip the test step, for a pipeline that runs tests elsewhere--no-release: Skip the release step, leaving the release to a later step or job
Pipeline steps:
- Updates metadata files (VERSION.md, CHANGELOG.md, LICENSE.md, COPYRIGHT.md, AUTHORS.md)
- Checks version increment (skips release if
[skip ci]or no meaningful changes) - Installs dotnet-script if
.csxfiles are present - Restores NuGet packages
- Builds the solution
- Runs tests with coverage
- Packs NuGet packages (if ShouldRelease)
- Publishes executables for all platforms (if ShouldRelease)
- Generates SHA256 hashes for all artifacts
- Publishes NuGet packages to configured feeds (if ShouldRelease)
- Creates a GitHub release with assets (if ShouldRelease)
--no-test and --no-release exist so a workflow can split the pipeline across jobs without
losing the parts that only ci performs: the metadata update and commit, the repository topics,
the version gate that makes [skip ci] work, and the version, release_hash, should_release,
and build_skipped step outputs. A workflow that fans tests across a matrix runs
ktsubuild ci --no-test --no-release for everything around the tests, then ktsubuild release
once its quality gate passes.
--no-release stops this run from releasing. It does not change what the should_release output
reports, because the job that reads that output is the one performing the release.
Build workflow: restore, build, and test.
ktsubuild build [options]Options:
--no-test: Skip the test step, running restore and build only
Test project discovery and execution, for splitting a test run across several machines.
List the workspace's test projects as a single line of JSON on stdout.
ktsubuild test list [options]Output:
[{"project":"tests/Foo.Tests/Foo.Tests.csproj","platform":"neutral"},{"project":"tests/Bar.Tests/Bar.Tests.csproj","platform":"windows"}]project is a forward-slash path relative to the workspace, on every host. platform is neutral, windows, or ios. Entries are sorted by project.
The list covers every test project regardless of the current host, unlike build and ci, which test only what the host can build. A windows project appears on Linux and an ios project appears on Windows. The caller decides which host and project pairs are valid, which is what lets one machine enumerate a matrix that other machines run.
Errors are written to stdout, not stderr, so check the exit code before parsing the output. Exit code 0 means stdout holds the JSON line, and 1 means it holds an error message.
Run one test project with coverage, instead of every test project in the workspace.
ktsubuild test run --project <path> [options]Options:
--project: Path to the test project, relative to the workspace or absolute (required)--no-build: Skip building before running the tests, for a caller that has already built this project
The project isn't checked against the test list results. Pointing this at a project the host can't build, or at one that isn't a test project, fails during the test run rather than with a message naming the cause, so filter with test list first.
--no-build exists so a CI matrix can build once per platform and reuse that output across every
test cell on the same platform, instead of rebuilding the same tree in each cell. The caller has to
guarantee what the flag asserts: the project must already be built for the configuration being
tested, in the same workspace path. dotnet test --no-build reads obj/project.assets.json, and
that file holds absolute paths, so output moved between machines or paths will not resolve.
Restore, build, and test every test project the host can build, in one dotnet test invocation
across the workspace.
ktsubuild test all [options]Projects the host cannot build are skipped and named, with the reason, before anything is built. The single invocation reports every project's results itself, so there is nothing left for this command to accumulate: a failure in one project shows up in that one report rather than stopping the projects around it.
The invocation asks ktsu.Sdk to pin every project's build to the host runtime by setting
-p:KtsuHostRuntimeOnly=true, an opt-in property the Sdk turns into a per-project runtime
identifier. A workspace-wide run cannot take a runtime identifier directly: passing
-p:RuntimeIdentifier on a solution build fails with NETSDK1134, which is why the property
exists instead of the identifier itself. On a repository whose Sdk version does not know the
property, the flag is inert and the run is runtime-agnostic, exactly as it always was.
The pin is the point once it applies. Without it, a test project's output carries the native
assets for every runtime identifier its packages ship, which for a repository using the ImGui
packages is sixteen of them, Android included. Measured on ImGuiApp, the smallest test project's
output went from 115 MB to 39 MB with the pin, and its tests passed either way. That copying is
what makes a test run slow, and it costs most on Windows, where file writes are several times
slower than on Linux. An earlier version of test all paid for the pin by running dotnet test
once per project, which cost more in repeated test host startups than the copying it saved
(measured on ImGuiApp: 21.4 minutes against 22.5 unpinned on Windows, 12.7 against 8.0 on Ubuntu),
which is why the pin now rides a single invocation instead of a loop.
This is for testing, not for shipping. release still publishes for every runtime it names, and
build stays runtime-agnostic, because a solution build cannot take a runtime identifier at all.
--exclude takes a glob matched against each project's path as the solution records it, and is
repeatable. Matching projects are left out of the test run:
ktsubuild test all --exclude "**/*.UITests/*"The exclusion works by writing a solution filter and testing that, so the run stays a single
dotnet test invocation. Looping over the remaining projects instead would cost one test host
startup each, which is the trade this command already measured and rejected.
Matching is case insensitive and runs against the forward-slash form of the path, so one pattern
works whichever platform wrote the solution. ** crosses directory separators and * does not.
Every excluded project is named in the log and the closing summary counts only what ran, because a project silently dropped from a run is indistinguishable from a run that passed. A pattern matching nothing is reported too, at information level rather than as a warning, because one workflow file shared across every repository passes the same patterns everywhere and matching nothing is the ordinary case for a repository that has no such projects yet.
Use this when a suite is worth running on one platform but not on all of them. ImGuiApp excludes its UI suites on Windows: they are the entire cost of that job, and what they exercise is a managed CPU rasterizer that measures the same on both operating systems.
Release workflow: pack, publish NuGet packages, and create GitHub release.
ktsubuild release [options]Options:
--dry-run: Preview actions without executing them
release resolves the version the same way ci does, from the repository's tags and commit
history, and publishes against the current commit. It also honors the version gate, so a run whose
commits all carry [skip ci] publishes nothing.
Targeting the current commit matters in a split pipeline. When ci --no-test --no-release runs
first, it commits the updated metadata, so the current commit is the one whose VERSION.md carries
the version being published. Targeting the commit that triggered the run instead would tag a tree
that predates the bump.
Version management commands.
Display current version information including last tag, calculated version, and increment reason.
ktsubuild version show [options]Output:
Current Version: 1.2.3
Last Tag: v1.2.2
Last Version: 1.2.2
Version Increment: Patch
Reason: Found changes warranting at least a patch version
Is Prerelease: False
Calculate and display the next version number.
ktsubuild version bump [options]Create or update the VERSION.md file with the calculated version.
ktsubuild version create [options]Metadata file management commands.
Update all metadata files (VERSION.md, CHANGELOG.md, LICENSE.md, COPYRIGHT.md, AUTHORS.md, URL files).
ktsubuild metadata update [options]Options:
--no-commit: Don't commit changes after updating
Generate LICENSE.md and COPYRIGHT.md files from embedded templates.
ktsubuild metadata license [options]Generate CHANGELOG.md from git history.
ktsubuild metadata changelog [options]Windows Package Manager manifest commands.
Generate Winget manifests for a version.
ktsubuild winget generate --version <version> [options]Options:
--version,-V: The version to generate manifests for (required)--repo,-r: The GitHub repository (owner/repo)--package-id,-p: The package identifier--staging,-s: The staging directory with hashes.txt
Upload manifests to a GitHub release.
ktsubuild winget upload --version <version> [options]Options:
--version,-V: The version to upload manifests for (required)
Organization profile generation.
Generate a GitHub organization profile README. Reads every public repository in the organization, then appends an applications table and a libraries table to a template.
ktsubuild profile readme --org <organization> [options]A repository is listed once it has a stable release, so work in progress stays off the public profile.
The Ships column says what each repository produces, read from the SDK its projects declare:
| Badge | Declared by | Meaning |
|---|---|---|
lib |
plain ktsu.Sdk |
A NuGet package other projects reference |
cli |
ktsu.Sdk.ConsoleApp |
A command line program |
app |
ktsu.Sdk.App |
A windowed application |
tool |
ktsu.Sdk.Tool |
A .NET tool installed with dotnet tool install |
A repository can ship several at once. Test, benchmark, sample, example, and demo projects are
skipped, by file name and by the directories above them, so a demo application does not count as
something the repository ships. Platform SDKs such as ktsu.Sdk.Windows say which platform a
project targets rather than what kind of thing it is, so they are not shown.
The SDK column shows the version each repository pins for --sdk-package in its global.json,
green when the repository has kept up with the newest published version and yellow when it has been
left behind. Stars and Activity carry the stargazer count and the commits pushed in the last
30 days, each left blank at zero.
Options:
--org,-o: The GitHub organization to profile (required)--template,-t: The README template the tables are appended to (default:./profile/README.template)--output: Where to write the rendered README (default:./profile/README.md)--package-prefix: The NuGet package prefix, so repoExtensionsresolves toktsu.Extensions(default:ktsu)--sdk-package: The MSBuild SDK whose pinned version is reported and compared (default:ktsu.Sdk)--exclude: A repository to leave out of the tables, repeatable--only: Consider only this repository, repeatable. Useful for checking one row without regenerating the whole profile--fallback-workflow: A workflow file name to try when a repository has nodotnet.yml, repeatable
Build status comes from dotnet.yml on the default branch. A repository that names its build
workflow something else reports no status unless --fallback-workflow names it, and a fallback
logs a warning so the repository gets renamed rather than the exception living here forever.
ktsubuild profile readme --org ktsu-dev --exclude Sdk --fallback-workflow ci.ymlThe run fails if the template links a repository in the organization that is archived or no longer public. The curated lists at the top of a profile template are written by hand, so nothing else stops them promoting retired work, and the generated table below is a separate list that will not show the problem.
Failed to generate profile README: The profile template links 1 repository that is archived or no
longer public: PersistenceProvider. Remove each entry, or point it at whatever replaced it.
Requires the gh CLI to be authenticated, or GH_TOKEN to be set.
KtsuBuild determines version bumps through three methods (in order of precedence):
Use --version-bump to explicitly control the version increment:
# Force a major version bump
ktsubuild ci --version-bump major
# Force a minor version bump
ktsubuild ci --version-bump minor
# Force a patch version bump
ktsubuild ci --version-bump patch
# Use automatic detection (default)
ktsubuild ci --version-bump autoThis option is also available in GitHub Actions workflow_dispatch:
workflow_dispatch:
inputs:
version-bump:
type: choice
options: [auto, patch, minor, major]Control version increments by including tags in your commit messages:
| Tag | Effect | Example |
|---|---|---|
[major] |
Major version bump (1.0.0 -> 2.0.0) | Breaking API changes |
[minor] |
Minor version bump (1.0.0 -> 1.1.0) | New features |
[patch] |
Patch version bump (1.0.0 -> 1.0.1) | Bug fixes |
[pre] |
Prerelease bump (1.0.0 -> 1.0.1-pre.0) | Unstable changes |
[skip ci] |
Skip release entirely | Documentation-only changes |
Examples:
git commit -m "[minor] Add new authentication feature"
git commit -m "[patch] Fix null reference in user service"
git commit -m "[major] Redesign public API"
git commit -m "[skip ci] Update documentation"If no CLI option or commit tag is specified, KtsuBuild automatically determines the version bump by:
- Public API analysis: Diffs C# files for added/removed/modified public types, methods, properties, and constants. Any public API surface change triggers a minor bump.
- Commit filtering: Bot commits (dependabot, renovate, etc.) and PR merge commits are excluded from analysis.
- Fallback: Meaningful code changes default to patch; trivial changes default to prerelease.
KtsuBuild generates and maintains these files in the workspace:
| File | Purpose |
|---|---|
VERSION.md |
Contains the current version number |
CHANGELOG.md |
Complete changelog with all versions |
LATEST_CHANGELOG.md |
Changelog for the current version only (used as release notes) |
LICENSE.md |
MIT license with project URL and copyright |
COPYRIGHT.md |
Copyright notice with year range and contributors |
AUTHORS.md |
List of contributors from git history |
PROJECT_URL.url |
Windows shortcut to the project repository |
AUTHORS.url |
Windows shortcut to the organization/owner |
KtsuBuild reads these environment variables when running in CI/CD:
| Variable | Description |
|---|---|
GITHUB_TOKEN / GH_TOKEN |
GitHub API token for releases and packages |
NUGET_API_KEY |
NuGet.org API key for publishing |
KTSU_PACKAGE_KEY |
API key for ktsu.dev package feed |
GITHUB_SERVER_URL |
GitHub server URL (default: https://github.com) |
GITHUB_REF |
Git reference (branch/tag) |
GITHUB_SHA |
Git commit SHA |
GITHUB_REPOSITORY |
Repository in owner/repo format |
EXPECTED_OWNER |
Expected owner for official builds |
The build system automatically determines:
- IsOfficial: Whether the repository is the official one (not a fork, matches ExpectedOwner)
- IsMain: Whether the build is on the main branch
- IsTagged: Whether the current commit is already tagged
- ShouldRelease: Whether a release should be created (
IsMain && !IsTagged && IsOfficial)
For executable projects, KtsuBuild publishes to these runtime identifiers:
| Platform | Architectures |
|---|---|
| Windows | x64, x86, arm64 |
| Linux | x64, arm64 |
| macOS | x64, arm64 |
Each target produces a self-contained, single-file executable packaged as a ZIP archive with SHA256 hash.
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
version-bump:
description: 'Version bump type'
required: false
default: 'auto'
type: choice
options:
- auto
- patch
- minor
- major
jobs:
build:
runs-on: windows-latest
permissions:
contents: write
packages: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Clone KtsuBuild
run: git clone --depth 1 https://github.com/ktsu-dev/KtsuBuild.git "${{ runner.temp }}/KtsuBuild"
shell: bash
- name: Run CI Pipeline
id: pipeline
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
NUGET_API_KEY: ${{ secrets.NUGET_KEY }}
KTSU_PACKAGE_KEY: ${{ secrets.KTSU_PACKAGE_KEY }}
EXPECTED_OWNER: ktsu-dev
run: |
$versionBump = "${{ github.event.inputs.version-bump }}"
# Build arguments array - only add --version-bump if explicitly set (backward compatible)
$args = @("ci", "--workspace", "${{ github.workspace }}", "--verbose")
if (![string]::IsNullOrEmpty($versionBump) -and $versionBump -ne "auto") {
$args += @("--version-bump", $versionBump)
}
& dotnet run --project "${{ runner.temp }}/KtsuBuild/KtsuBuild.CLI" -- @args# Check what version would be released
ktsubuild version show
# Preview CI actions without making changes
ktsubuild ci --dry-run
# Force a specific version bump
ktsubuild ci --version-bump minor
# Build and test locally
ktsubuild build
# Update metadata files only
ktsubuild metadata update --no-commit
# Generate winget manifests
ktsubuild winget generate --version 1.0.0KtsuBuild is organized into three projects:
- KtsuBuild - Core library with all business logic, multi-targeted across .NET 5-10 and netstandard2.0/2.1
- KtsuBuild.Tool - CLI using System.CommandLine 2.0.3 with Microsoft.Extensions.DependencyInjection, packed as the
ktsu.KtsuBuild.Tool.NET tool (ktsubuildcommand) - KtsuBuild.Tests - Test suite using MSTest.Sdk with NSubstitute for mocking
All services implement interfaces from the KtsuBuild.Abstractions namespace, enabling testability and loose coupling.
This project is licensed under the MIT License - see the LICENSE.md file for details.