Mavusi.FluentPipelines.Core
1.0.0
dotnet add package Mavusi.FluentPipelines.Core --version 1.0.0
NuGet\Install-Package Mavusi.FluentPipelines.Core -Version 1.0.0
<PackageReference Include="Mavusi.FluentPipelines.Core" Version="1.0.0" />
<PackageVersion Include="Mavusi.FluentPipelines.Core" Version="1.0.0" />
<PackageReference Include="Mavusi.FluentPipelines.Core" />
paket add Mavusi.FluentPipelines.Core --version 1.0.0
#r "nuget: Mavusi.FluentPipelines.Core, 1.0.0"
#:package Mavusi.FluentPipelines.Core@1.0.0
#addin nuget:?package=Mavusi.FluentPipelines.Core&version=1.0.0
#tool nuget:?package=Mavusi.FluentPipelines.Core&version=1.0.0
Fluent Pipelines
Fluent Pipelines is a powerful, type-safe fluent API for defining CI/CD pipelines in C#. Write your pipeline configuration once using an intuitive builder pattern and emit to GitHub Actions, Azure DevOps, or GitLab CI YAML.
β¨ Features
- π― Type-Safe - Catch configuration errors at compile time, not runtime
- π Multi-Platform - Define once, emit to GitHub Actions, Azure DevOps, or GitLab CI
- π‘ Intuitive - Fluent API that reads like natural language
- π§ͺ Testable - Unit test your pipelines before deployment
- π¦ Extensible - Easy to add custom steps and emitters
- π¨ Clean - No YAML syntax errors, consistent formatting
Why This Exists
Most pipeline definitions become large, repetitive YAML files with weak reuse and no compile-time safety. This project gives you:
- Strong typing and IntelliSense
- Provider-agnostic core model (AST)
- Separate validation layer
- Pluggable emitters (GitHub Actions, Azure DevOps, GitLab)
- Escape hatch for provider-specific raw syntax
Architecture: Builder β Immutable AST β Validation β Emitter β YAML/JSON
Project Layout
src/
PipelineCore/ # Immutable pipeline AST
PipelineDsl/ # Fluent builders
PipelineValidation/ # Core validation rules
PipelineSerialization/ # Shared serialization helpers (future)
Emitters/
GitHubActionsEmitter/ # GitHub Actions YAML emitter
AzureDevOpsEmitter/ # Azure DevOps YAML emitter
GitLabEmitter/ # Placeholder skeleton
tests/
PipelineCore.Tests/
PipelineDsl.Tests/
Emitter.Tests/
π¦ Installation
Install the main package:
dotnet add package Mavusi.FluentPipelines
Then install the emitter(s) for your target platform(s):
# For GitHub Actions
dotnet add package Mavusi.FluentPipelines.GitHubActions
# For Azure DevOps
dotnet add package Mavusi.FluentPipelines.AzureDevOps
# For GitLab CI
dotnet add package Mavusi.FluentPipelines.GitLab
π Quick Start
Here's a simple CI pipeline:
using PipelineDsl;
using GitHubActionsEmitter;
var pipeline = Pipeline.Create("CI")
.OnPush("main")
.Job("build", job => job
.RunsOnUbuntuLatest()
.Step(s => s.Checkout())
.Step(s => s.SetupDotNet("8.0"))
.Step(s => s.Run("dotnet build"))
.Step(s => s.Run("dotnet test")))
.Build();
var emitter = new GitHubActionsEmitter();
var yaml = emitter.Emit(pipeline);
File.WriteAllText(".github/workflows/ci.yml", yaml);
This generates:
name: CI
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0
- run: dotnet build
- run: dotnet test
π Complete Example
Here's a production-ready build, test, and deploy pipeline:
using PipelineDsl;
using GitHubActionsEmitter;
var pipeline = Pipeline.Create("Build and Deploy API")
.OnPush("main")
.OnManual()
.Variable("DOTNET_VERSION", "8.0.x")
.Variable("AZURE_RESOURCE_GROUP", "rg-myapi-prod")
.Job("build-test-package", job => job
.RunsOnUbuntuLatest()
.Output("image-tag", "${{ steps.meta.outputs.image-tag }}")
.Step(s => s.Checkout().WithName("Checkout source"))
.Step(s => s.SetupDotNet("${{ env.DOTNET_VERSION }}").WithName("Setup .NET"))
.Step(s => s.Run("dotnet restore").WithName("Restore dependencies"))
.Step(s => s.Run("dotnet build --configuration Release --no-restore")
.WithName("Build solution"))
.Step(s => s.Run("dotnet test --configuration Release --no-build")
.WithName("Run tests"))
.Step(s => s.Run("""
SHORT_SHA=${GITHUB_SHA::7}
IMAGE_TAG=main-${SHORT_SHA}
echo "image-tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
""").WithId("meta").WithName("Generate image metadata"))
.Step(s => s.Action("azure/login", "v2", new Dictionary<string, string>
{
["creds"] = "${{ secrets.AZURE_CREDENTIALS }}"
}).WithName("Azure login"))
.Step(s => s.Run("docker build -t myregistry.azurecr.io/myapi:${{ steps.meta.outputs.image-tag }} .")
.WithName("Build Docker image"))
.Step(s => s.Run("docker push myregistry.azurecr.io/myapi:${{ steps.meta.outputs.image-tag }}")
.WithName("Push Docker image")))
.Job("deploy", job => job
.RunsOnUbuntuLatest()
.DependsOn("build-test-package")
.InEnvironment("production", "https://api.mycompany.com")
.Step(s => s.Action("azure/login", "v2", new Dictionary<string, string>
{
["creds"] = "${{ secrets.AZURE_CREDENTIALS }}"
}).WithName("Azure login"))
.Step(s => s.Run("""
az containerapp update \
--name $CONTAINER_APP_NAME \
--resource-group $AZURE_RESOURCE_GROUP \
--image myregistry.azurecr.io/myapi:${{ needs.build-test-package.outputs.image-tag }}
""").WithName("Deploy to Azure Container Apps")))
.Build();
var emitter = new GitHubActionsEmitter();
var yaml = emitter.Emit(pipeline);
File.WriteAllText(".github/workflows/deploy.yml", yaml);
π Core Concepts
Pipeline Builder
Every pipeline starts with Pipeline.Create(name):
var pipeline = Pipeline.Create("My Pipeline")
.OnPush("main", "develop") // Add triggers
.OnPullRequest("main")
.OnSchedule("0 0 * * *") // Daily at midnight
.OnManual() // workflow_dispatch
.Variable("ENV", "production") // Pipeline-level variables
.Job("job1", job => { /* ... */ }) // Add jobs
.Build(); // Generate AST
Job Builder
Jobs define where and how work gets done:
.Job("build", job => job
.RunsOnUbuntuLatest() // Runner
.DependsOn("setup") // Job dependencies
.Output("version", "${{ steps.step1.outputs.ver }}") // Job outputs
.InEnvironment("staging", "https://staging.app.com") // Environment
.Step(s => { /* ... */ })) // Steps
Step Builder
Steps are the individual tasks:
// Run a script
.Step(s => s.Run("dotnet build")
.WithName("Build Project")
.WithId("build-step"))
// Use an action
.Step(s => s.Action("actions/cache", "v3", new Dictionary<string, string>
{
["path"] = "~/.nuget/packages",
["key"] = "${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}"
}))
// Convenience methods
.Step(s => s.Checkout()) // actions/checkout@v4
.Step(s => s.SetupDotNet("8.0")) // actions/setup-dotnet@v4
Multi-line Scripts
Use C# raw string literals for complex scripts:
.Step(s => s.Run("""
dotnet restore
dotnet build --configuration Release
dotnet test --no-build
dotnet pack --output ./artifacts
"""))
π§ Advanced Features
Job Dependencies and Outputs
var pipeline = Pipeline.Create("CI")
.Job("setup", job => job
.Output("version", "${{ steps.get-version.outputs.version }}")
.Step(s => s.Run("echo version=1.0.0 >> $GITHUB_OUTPUT")
.WithId("get-version")))
.Job("build", job => job
.DependsOn("setup")
.Step(s => s.Run("echo Building version ${{ needs.setup.outputs.version }}")))
.Build();
Environments and Approvals
.Job("deploy-prod", job => job
.InEnvironment("production", "https://api.example.com")
.Step(s => s.Run("kubectl apply -f deployment.yml")))
Triggers with Filters
.OnPush(
branches: new[] { "main", "release/*" },
paths: new[] { "src/**", "tests/**" },
tags: new[] { "v*" })
π― Use Cases
- β Multi-platform deployments - Maintain identical pipelines for GitHub, Azure DevOps, and GitLab
- β Pipeline as code - Version control, code review, and test your CI/CD
- β Template generation - Generate pipelines programmatically from templates
- β Migration - Easier to migrate between CI/CD platforms
- β Documentation - Self-documenting pipeline code with IntelliSense
π API Reference
Pipeline Methods
| Method | Description |
|---|---|
Create(name) |
Start a new pipeline |
OnPush(branches...) |
Add push trigger |
OnPullRequest(branches...) |
Add PR trigger |
OnSchedule(cron) |
Add scheduled trigger |
OnManual() |
Add manual/workflow_dispatch trigger |
Variable(name, value) |
Add pipeline-level variable |
Job(name, configure) |
Add a job |
Build() |
Generate the pipeline AST |
Job Methods
| Method | Description |
|---|---|
RunsOnUbuntuLatest() |
Use ubuntu-latest runner |
RunsOn(runner) |
Specify custom runner |
DependsOn(jobs...) |
Set job dependencies |
Output(name, value) |
Define job output |
InEnvironment(name, url) |
Set deployment environment |
Step(configure) |
Add a step |
Step Methods
| Method | Description |
|---|---|
Run(script) |
Execute shell script |
Action(id, version, inputs) |
Use an action/task |
Checkout(version) |
Shortcut for actions/checkout |
SetupDotNet(version) |
Shortcut for actions/setup-dotnet |
WithName(name) |
Set step display name |
WithId(id) |
Set step identifier |
When(condition) |
Add conditional execution |
π§ͺ Testing Your Pipelines
One of the key advantages of defining pipelines in C# is testability:
[Fact]
public void Pipeline_ShouldHaveCheckoutStep()
{
var pipeline = Pipeline.Create("Test")
.OnPush("main")
.Job("build", job => job.Step(s => s.Checkout()))
.Build();
var emitter = new GitHubActionsEmitter();
var yaml = emitter.Emit(pipeline);
Assert.Contains("uses: actions/checkout@v4", yaml);
}
π οΈ Development
Building from Source
git clone https://github.com/mavusi/Mavusi.FluentPipelines.git
cd Mavusi.FluentPipelines
dotnet build Mavusi.FluentPipelines.sln
dotnet test Mavusi.FluentPipelines.sln
Creating a Pipeline Generator
Create a tiny generator app that emits a real GitHub workflow file:
- Create the generator project:
mkdir -p tools/PipelineGen
cd tools/PipelineGen
dotnet new console -n PipelineGen -f net9.0
cd PipelineGen
- Reference the local DSL and emitter projects:
dotnet add reference ../../../src/PipelineDsl/PipelineDsl.csproj
dotnet add reference ../../../src/Emitters/GitHubActionsEmitter/GitHubActionsEmitter.csproj
- Replace
Program.cswith:
using PipelineDsl;
using GitHubActionsEmitter;
var pipeline = Pipeline
.Create("CI")
.OnPush("main")
.OnPullRequest("main")
.Job("build", job => job
.RunsOnUbuntuLatest()
.Step(step => step.Checkout())
.Step(step => step.SetupDotNet("9.0"))
.Step(step => step.Run("dotnet restore"))
.Step(step => step.Run("dotnet build -c Release --no-restore"))
.Step(step => step.Run("dotnet test -c Release --no-build")))
.Build();
var yaml = new GitHubActionsEmitter.GitHubActionsEmitter().Emit(pipeline);
Directory.CreateDirectory("../../../.github/workflows");
File.WriteAllText("../../../.github/workflows/ci.yml", yaml);
Console.WriteLine("Generated .github/workflows/ci.yml");
- Generate the workflow:
dotnet run
- Commit and push:
git add .github/workflows/ci.yml tools/PipelineGen
git commit -m "Add generated CI workflow"
git push
Expected output file:
name: CI
on:
push:
branches:
- main
pull-request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0
- run: dotnet restore
- run: dotnet build -c Release --no-restore
- run: dotnet test -c Release --no-build
Azure DevOps Example
Create a tiny generator app that emits an Azure DevOps pipeline file.
- Create the generator project:
mkdir -p tools/PipelineGen.Ado
cd tools/PipelineGen.Ado
dotnet new console -n PipelineGen.Ado -f net9.0
cd PipelineGen.Ado
- Reference the local DSL and Azure emitter projects:
dotnet add reference ../../../src/PipelineDsl/PipelineDsl.csproj
dotnet add reference ../../../src/Emitters/AzureDevOpsEmitter/AzureDevOpsEmitter.csproj
- Replace
Program.cswith:
using PipelineDsl;
using AzureDevOpsEmitter;
var pipeline = Pipeline
.Create("CI")
.OnPush("main")
.Job("build", job => job
.RunsOnUbuntuLatest()
.Step(step => step.Run("dotnet restore"))
.Step(step => step.Run("dotnet build -c Release --no-restore"))
.Step(step => step.Run("dotnet test -c Release --no-build")))
.Build();
var yaml = new AzureDevOpsEmitter.AzureDevOpsEmitter().Emit(pipeline);
File.WriteAllText("../../../azure-pipelines.yml", yaml);
Console.WriteLine("Generated azure-pipelines.yml");
- Generate the pipeline YAML:
dotnet run
- Commit and push:
git add azure-pipelines.yml tools/PipelineGen.Ado
git commit -m "Add generated Azure DevOps pipeline"
git push
Expected output file:
trigger:
branches:
include:
- main
jobs:
- job: build
pool:
vmImage: ubuntu-latest
steps:
- script: dotnet restore
- script: dotnet build -c Release --no-restore
- script: dotnet test -c Release --no-build
π€ Contributing
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
π License
This project is licensed under the MIT License - see the LICENSE file for details.
π Acknowledgments
- Built with YamlDotNet for YAML serialization
- Inspired by modern fluent API patterns in .NET
π Support
- π Issue Tracker
- π¬ Discussions
πΊοΈ Roadmap
- Additional platform emitters (Jenkins, CircleCI)
- Pipeline validation and linting
- Visual Studio Code extension
- Pipeline import from existing YAML
- More built-in step templates
Made with β€οΈ by Mavusi
Example 1: Web API CI (Restore, Build, Test)
Real-world-adjacent scenario: run CI on pushes and PRs to main.
using PipelineDsl;
using GitHubActionsEmitter;
var pipeline = Pipeline
.Create("Web API CI")
.OnPush("main")
.OnPullRequest("main")
.Variable("DOTNET_NOLOGO", "true")
.Job("build", job => job
.RunsOnUbuntuLatest()
.Step(step => step.Checkout())
.Step(step => step.SetupDotNet("9.0"))
.Step(step => step.Run("dotnet restore ./src/MyApi/MyApi.csproj"))
.Step(step => step.Run("dotnet build ./src/MyApi/MyApi.csproj -c Release --no-restore"))
.Step(step => step.Run("dotnet test ./tests/MyApi.Tests/MyApi.Tests.csproj -c Release --no-build")))
.Build();
var emitter = new GitHubActionsEmitter.GitHubActionsEmitter();
var yaml = emitter.Emit(pipeline);
Console.WriteLine(yaml);
Generated GitHub Actions YAML (shape):
name: Web API CI
on:
push:
branches:
- main
pull-request:
branches:
- main
env:
DOTNET_NOLOGO: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 9.0
- run: dotnet restore ./src/MyApi/MyApi.csproj
- run: dotnet build ./src/MyApi/MyApi.csproj -c Release --no-restore
- run: dotnet test ./tests/MyApi.Tests/MyApi.Tests.csproj -c Release --no-build
Example 2: Multi-Job Pipeline (Lint β Build β Test)
Real-world-adjacent scenario: strict job ordering for faster failure feedback.
using PipelineDsl;
using AzureDevOpsEmitter;
var pipeline = Pipeline
.Create("Service CI")
.OnPush("main", "develop")
.Job("lint", job => job
.RunsOnLinux()
.Step(step => step.Run("dotnet format --verify-no-changes")))
.Job("build", job => job
.RunsOnLinux()
.DependsOn("lint")
.Step(step => step.Run("dotnet restore"))
.Step(step => step.Run("dotnet build -c Release --no-restore")))
.Job("test", job => job
.RunsOnLinux()
.DependsOn("build")
.Step(step => step.Run("dotnet test -c Release --no-build")))
.Build();
var emitter = new AzureDevOpsEmitter.AzureDevOpsEmitter();
var yaml = emitter.Emit(pipeline);
Console.WriteLine(yaml);
Generated Azure DevOps YAML (shape):
trigger:
branches:
include:
- main
- develop
jobs:
- job: lint
pool:
vmImage: ubuntu-latest
steps:
- script: dotnet format --verify-no-changes
- job: build
dependsOn:
- lint
pool:
vmImage: ubuntu-latest
steps:
- script: dotnet restore
- script: dotnet build -c Release --no-restore
- job: test
dependsOn:
- build
pool:
vmImage: ubuntu-latest
steps:
- script: dotnet test -c Release --no-build
Example 3: Escape Hatch for Provider-Specific Syntax
Real-world-adjacent scenario: using a niche provider feature not yet represented in the core AST.
using PipelineDsl;
var pipeline = Pipeline
.Create("Release")
.OnPush("main")
.Job("publish", job => job
.RunsOnUbuntuLatest()
.Step(step => step.Checkout())
.Step(step => step.Raw("""
- name: Upload SBOM
uses: anchore/sbom-action@v0
with:
path: ./artifacts/sbom.spdx.json
""")))
.Build();
Validation Example
Core validation is separate from emitters.
using PipelineValidation;
var validator = new CorePipelineValidator();
var result = validator.Validate(pipeline);
if (!result.IsValid)
{
foreach (var error in result.Errors)
{
Console.Error.WriteLine($"[{error.Code}] {error.Message}");
}
}
Built-in rules include:
- Duplicate job names
- Missing dependencies
- Circular dependency graphs
- Empty step lists
- Missing trigger warning
Extending the DSL
You can package reusable domain-specific steps as extension methods.
using PipelineDsl;
public static class DockerExtensions
{
public static JobBuilder DockerBuild(this JobBuilder builder, string image, string dockerfile = "Dockerfile")
{
return builder
.Step(s => s.Run($"docker build -f {dockerfile} -t {image} ."));
}
}
Usage:
var pipeline = Pipeline
.Create("Container CI")
.OnPush("main")
.Job("container", job => job
.RunsOnUbuntuLatest()
.Step(s => s.Checkout())
.DockerBuild("ghcr.io/acme/orders-api:latest"))
.Build();
Current Scope
Implemented:
- Core AST
- Fluent DSL
- Core validation engine
- GitHub Actions emitter
- Azure DevOps emitter
- Unit and emitter tests
Planned:
- GitLab emitter implementation
- Matrix support in DSL and emitters
- Artifacts/caching abstractions
- Roslyn analyzer package
- CLI tooling (
generate,validate,preview)
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- No dependencies.
NuGet packages (4)
Showing the top 4 NuGet packages that depend on Mavusi.FluentPipelines.Core:
| Package | Downloads |
|---|---|
|
Mavusi.FluentPipelines
A powerful, type-safe fluent API for defining CI/CD pipelines in C#. Write your pipeline configuration once using an intuitive builder pattern and emit to GitHub Actions, Azure DevOps, or GitLab CI YAML. Perfect for maintaining consistent pipelines across multiple platforms. |
|
|
Mavusi.FluentPipelines.AzureDevOps
Azure DevOps emitter for Fluent Pipelines. Converts fluent pipeline definitions to Azure Pipelines YAML. Use with Mavusi.FluentPipelines to generate Azure DevOps pipelines from C# code. |
|
|
Mavusi.FluentPipelines.GitHubActions
GitHub Actions emitter for Fluent Pipelines. Converts fluent pipeline definitions to GitHub Actions workflow YAML. Use with Mavusi.FluentPipelines to generate GitHub Actions workflows from C# code. |
|
|
Mavusi.FluentPipelines.GitLab
GitLab CI emitter for Fluent Pipelines. Converts fluent pipeline definitions to GitLab CI/CD YAML. Use with Mavusi.FluentPipelines to generate GitLab pipelines from C# code. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0 | 165 | 5/11/2026 |