Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

Recipe 1: Multi-Environment Deployment Pipeline

A common pattern is deploying to staging first, then promoting to production with manual approval:

public class MultiEnvironmentDeployContributor : IGitLabCiContributor
{
    private readonly string _appName;
    private readonly string _registryBase;

    public MultiEnvironmentDeployContributor(string appName, string registryBase)
    {
        _appName = appName;
        _registryBase = registryBase;
    }

    public void Contribute(GitLabCiFile ciFile)
    {
        ciFile.Stages ??= new List<object>();
        foreach (var stage in new[] { "build", "test", "staging", "production" })
        {
            if (!ciFile.Stages.Contains(stage))
                ciFile.Stages.Add(stage);
        }

        ciFile.Variables ??= new Dictionary<string, object?>();
        ciFile.Variables["DOCKER_IMAGE"] = $"{_registryBase}/{_appName}";
        ciFile.Variables["DOCKER_TAG"] = "$CI_COMMIT_SHORT_SHA";

        ciFile.Jobs ??= new Dictionary<string, GitLabCiJob>();

        // Docker build
        ciFile.Jobs["docker-build"] = new GitLabCiJob
        {
            Image = "docker:24",
            Script = new List<string>
            {
                "docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY",
                "docker build -t $DOCKER_IMAGE:$DOCKER_TAG .",
                "docker push $DOCKER_IMAGE:$DOCKER_TAG",
                "docker tag $DOCKER_IMAGE:$DOCKER_TAG $DOCKER_IMAGE:latest",
                "docker push $DOCKER_IMAGE:latest"
            },
            Tags = new List<object> { "docker", "dind" }
        };

        // Deploy to staging (automatic)
        ciFile.Jobs["deploy-staging"] = new GitLabCiJob
        {
            Script = new List<string>
            {
                "kubectl config use-context staging",
                "kubectl set image deployment/$APP_NAME " +
                    "$APP_NAME=$DOCKER_IMAGE:$DOCKER_TAG",
                "kubectl rollout status deployment/$APP_NAME --timeout=300s"
            },
            Environment = new GitLabCiJobTemplateEnvironmentConfig
            {
                Name = "staging",
                Url = $"https://staging.{_appName}.example.com"
            },
            Dependencies = new List<string> { "docker-build" }
        };

        // Deploy to production (manual gate)
        ciFile.Jobs["deploy-production"] = new GitLabCiJob
        {
            Script = new List<string>
            {
                "kubectl config use-context production",
                "kubectl set image deployment/$APP_NAME " +
                    "$APP_NAME=$DOCKER_IMAGE:$DOCKER_TAG",
                "kubectl rollout status deployment/$APP_NAME --timeout=300s"
            },
            When = "manual",
            AllowFailure = false,
            Environment = new GitLabCiJobTemplateEnvironmentConfig
            {
                Name = "production",
                Url = $"https://{_appName}.example.com"
            },
            Dependencies = new List<string> { "docker-build" },
            Needs = new List<GitLabCiJobTemplateNeedsConfig>
            {
                new() { Job = "deploy-staging" }
            }
        };
    }
}

Usage:

var pipeline = new GitLabCiFile()
    .Apply(new DotNetBuildContributor())
    .Apply(new DotNetTestContributor())
    .Apply(new MultiEnvironmentDeployContributor("my-api", "registry.example.com"));

var yaml = GitLabCiYamlWriter.Serialize(pipeline);

This produces a complete pipeline with build → test → staging (auto) → production (manual) flow.

Recipe 2: Matrix Testing

Use variables and parallel execution for testing across multiple configurations:

public class DotNetMatrixTestContributor : IGitLabCiContributor
{
    private readonly string[] _frameworks;
    private readonly string[] _configurations;

    public DotNetMatrixTestContributor(
        string[] frameworks,
        string[]? configurations = null)
    {
        _frameworks = frameworks;
        _configurations = configurations ?? new[] { "Debug", "Release" };
    }

    public void Contribute(GitLabCiFile ciFile)
    {
        ciFile.Stages ??= new List<object>();
        if (!ciFile.Stages.Contains("test"))
            ciFile.Stages.Add("test");

        ciFile.Jobs ??= new Dictionary<string, GitLabCiJob>();

        foreach (var framework in _frameworks)
        {
            foreach (var config in _configurations)
            {
                var jobName = $"test-{framework}-{config}".ToLowerInvariant();
                var sdkVersion = framework switch
                {
                    "net8.0" => "8.0",
                    "net9.0" => "9.0",
                    "net10.0" => "10.0",
                    _ => "9.0"
                };

                ciFile.Jobs[jobName] = new GitLabCiJob
                {
                    Image = $"mcr.microsoft.com/dotnet/sdk:{sdkVersion}",
                    Script = new List<string>
                    {
                        $"dotnet test -f {framework} -c {config} " +
                        $"--logger \"trx;LogFileName={framework}-{config}.trx\"",
                    },
                    Artifacts = new GitLabCiArtifacts
                    {
                        Reports = new GitLabCiArtifactsReports
                        {
                            Junit = new List<string>
                            {
                                $"**/{framework}-{config}.trx"
                            }
                        },
                        ExpireIn = "7 days"
                    },
                    AllowFailure = framework == "net10.0" // Preview framework
                };
            }
        }
    }
}

Usage:

var pipeline = new GitLabCiFile()
    .Apply(new DotNetMatrixTestContributor(
        frameworks: new[] { "net8.0", "net9.0", "net10.0" },
        configurations: new[] { "Debug", "Release" }));

var yaml = GitLabCiYamlWriter.Serialize(pipeline);

This generates 6 test jobs: test-net8.0-debug, test-net8.0-release, test-net9.0-debug, etc.

Recipe 3: Conditional Contributors

Contributors can be composed conditionally based on project characteristics:

public static class PipelineFactory
{
    public static GitLabCiFile CreatePipeline(ProjectConfig config)
    {
        var file = new GitLabCiFile();
        var contributors = new List<IGitLabCiContributor>();

        // Always include build and test
        contributors.Add(new DotNetBuildContributor(
            sdkImage: $"mcr.microsoft.com/dotnet/sdk:{config.DotNetVersion}"));
        contributors.Add(new DotNetTestContributor());

        // Docker only if Dockerfile exists
        if (config.HasDockerfile)
            contributors.Add(new DockerBuildContributor(config.DockerImage));

        // NuGet publish for library projects
        if (config.IsLibrary)
            contributors.Add(new NuGetPublishContributor(config.NuGetSource));

        // Deployment for service projects
        if (config.IsService)
            contributors.Add(new MultiEnvironmentDeployContributor(
                config.AppName, config.Registry));

        // Code quality (optional)
        if (config.EnableCodeQuality)
            contributors.Add(new CodeQualityContributor());

        // Security scanning (optional)
        if (config.EnableSecurityScanning)
            contributors.Add(new SecurityScanContributor());

        // Apply all contributors
        file.Apply(contributors.ToArray());

        return file;
    }
}

public record ProjectConfig
{
    public string DotNetVersion { get; init; } = "9.0";
    public bool HasDockerfile { get; init; }
    public string? DockerImage { get; init; }
    public bool IsLibrary { get; init; }
    public string? NuGetSource { get; init; }
    public bool IsService { get; init; }
    public string? AppName { get; init; }
    public string? Registry { get; init; }
    public bool EnableCodeQuality { get; init; }
    public bool EnableSecurityScanning { get; init; }
}

Usage:

var config = new ProjectConfig
{
    DotNetVersion = "9.0",
    HasDockerfile = true,
    DockerImage = "registry.example.com/my-api:latest",
    IsService = true,
    AppName = "my-api",
    Registry = "registry.example.com",
    EnableCodeQuality = true,
    EnableSecurityScanning = true
};

var pipeline = PipelineFactory.CreatePipeline(config);
var yaml = GitLabCiYamlWriter.Serialize(pipeline);
File.WriteAllText(".gitlab-ci.yml", yaml);

Recipe 4: Pipeline Linting and Validation

Use the reader to parse existing pipelines and validate them against organizational rules:

public static class PipelineLinter
{
    public static List<string> Lint(string yamlPath)
    {
        var warnings = new List<string>();
        var yaml = File.ReadAllText(yamlPath);
        var pipeline = GitLabCiYamlReader.Deserialize(yaml);

        // Rule 1: Pipeline must define stages
        if (pipeline.Stages is null || pipeline.Stages.Count == 0)
            warnings.Add("Pipeline should define explicit stages.");

        // Rule 2: All jobs must have scripts
        if (pipeline.Jobs is not null)
        {
            foreach (var (name, job) in pipeline.Jobs)
            {
                if (job.Script is null || job.Script.Count == 0)
                    warnings.Add($"Job '{name}' has no script commands.");

                // Rule 3: Production jobs must be manual
                if (job.Environment?.Name?.Contains("production") == true &&
                    job.When != "manual")
                    warnings.Add($"Job '{name}' deploys to production " +
                                 "but is not manual.");

                // Rule 4: Jobs should have timeout
                if (job.Timeout is null)
                    warnings.Add($"Job '{name}' has no timeout set. " +
                                 "Consider adding one.");

                // Rule 5: Test jobs should have artifacts/reports
                if (name.Contains("test") &&
                    job.Artifacts?.Reports is null)
                    warnings.Add($"Test job '{name}' does not produce " +
                                 "test reports.");
            }
        }

        // Rule 6: Check version compatibility
        var targetVersion = GitLabCiVersion.Parse("18.3.0");
        if (pipeline.Jobs is not null)
        {
            foreach (var (name, job) in pipeline.Jobs)
            {
                if (job.Run is not null)
                {
                    warnings.Add($"Job '{name}' uses 'run:' which " +
                        $"requires GitLab 18.5+, but target is " +
                        $"{targetVersion}.");
                }
                if (job.Inputs is not null)
                {
                    warnings.Add($"Job '{name}' uses 'inputs:' which " +
                        $"requires GitLab 18.6+, but target is " +
                        $"{targetVersion}.");
                }
            }
        }

        return warnings;
    }
}

Recipe 5: Pipeline Migration Tool

Read an existing pipeline, transform it, and write it back:

public static class PipelineMigrator
{
    /// <summary>
    /// Migrates a pipeline from deprecated global keywords to the
    /// recommended 'default:' block.
    /// </summary>
    public static void MigrateToDefaultBlock(string yamlPath)
    {
        var yaml = File.ReadAllText(yamlPath);
        var pipeline = GitLabCiYamlReader.Deserialize(yaml);

        // Move deprecated global keywords to default block
        if (pipeline.Image is not null ||
            pipeline.BeforeScript is not null ||
            pipeline.AfterScript is not null ||
            pipeline.Cache is not null)
        {
            pipeline.Default ??= new GitLabCiDefault();

            if (pipeline.Image is not null)
            {
                pipeline.Default.Image ??= pipeline.Image;
                pipeline.Image = null;
            }

            if (pipeline.BeforeScript is not null)
            {
                pipeline.Default.BeforeScript ??= pipeline.BeforeScript;
                pipeline.BeforeScript = null;
            }

            if (pipeline.AfterScript is not null)
            {
                pipeline.Default.AfterScript ??= pipeline.AfterScript;
                pipeline.AfterScript = null;
            }

            if (pipeline.Cache is not null)
            {
                pipeline.Default.Cache ??= pipeline.Cache;
                pipeline.Cache = null;
            }
        }

        var updatedYaml = GitLabCiYamlWriter.Serialize(pipeline);
        File.WriteAllText(yamlPath, updatedYaml);
    }

    /// <summary>
    /// Adds standard organizational defaults to all jobs that don't
    /// already have them.
    /// </summary>
    public static void ApplyOrganizationDefaults(string yamlPath)
    {
        var yaml = File.ReadAllText(yamlPath);
        var pipeline = GitLabCiYamlReader.Deserialize(yaml);

        if (pipeline.Jobs is not null)
        {
            foreach (var (name, job) in pipeline.Jobs)
            {
                // Ensure all jobs have a timeout
                job.Timeout ??= "30 minutes";

                // Ensure deploy jobs have allow_failure = false
                if (name.Contains("deploy"))
                    job.AllowFailure ??= false;

                // Ensure test jobs produce reports
                if (name.Contains("test") && job.Artifacts?.Reports is null)
                {
                    job.Artifacts ??= new GitLabCiArtifacts();
                    job.Artifacts.Reports ??= new GitLabCiArtifactsReports();
                    job.Artifacts.Reports.Junit ??= new List<string>
                    {
                        "**/*.trx", "**/*junit*.xml"
                    };
                }
            }
        }

        var updatedYaml = GitLabCiYamlWriter.Serialize(pipeline);
        File.WriteAllText(yamlPath, updatedYaml);
    }
}

Recipe 6: Pipeline Diff Tool

Compare two pipelines programmatically:

public static class PipelineDiff
{
    public static PipelineComparison Compare(string yamlA, string yamlB)
    {
        var pipelineA = GitLabCiYamlReader.Deserialize(yamlA);
        var pipelineB = GitLabCiYamlReader.Deserialize(yamlB);

        var jobsA = pipelineA.Jobs?.Keys.ToHashSet() ?? new();
        var jobsB = pipelineB.Jobs?.Keys.ToHashSet() ?? new();

        return new PipelineComparison
        {
            AddedJobs = jobsB.Except(jobsA).ToList(),
            RemovedJobs = jobsA.Except(jobsB).ToList(),
            CommonJobs = jobsA.Intersect(jobsB).ToList(),
            StagesA = pipelineA.Stages?.Cast<string>().ToList() ?? new(),
            StagesB = pipelineB.Stages?.Cast<string>().ToList() ?? new(),
            VariablesAddedCount = CountNewVariables(
                pipelineA.Variables, pipelineB.Variables),
            VariablesRemovedCount = CountRemovedVariables(
                pipelineA.Variables, pipelineB.Variables),
        };
    }

    private static int CountNewVariables(
        Dictionary<string, object?>? a,
        Dictionary<string, object?>? b)
    {
        if (b is null) return 0;
        if (a is null) return b.Count;
        return b.Keys.Except(a.Keys).Count();
    }

    private static int CountRemovedVariables(
        Dictionary<string, object?>? a,
        Dictionary<string, object?>? b)
    {
        if (a is null) return 0;
        if (b is null) return a.Count;
        return a.Keys.Except(b.Keys).Count();
    }
}

public record PipelineComparison
{
    public List<string> AddedJobs { get; init; } = new();
    public List<string> RemovedJobs { get; init; } = new();
    public List<string> CommonJobs { get; init; } = new();
    public List<string> StagesA { get; init; } = new();
    public List<string> StagesB { get; init; } = new();
    public int VariablesAddedCount { get; init; }
    public int VariablesRemovedCount { get; init; }
}
Diagram
Every advanced recipe in this chapter reduces to a composition of five foundation APIs — GitLabCiFile, contributors, writer, reader, and version — with no new runtime machinery required.

⬇ Download