Mavusi.FluentPipelines.GitHubActions 1.0.0

dotnet add package Mavusi.FluentPipelines.GitHubActions --version 1.0.0
                    
NuGet\Install-Package Mavusi.FluentPipelines.GitHubActions -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Mavusi.FluentPipelines.GitHubActions" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Mavusi.FluentPipelines.GitHubActions" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Mavusi.FluentPipelines.GitHubActions" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Mavusi.FluentPipelines.GitHubActions --version 1.0.0
                    
#r "nuget: Mavusi.FluentPipelines.GitHubActions, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Mavusi.FluentPipelines.GitHubActions@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Mavusi.FluentPipelines.GitHubActions&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Mavusi.FluentPipelines.GitHubActions&version=1.0.0
                    
Install as a Cake Tool

Fluent Pipelines

NuGet License: MIT

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:

  1. Create the generator project:
mkdir -p tools/PipelineGen
cd tools/PipelineGen
dotnet new console -n PipelineGen -f net9.0
cd PipelineGen
  1. Reference the local DSL and emitter projects:
dotnet add reference ../../../src/PipelineDsl/PipelineDsl.csproj
dotnet add reference ../../../src/Emitters/GitHubActionsEmitter/GitHubActionsEmitter.csproj
  1. Replace Program.cs with:
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");
  1. Generate the workflow:
dotnet run
  1. 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.

  1. 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
  1. Reference the local DSL and Azure emitter projects:
dotnet add reference ../../../src/PipelineDsl/PipelineDsl.csproj
dotnet add reference ../../../src/Emitters/AzureDevOpsEmitter/AzureDevOpsEmitter.csproj
  1. Replace Program.cs with:
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");
  1. Generate the pipeline YAML:
dotnet run
  1. 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

πŸ—ΊοΈ 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 89 5/11/2026