Self-hosted service
thomhurst/ModularPipelines avatar
thomhurst/ModularPipelines

ModularPipelines: writing CI/CD pipelines as C# modules

Write your pipelines in C# . | | ModularPipelines.Java | Helpers for interacting with Java build tools (Maven, Gradle).

552 stars23 forksC#MIT

At a glance

What is it?
A C# framework that replaces vendor YAML with compiled modules, dependency attributes and local debugging. The trade-off is that your build now needs the .NET SDK and a NuGet restore.
Who is it for?
Adopt ModularPipelines if your build is already .NET and your team is tired of debugging YAML through a ten-minute agent round trip. Skip it if your pipeline must run in a minimal container without the .NET SDK, or if non-C# contributors own the pipeline files.
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 last received commits 4 days ago.
What is it written in?
Mainly C#, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 25, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem ModularPipelines targets: YAML feedback loops

The README states the complaint plainly: YAML pipelines are impossible to debug locally, have no compile-time safety, encourage copy-paste reuse, and lock you to a vendor. Each of those is a different failure. The local debugging one is the most expensive in practice. When a variable name is misspelled in a YAML step, you learn about it after a push, a queue wait and an agent start, not in your editor. ModularPipelines moves that class of error into the C# compiler, which runs before anything is pushed.

The audience is narrower than the slogan suggests. This is for teams whose applications are already .NET or who are willing to add the .NET SDK to their build image, and who want pipeline logic to live under the same review, refactoring and testing rules as application code. If your repository is a Python service with three shell steps, the framework adds a runtime dependency without removing much pain.

How modules, dependency attributes and typed results fit together

A pipeline is a set of classes deriving from Module or Module<T>, where T is the type the module returns. Each module overrides ExecuteAsync and receives an IModuleContext plus a CancellationToken. The context exposes tool wrappers such as context.Tools.DotNet, so a build step is a method call rather than a shell string.

Ordering is declared, not scripted. The DependsOn<T> attribute on a class tells the framework that this module must wait for another. The README states that ModularPipelines then works out what can run in parallel, so the graph is derived from the attributes rather than from a hand-written stage list. Data moves through return values: a module returning BuildInfo can be consumed by a module that declares DependsOn<BuildModule> and calls GetModule<T>(). The README describes this as strongly-typed results with no shared mutable state.

The framework also ships Roslyn analyzers. The README lists four things they catch: a missing DependsOn when calling GetModule<T>(), circular dependencies between modules, a forgotten await on a module result, and Console.Write used instead of the logging system. That last one matters because the README states secrets are automatically obfuscated in logs, which only holds if output goes through the framework's logging path.

Installing ModularPipelines and running a first pipeline

The README gives a template-based quick start. Installing the template package and generating a project produces separate restore, build, test and publish modules with explicit dependencies and configurable paths.

bash
dotnet new install ModularPipelines.Templates
dotnet new modularpipeline -n MyPipeline \
  --solution ../MySolution.slnx \
  --publish-project ../src/MyApp/MyApp.csproj
cd MyPipeline
dotnet run

After dotnet run, the README says you get console progress while the pipeline executes and a results summary when it finishes. The generated project is the fastest way to see the module graph in a working state, and the README points to the template source directory for a complete copy-ready example.

If you are adding modules to an existing project instead of generating one, the README installs the core framework plus the .NET CLI integration used by the examples:

bash
dotnet add package ModularPipelines
dotnet add package ModularPipelines.DotNet

You then configure and execute the pipeline from Program.cs. The README's example is four lines: a using for ModularPipelines, a using for ModularPipelines.Extensions, a call to Pipeline.CreateBuilder(args), and an awaited builder.RunAsync().

csharp
using ModularPipelines;
using ModularPipelines.Extensions;

var builder = Pipeline.CreateBuilder(args);
await builder.RunAsync();

A module is a class with an attribute and an override. The README's publish example declares its prerequisites on the class and returns a CommandResult:

csharp
[DependsOn<BuildModule>]
[DependsOn<TestModule>]
public class PublishModule : Module<CommandResult>
{
    protected override async Task<CommandResult> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
    {
        return await context.Tools.DotNet.PublishAsync(new DotNetPublishOptions
        {
            ProjectSolution = "src/MyApp/MyApp.csproj",
            Configuration = "Release",
            Output = "publish/"
        }, cancellationToken: cancellationToken);
    }
}

The README's point about this snippet is that it is ordinary C#: you can set a breakpoint inside ExecuteAsync, inspect variables, and step through the pipeline on your machine before pushing. Note that the option names shown (ProjectSolution, Configuration, Output) come from the README example; check the wrapper's own options type for the full set rather than assuming a flag exists.

Where ModularPipelines is the wrong tool

The framework runs your pipeline as a .NET program, so the machine executing it needs the .NET SDK and a package restore. That is a real cost in a locked-down build environment, in an air-gapped runner, or in a slim container where you deliberately keep the toolchain minimal. YAML has no such prerequisite because the agent already understands it.

There is also a bootstrapping problem. If the pipeline is what builds your .NET application, and the pipeline itself is a .NET application, you have to decide how the pipeline gets built and published before it can build anything else. The README does not document that bootstrap path, and it does not document rollback behaviour for a failed pipeline run. Treat those as open questions to answer against your own infrastructure, not as features you can assume.

Finally, the framework is a layer over the CLIs you already call. It does not replace Maven, Gradle, Docker or dotnet; the integration packages wrap them. If a tool you depend on has no wrapper in the package table, you are back to invoking a process yourself, and the typed-options benefit disappears for that step.

How ModularPipelines differs from YAML-based CI systems

The direct comparison is with GitHub Actions, Azure Pipelines and similar YAML-driven systems. In those, the pipeline definition is data interpreted by the provider's agent, and the provider owns the schema, the expression syntax and the execution model. Reuse means composite actions or templates, and portability means rewriting them.

ModularPipelines inverts that. The definition is a compiled program, the execution model is a dependency graph derived from attributes, and the provider becomes a thin trigger that runs dotnet run. The README frames the migration benefit as changing one line when moving between GitHub Actions, Azure Pipelines and TeamCity, with the modules unchanged.

That is a genuine difference in approach, but it is not free. You gain compile-time checking and local debugging; you take on ownership of a program, its dependencies, its NuGet restore and its runtime. The vendor-neutrality claim is also bounded by the integration packages: if your pipeline leans heavily on provider-specific features such as matrix expansion or native artifact attestation, those still belong to the provider, and the framework does not claim to reproduce them.

Maintenance, licensing and upgrade cost

The repository is not archived. Its last push was on 2026-04-17, which is more than six months before today's date, so the project should not be described as actively maintained on the strength of that timestamp alone. The release history shows v3.2.8 on 2026-04-17, v3.1.90 on 2026-02-07 and v3.1.6 on 2026-01-19, so the cadence in early 2026 was frequent and then stopped at the point of the last push.

That matters for upgrade planning in a specific way. The repository contains both RELEASE_NOTES_V3.md and RELEASE_NOTES_V4.md at the top level, which indicates a major version transition is in progress or planned. Major versions of a framework that your build depends on are the expensive kind: the pipeline is the thing that produces your artifacts, so a breaking change there blocks everything downstream. Pin the ModularPipelines package versions and read the V4 notes before moving.

The licence is MIT, which permits commercial and private use and modification. Integration packages are separate NuGet packages, so each one you add is a dependency you are choosing; the README's table lists them individually. This is a description of the licence text, not legal advice.

Editorial conclusion

Adopt ModularPipelines if your build is already .NET and your team is tired of debugging YAML through a ten-minute agent round trip. Skip it if your pipeline must run in a minimal container without the .NET SDK, or if non-C# contributors own the pipeline files. Before committing, run the quick start template, confirm that your CI provider can execute a dotnet run step, and check that the tool wrappers you depend on exist in the package table, since the framework is only as useful as the integrations it ships.

Frequently asked questions

What is ModularPipelines and what does it do?

It is a C# framework for writing CI/CD pipelines as code instead of YAML. Pipelines are composed of modules that declare dependencies with attributes, run in parallel where the graph allows, and return strongly-typed results other modules can consume.

How do I install ModularPipelines and create a first pipeline?

The README installs the template package with dotnet new install ModularPipelines.Templates, then generates a project with dotnet new modularpipeline, passing a solution and a publish project. Running dotnet run in the generated directory executes the pipeline. For an existing project, add the ModularPipelines and ModularPipelines.DotNet packages and call Pipeline.CreateBuilder(args) followed by RunAsync in Program.cs.

Can I debug a ModularPipelines pipeline locally?

Yes. The README states the pipeline is regular C# code, so you can set a breakpoint inside a module's ExecuteAsync method, step through it and inspect variables on your own machine before pushing.

How does ModularPipelines handle secrets in logs?

The README states that secrets are automatically obfuscated in logs. The built-in analyzers also flag Console.Write used instead of the logging system, which is relevant because output that bypasses the framework's logging path would not go through that obfuscation.

Official sources

  1. Official documentation
  2. Official README
  3. Project repository
  4. Release notes
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/thomhurst-modularpipelines.svg)](https://hysenlabs.com/projects/thomhurst-modularpipelines)