ModularPipelines, a CI framework where the pipeline is a C# module graph
Write your pipelines in C# . | | ModularPipelines.Java | Helpers for interacting with Java build tools (Maven, Gradle).
At a glance
- What is it?
- ModularPipelines replaces YAML with classes you can attach a debugger to. What the attributes, analyzers, wrapper packages, and versioning setup actually give you.
- Who is it for?
- ModularPipelines earns its place when a team already writes C#, already has shared build logic it keeps copying between YAML files, and wants a pipeline it can run and step through before it reaches a hosted agent. It costs you a package dependency per external CLI you touch, a private build of the repository, and a release train that tags infrequently, so a shop with one small pipeline will feel the overhead more than the benefit.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly C#, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.
Star history
- thomhurst/ModularPipelines560 ★ · +19
| Repository | 2026-07-31 | 2026-11-13 |
|---|---|---|
| thomhurst/ModularPipelines | 541 | 560 |
19 Stars in the last 105 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
Two install paths, and only one creates a new project
There are two ways in, and they hand you different amounts of code. The template path installs the template pack and scaffolds a whole pipeline project:
dotnet new install ModularPipelines.TemplatesThe generated project arrives with separate restore, build, test, and publish modules, explicit dependencies between them, and configurable paths, and it can be run with `dotnet run` before it is wired to any hosted runner. If you would rather add pipeline modules to a project you already have, you install the core framework and the .NET CLI integration instead:
dotnet add package ModularPipelines
dotnet add package ModularPipelines.DotNetThat second path leaves the wiring to you, and the wiring is three lines in `Program.cs`.
Dependencies are attributes, not a schedule you maintain
A module is a class that derives from `Module<T>`, and the scheduler reads the dependency graph off the type rather than from a file you edit. `[DependsOn<BuildModule>]` on `PublishModule` is the whole declaration, and anything not declared as a dependency is free to run at the same time as anything else. Results travel as typed return values, so a module that needs what another one produced takes it as a parameter:
public class BuildModule : Module<BuildInfo>
{
protected override async Task<BuildInfo> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
await context.Tools.DotNet.BuildAsync(
new DotNetBuildOptions { ProjectSolution = "MyApp.csproj" },
cancellationToken: cancellationToken);
return new BuildInfo { Version = "1.0.0", OutputPath = "bin/Release" };
}
}The design point is that modules hand each other values rather than sharing mutable state.
Roslyn analyzers reject four specific mistakes at compile time
The compile-time safety claim rests on four analyzers, and each one targets a failure that YAML cannot catch. A missing `[DependsOn]` on a `GetModule<T>()` call is one, so a module cannot silently read a result that nothing guarantees has run yet. Circular dependencies are another, which means the graph the scheduler walks is checked before it is walked at all. The other two are a forgotten `await` on a module result and a `Console.Write` where the logging system belongs. Because these are Roslyn analyzers rather than runtime checks, they surface as errors in the IDE and in the build, not as a red step in a CI log.
Secrets are obfuscated in log output, not just read from a store
The secret handling claim is about output, not storage. Secrets are automatically obfuscated in logs, so a module that prints its context does not print a key. That is a different guarantee from never writing a secret to disk, and it is worth keeping straight when you decide where your values come from. On the injection side the pipeline is built the way an ASP.NET Core application is, which means services and configuration arrive through the container and a test can substitute a fake for a real dependency rather than rearranging environment variables. The host itself is three lines:
var builder = Pipeline.CreateBuilder(args);
await builder.RunAsync();`Pipeline.CreateBuilder(args)` takes the process arguments, so a hosted runner's own command line passes through to the builder.
One wrapper package per CLI, and a script that updates them all
The integration surface is a flat list of small packages, each wrapping one external command line: AmazonWebServices, Ansible, ArgoCd, Azure, Azure.Pipelines for agents, Chocolatey, Cmd, Cosign for container signing, and Docker. A separate Java package covers the Maven and Gradle build tools. Each one ships as its own NuGet package, which means adopting a tool means adding a dependency, and the count of those dependencies is what the `update-all-services.ps1` script at the repository root exists to manage. Version numbers for the whole set are centralised in `Directory.Packages.props` rather than repeated in each project, and `renovate.json` sits next to it for automated bumps.
Version numbers come from Git tags, and the newest tag is months behind the branch
There is no version literal to read in the README, because `GitVersion.yml` sits at the root and derives the number from tags. The three tagged releases are 3.2.8 on 2026-04-17, 3.1.90 on 2026-02-07, and 3.1.6 on 2026-01-19, and the most recent push to the default branch main is 2026-09-25. So the branch has moved well past the newest tag, and there is a version-4 notes file sitting next to the version-3 one without a matching release. If you pin a package version rather than tracking the branch, you are pinning code from April.
Four solution files keep analyzers, examples, and tests separate
The root is laid out for a large multi-project build rather than a single solution. `ModularPipelines.All.slnx` is the aggregate, `ModularPipelines.slnx` is the main one, and `ModularPipelines.Analyzers.slnx` and `ModularPipelines.Examples.slnx` split the analyzer and example projects out, with `ModularPipelines.Tests.slnf` as a solution filter over the tests. Alongside them sit `Directory.Build.props` for shared build settings, `global.json` for SDK pinning, `stylecop.json` and a `.ruleset` for style, and `BuildSolutions.txt` alongside a `.sln.DotSettings` file for the IDE. Two release-notes files and a `README_Template.md` suggest the documentation and release process is scripted from the repository itself.
The publish call is the point where the debugger pays off
The example the README leads with is a publish module, and it is chosen because it has options worth getting wrong:
return await context.Tools.DotNet.PublishAsync(new DotNetPublishOptions
{
ProjectSolution = "src/MyApp/MyApp.csproj",
Configuration = "Release",
Output = "publish/"
}, cancellationToken: cancellationToken);Those three properties are an object, so a typo in one is a compile error rather than a silently ignored line. The vendor lock-in argument rests on the same property: the module holds the logic, and the tool call is a library call, so moving from GitHub Actions to Azure Pipelines to TeamCity is a change to the runner rather than a rewrite of the steps. The cost is that every external tool now has to exist as a wrapper package before you can call it.
Editorial conclusion
ModularPipelines earns its place when a team already writes C#, already has shared build logic it keeps copying between YAML files, and wants a pipeline it can run and step through before it reaches a hosted agent. It costs you a package dependency per external CLI you touch, a private build of the repository, and a release train that tags infrequently, so a shop with one small pipeline will feel the overhead more than the benefit. Before adopting it, open a solution locally, confirm the analyzers fire on a deliberate mistake, and check that the wrapper package for each tool you already use exists.
Frequently asked questions
Do I need a ModularPipelines template or can I add it to an existing project?
Both work. The template pack scaffolds a project with separate restore, build, test, and publish modules. For an existing project you add the ModularPipelines and ModularPipelines.DotNet packages, then build and run the pipeline from Program.cs.
How does ModularPipelines decide what can run in parallel?
Modules declare dependencies with DependsOn attributes and the framework works out the rest. Anything not declared as a dependency of something else is free to run at the same time, so there is no separate parallel job configuration to maintain.
What mistakes do the ModularPipelines Roslyn analyzers catch?
Four: a missing DependsOn when calling GetModule, circular dependencies between modules, a forgotten await on a module result, and using Console.Write instead of the logging system. They are analyzers, so they fail the build rather than failing at run time.
How does ModularPipelines handle secrets?
Secrets are automatically obfuscated in log output, so a printed context does not leak a key. That is a logging guarantee, and the project says nothing more about where the secret values themselves are stored.
What is the newest ModularPipelines release?
The three most recent tags are 3.2.8 on 2026-04-17, 3.1.90 on 2026-02-07, and 3.1.6 on 2026-01-19. Versions come from GitVersion.yml rather than a literal in the README, and the default branch main was last pushed to on 2026-09-25.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/thomhurst-modularpipelines)Star history chart for your README
[](https://hysenlabs.com/projects/thomhurst-modularpipelines)Was this useful?