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

Part II: The Three-Phase Pipeline

Same pipeline, different input. Docker Compose Bundle reads JSON Schemas. Traefik Bundle reads JSON Schemas. GitLab.DockerCompose reads Ruby templates. The pattern is the same: download, parse, merge, emit, render.

The Pattern

GitLab.DockerCompose follows the same three-phase pipeline used by the Docker Compose Bundle and the Traefik Bundle:

  1. Design Time -- A one-time tool that downloads versioned configuration files from an upstream source
  2. Build Time -- A Roslyn Source Generator that parses, merges, and emits C# types
  3. Runtime -- Hand-written code that uses the generated types with IntelliSense and compile-time safety

The key difference is the input format. Docker Compose Bundle parses JSON Schema files. Traefik Bundle parses JSON Schema files. GitLab.DockerCompose parses Ruby templates -- a format that was never designed to be machine-readable.

The Full Pipeline

Diagram
The pipeline mirrors the Docker Compose and Traefik bundles — only the input format differs — walking from tag discovery to unified model to generated builders to a Ruby block embedded in docker-compose.yml.

Project Structure

Each phase maps to a dedicated project:

GitLab.DockerCompose/
├── src/
│   ├── FrenchExDev.Net.GitLab.DockerCompose.Design/     ← Phase 1
│   │   ├── Program.cs                                    (pipeline runner)
│   │   └── GitLabTagsVersionCollector.cs                 (API client)
│   │
│   ├── FrenchExDev.Net.GitLab.DockerCompose.SourceGenerator/  ← Phase 2
│   │   ├── GitLabOmnibusGenerator.cs                     (IIncrementalGenerator)
│   │   ├── GitLabRbParser.cs                             (Ruby parser)
│   │   ├── GitLabRbModel.cs                              (parsed data model)
│   │   ├── GitLabRbVersionMerger.cs                      (version merger)
│   │   ├── GitLabConfigEmitter.cs                        (C# class emitter)
│   │   └── NamingHelper.cs                               (snake_case ↔ PascalCase)
│   │
│   └── FrenchExDev.Net.GitLab.DockerCompose/             ← Phase 3
│       ├── Contributors/
│       │   ├── GitLabComposeContributor.cs               (core GitLab service)
│       │   ├── PostgresqlComposeContributor.cs           (external PostgreSQL)
│       │   ├── RedisComposeContributor.cs                (external Redis)
│       │   ├── GitLabRunnerComposeContributor.cs         (CI/CD runner)
│       │   └── MinioComposeContributor.cs                (S3 storage)
│       ├── Rendering/
│       │   └── GitLabRbRenderer.cs                       (C# → Ruby)
│       └── GitLabDockerImages.cs                         (image constants)
│
├── resources/                                            ← Phase 1 output
│   ├── gitlab-9.2.10.rb
│   ├── gitlab-15.0.5.rb
│   ├── ...
│   └── gitlab-18.10.1.rb                                (~80 files)
│
└── test/
    ├── FrenchExDev.Net.GitLab.DockerCompose.Tests/
    └── FrenchExDev.Net.GitLab.DockerCompose.SourceGenerator.Tests/

The dependency graph is strictly one-directional:

Diagram
The three projects never reference each other — the Design console app writes files, the netstandard2.0 generator picks them up as AdditionalFiles, and the net10.0 main library is the only place generated types land.

The Design tool is a standalone console app. It has no runtime dependency on the Source Generator or the main library. The Source Generator is a netstandard2.0 assembly (Roslyn requirement). The main library targets net10.0 and contains only the generated types plus hand-written contributors and rendering.

The Cost Model

Phase When It Runs Frequency Cost
Design dotnet run --project .Design Once per GitLab release (~monthly) ~10 seconds (API calls + downloads)
Build dotnet build Every compile (incremental) ~2 seconds (parse + merge + emit)
Runtime Application execution Every deployment Zero reflection for builders; reflection in renderer

Design time is the most expensive per run but runs the least often. When GitLab 18.11 ships, you run the design tool once. It downloads the new gitlab-18.11.x.rb file. Existing files are untouched (idempotent).

Build time is incremental. The Roslyn Source Generator only re-runs when AdditionalFiles change (i.e., when a new .rb file is added). Day-to-day development that doesn't touch the resources directory sees zero generator overhead.

Runtime has two costs: the Builder API is zero-reflection (all generated code, direct property assignment), while the Renderer uses reflection to iterate properties. For a configuration object built once at application startup, the reflection cost is negligible.

Alternative 1: Manual class maintenance

Write the 55 config classes by hand. Update them manually when GitLab adds settings.

Problem: The gitlab_rails prefix alone has 200+ settings. Maintaining 55 classes with 500+ total properties across 80 versions by hand is unsustainable. You'd miss settings, mistype names, and have no version metadata.

Alternative 2: Runtime parsing

Parse gitlab.rb at runtime and build a dynamic configuration object.

Problem: No IntelliSense, no compile-time checks, no autocomplete. You're back to strings. Plus you'd need to ship 80 .rb files with your application and parse them on every startup.

Alternative 3: Single-version generation

Generate types from only the latest gitlab.rb version.

Problem: No [SinceVersion] / [UntilVersion] metadata. You can't tell developers "this setting only exists in v17+". You lose the entire version story.

The three-phase approach gives you: design-time freshness (new versions picked up automatically), build-time intelligence (version-aware types with IntelliSense), and runtime correctness (validated configuration rendered to proper Ruby syntax). Each phase does one thing well, and the pipeline is the same proven pattern used across the monorepo.

Shared Infrastructure

The three-phase pipeline isn't built from scratch for each project. Several components are shared across GitLab.DockerCompose, Docker Compose Bundle, and Traefik Bundle:

Component Package Purpose
IVersionCollector FrenchExDev.Net.Wrapper.Versioning Collects version tags from GitHub/GitLab APIs
DesignPipeline<T> FrenchExDev.Net.Wrapper.Versioning Orchestrates download + save with rate limiting
VersionFilters FrenchExDev.Net.Wrapper.Versioning LatestPatchPerMinor, LatestPatchPerMajor, etc.
BuilderEmitter FrenchExDev.Net.Builder.SourceGenerator.Lib Emits fluent builder classes from property models
IComposeFileContributor FrenchExDev.Net.DockerCompose.Bundle Contributor interface for Docker Compose services
ComposeFile FrenchExDev.Net.DockerCompose.Bundle Typed Docker Compose model (from 32 merged schemas)

The only project-specific components are:

  • GitLabTagsVersionCollector -- uses GitLab Tags API instead of GitHub Releases
  • GitLabRbParser -- parses Ruby templates instead of JSON Schemas
  • GitLabRbVersionMerger -- same algorithm as SchemaVersionMerger but for Ruby nodes
  • GitLabConfigEmitter -- emits config classes from Ruby nodes instead of JSON Schema types
  • GitLabRbRenderer -- converts C# objects back to Ruby syntax (unique to this project)
  • The five contributors -- project-specific service definitions

The next part dives into Phase 1: how the design-time tool talks to the GitLab API and downloads 80 versioned templates.

⬇ Download