Speed up CI builds with Meziantou.DeltaBuild

 
 
  • Gérald Barré

In a monorepo, every commit or pull request can trigger a full rebuild and test of all projects even when only one small piece changed. Meziantou.DeltaBuild is a .NET CLI tool that solves this problem by analyzing git changes and the MSBuild dependency graph to produce a filtered solution or build file that contains only the affected projects.

It can also help with test sharding by splitting affected test projects across multiple CI agents to reduce total test time.

#Installation

Install it as a global tool:

Shell
dotnet tool install --global Meziantou.DeltaBuild

#How it works

When you run Meziantou.DeltaBuild generate, it performs the following steps:

  1. Resolve commits: Determines the base and head commits. On GitHub Actions pull_request events, it automatically reads GITHUB_BASE_REF to compute the merge base, so no extra configuration is required.
  2. Get changed files: Runs git diff --name-only between the two commits.
  3. Parse the input: Reads the solution, traversal, or project file to discover all projects.
  4. Build the project graph: Constructs the MSBuild dependency graph using the selected analysis engine.
  5. Determine affected projects: Marks a project as affected if any of its owned files (source files, imports, .props, .targets, .editorconfig, NuGet configs, etc.) appears in the changed file list.
  6. Expand transitive dependents: Walks up the dependency graph to include every project that depends on an affected project, directly or indirectly.
  7. Write output: Produces a filtered solution or build file containing only the final set of affected projects.

Your CI pipeline then builds this smaller file instead of the full solution.

Meziantou.DeltaBuild loads the input using MSBuild APIs, so it understands all the complexities of MSBuild, including SDK-style projects, multi-targeting, conditional imports, and custom build logic. It also supports various output formats (solution files, traversal projects, JSON, text) to fit different build systems.

#Basic usage

The minimal command compares HEAD against the merge base of the default remote branch. On GitHub Actions pull_request events, Meziantou.DeltaBuild automatically uses GITHUB_BASE_REF to detect the base branch, so no --base-commit argument is needed. The --input file can be a solution (.sln or .slnx) or an MSBuild traversal project (.proj). The output format is inferred from the --output extension (solution file, traversal project, JSON, or text).

Shell
Meziantou.DeltaBuild generate --input MyRepo.slnx --output DeltaBuild.slnx

Then build only the affected projects:

Shell
dotnet build DeltaBuild.slnx
dotnet test DeltaBuild.slnx

#Local development

You can also compare uncommitted changes against a branch before committing:

Shell
Meziantou.DeltaBuild generate \
  --input MyRepo.slnx \
  --output delta.slnx \
  --base-commit $(git merge-base origin/main HEAD) \
  --working-tree

The --working-tree flag includes staged, unstaged, and untracked files in the analysis.

#Test sharding for parallel CI

Meziantou.DeltaBuild can also split test projects across multiple CI agents to reduce total test time. Use --shard and --total-shards to select a subset of affected test projects:

Shell
# Agent 1
Meziantou.DeltaBuild generate \
  --input MyRepo.slnx \
  --output delta.proj \
  --test-projects-only \
  --shard 1 \
  --total-shards 3

# Agent 2
Meziantou.DeltaBuild generate \
  --input MyRepo.slnx \
  --output delta.proj \
  --test-projects-only \
  --shard 2 \
  --total-shards 3

# Agent 3
Meziantou.DeltaBuild generate \
  --input MyRepo.slnx \
  --output delta.proj \
  --test-projects-only \
  --shard 3 \
  --total-shards 3

The --shard-separate option lets you pin specific projects to different shards. This is useful when you have a slow integration test project that should never share a shard with another slow test project:

Shell
Meziantou.DeltaBuild generate \
  --input MyRepo.slnx \
  --output delta.proj \
  --test-projects-only \
  --shard 1 \
  --total-shards 3 \
  --shard-separate tests/Smoke/Smoke.Tests.csproj \
  --shard-separate tests/Integration/Integration.Tests.csproj

Listed projects are spread across shards in declaration order (round-robin), so they end up in separate shards when possible.

#Full and hierarchical rebuild triggers

Some file changes should always trigger a full rebuild regardless of project ownership. Use --full-rebuild-trigger with glob patterns:

Shell
Meziantou.DeltaBuild generate \
  --input MyRepo.slnx \
  --output delta.slnx \
  --full-rebuild-trigger "**/.github/workflows/**"

For files that should only trigger a rebuild within their folder subtree, use --hierarchical-rebuild-trigger. By default, global.json, nuget.config, and .editorconfig are hierarchical triggers. For example, changing src/nuget.config will only rebuild projects under src/.

#Output formats

The output format is inferred from the file extension of --output:

ExtensionFormat
.slnVisual Studio solution (v12)
.slnxXML-based solution (Visual Studio 2022+)
.projMSBuild Traversal SDK project
.jsonJSON array of affected project paths
.txtOne project path per line

#Additional resources

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?