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:
- Design Time -- A one-time tool that downloads versioned configuration files from an upstream source
- Build Time -- A Roslyn Source Generator that parses, merges, and emits C# types
- 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
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/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:
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 ReleasesGitLabRbParser-- parses Ruby templates instead of JSON SchemasGitLabRbVersionMerger-- same algorithm asSchemaVersionMergerbut for Ruby nodesGitLabConfigEmitter-- emits config classes from Ruby nodes instead of JSON Schema typesGitLabRbRenderer-- 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.