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

Why Schema-Driven Code Generation?

.gitlab-ci.yml has 50+ definition types, union types, version-dependent properties, and a flat root structure. Writing and maintaining typed models by hand would be:

  • Error-prone — miss a property, get it wrong, fall behind on new versions
  • Time-consuming — 30+ classes with 200+ properties across 11 versions
  • Unmaintainable — every GitLab release could add properties

By generating from GitLab's own schema, models are always correct by construction. Adding a new version is one CLI run + rebuild.

Why Follow the Four-Project Pattern?

Concern Project Benefit
Triggering Attributes Minimal dependency, netstandard2.0 compatible
Schema acquisition Design Separate executable, no build slowdown
Code generation SourceGenerator Compile-time only, zero runtime cost
Consumer API Main Library Clean public surface

This means no runtime schema parsing, no build-time downloads, and testability in isolation.

Why Merge Multiple Schema Versions?

GitLab evolves monthly. Rather than targeting a single version:

  • Unified API — one set of models covers all supported versions
  • Version metadata — each property carries [SinceVersion]/[UntilVersion]
  • Latest types — property definitions use the latest schema (most complete)
  • No breaking changes — adding a schema only adds properties, never removes

Why Type Aliases Mapped to Primitives?

Many definitions (like script = oneOf[string, array]) are type aliases. Rather than generating wrapper classes, they map to C# primitives (List<string>?). This keeps the API clean at the cost of losing some schema-level type distinction.

Why List<object> for Stages?

The schema defines stages as type: array without items type constraints. It maps to List<object>?. In practice, stages are always strings, but the schema doesn't enforce this.

Why Hand-Written Reader/Writer?

Unlike models and builders (which are generated), serialization is hand-written because:

  • Flat root merging — jobs at root level requires custom dictionary building
  • Union type handlingstring | List<string> needs IYamlTypeConverter
  • YamlDotNet configuration — naming conventions and null handling are runtime concerns
  • Forward compatibilityIgnoreUnmatchedProperties handles unknown YAML keys

Why No YAML Anchor/Alias Support?

YAML anchors (&anchor) and aliases (*alias) are syntax features, not GitLab CI concepts. The reader processes resolved YAML (YamlDotNet expands anchors), and the writer always emits explicit values. This keeps the C# model clean.

Why Catch-All Job Deserialization?

The reader wraps job deserialization in try/catch. When typed deserialization fails (e.g., a union type that doesn't map cleanly), it creates a minimal GitLabCiJob. Partial parsing is more useful than complete failure.


Main Library (.csproj)

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <RootNamespace>FrenchExDev.Net.GitLab.Ci.Yaml</RootNamespace>
    <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
    <CompilerGeneratedFilesOutputPath>
      $(BaseIntermediateOutputPath)/Generated
    </CompilerGeneratedFilesOutputPath>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\Result\...\FrenchExDev.Net.Result.csproj" />
    <ProjectReference Include="..\..\Builder\...\FrenchExDev.Net.Builder.csproj" />
    <ProjectReference Include="..\...Attributes\...Attributes.csproj" />
    <ProjectReference Include="..\...SourceGenerator\...SourceGenerator.csproj"
                      OutputItemType="Analyzer"
                      ReferenceOutputAssembly="false" />
    <ProjectReference Include="..\..\Builder\...SourceGenerator.Lib\...csproj"
                      OutputItemType="Analyzer"
                      ReferenceOutputAssembly="false" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="YamlDotNet" />
  </ItemGroup>

  <ItemGroup>
    <AdditionalFiles Include="schemas\gitlab-ci-*.json" />
    <EmbeddedResource Include="schemas\gitlab-ci-*.json" />
  </ItemGroup>
</Project>

Key configuration:

  • EmitCompilerGeneratedFiles — writes generated .g.cs files to obj/Generated/ for inspection and debugging
  • OutputItemType="Analyzer" — tells MSBuild that the SourceGenerator project is an analyzer, not a runtime dependency
  • ReferenceOutputAssembly="false" — the generator DLL is loaded by the compiler, not referenced at runtime
  • AdditionalFiles — makes JSON schemas visible to the source generator
  • EmbeddedResource — also embeds schemas as resources (optional, for runtime access)

Source Generator (.csproj)

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <Nullable>enable</Nullable>
    <LangVersion>latest</LangVersion>
    <IsRoslynComponent>true</IsRoslynComponent>
    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp"
                      PrivateAssets="all" />
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers"
                      PrivateAssets="all" />
    <PackageReference Include="System.Text.Json" PrivateAssets="all" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\Builder\...SourceGenerator.Lib\...csproj" />
  </ItemGroup>
</Project>

Key configuration:

  • netstandard2.0 — required by Roslyn (source generators must target ns2.0)
  • IsRoslynComponent — identifies this project as a Roslyn analyzer/generator
  • EnforceExtendedAnalyzerRules — enforces stricter analyzer rules for source generators
  • PrivateAssets="all" — prevents transitive dependency leaking
  • System.Text.Json — used for parsing JSON schemas (available in ns2.0 as a NuGet package)

Attributes (.csproj)

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFrameworks>netstandard2.0;net10.0</TargetFrameworks>
    <Nullable>enable</Nullable>
    <LangVersion>latest</LangVersion>
  </PropertyGroup>
</Project>

Minimal — zero dependencies, multi-target for maximum compatibility.

Design CLI (.csproj)

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\Wrapper.Versioning\...csproj" />
  </ItemGroup>
</Project>

A simple CLI executable that depends only on Wrapper.Versioning for version collection and download pipeline.


Comparison with Other Approaches

Approach Type Safety IntelliSense Version Awareness Modular Composition Round-Trip Maintenance
Hand-written YAML None IDE schema plugin (partial) Manual Copy-paste N/A High
YAML templating (anchors/includes) None None None Via includes N/A High
GitLab CI Lint API Runtime only None Server-side None N/A None
Third-party YAML libraries Basic Basic None None Partial Low
GitLab.Ci.Yaml Full compile-time Full Automatic (11 versions) Contributors Full Automatic

Why Schema-Driven Generation Wins

  1. Correctness by construction — Models are generated from the official schema. If it compiles, the property name is valid.

  2. Zero maintenance burden — When GitLab releases 18.11, run the Design CLI, rebuild, and all new properties appear automatically with [SinceVersion("18.11.0")].

  3. Composability — The contributor pattern enables reusable pipeline fragments. Build a library of contributors for your organization's standard patterns.

  4. Round-trip fidelity — Read an existing .gitlab-ci.yml, modify it programmatically, write it back. The YAML stays clean.

  5. Version intelligence — Know exactly when each property was introduced or removed. Target a specific GitLab version with confidence.


Architecture at a Glance

GitLab.Ci.Yaml/
├── doc/
│   ├── ARCHITECTURE.md                    Design documentation
│   ├── HOW-TO.md                         Usage guide
│   └── PHILOSOPHY.md                     Design rationale
├── src/
│   ├── FrenchExDev.Net.GitLab.Ci.Yaml/                Main library (net10.0)
│   │   ├── GitLabCiBundleDescriptor.cs                [GitLabCiBundle] trigger
│   │   ├── GitLabCiVersion.cs                         Version record + comparison
│   │   ├── IGitLabCiContributor.cs                    Contributor interface
│   │   ├── GitLabCiFileExtensions.cs                  Fluent .Apply() extensions
│   │   ├── Serialization/
│   │   │   ├── GitLabCiYamlWriter.cs                  Model → YAML
│   │   │   ├── GitLabCiYamlReader.cs                  YAML → Model
│   │   │   ├── StringOrListConverter.cs               string|list handler
│   │   │   └── GitLabCiNamingConvention.cs            PascalCase ↔ snake_case
│   │   ├── schemas/                                   11 versioned JSON schemas
│   │   │   ├── gitlab-ci-v18.0.0.json
│   │   │   ├── ...
│   │   │   └── gitlab-ci-v18.10.0.json
│   │   └── obj/Generated/                             61 auto-generated .g.cs files
│   │
│   ├── FrenchExDev.Net.GitLab.Ci.Yaml.Attributes/    Marker attribute (ns2.0+net10)
│   │   └── GitLabCiBundleAttribute.cs
│   │
│   ├── FrenchExDev.Net.GitLab.Ci.Yaml.SourceGenerator/  Roslyn SG (ns2.0)
│   │   ├── GitLabCiBundleGenerator.cs                 IIncrementalGenerator
│   │   ├── SchemaReader.cs                            JSON Schema → SchemaModel
│   │   ├── SchemaVersionMerger.cs                     Multi-version merging
│   │   ├── SchemaModels.cs                            Internal data models
│   │   ├── NamingHelper.cs                            Name conversion
│   │   ├── ModelClassEmitter.cs                       Schema → C# models
│   │   ├── BuilderHelper.cs                           Schema → builder models
│   │   └── VersionMetadataEmitter.cs                  Version constants + attributes
│   │
│   └── FrenchExDev.Net.GitLab.Ci.Yaml.Design/        Schema downloader (net10.0 Exe)
│       └── Program.cs                                 GitLab API → JSON schemas
│
└── test/
    └── FrenchExDev.Net.GitLab.Ci.Yaml.Tests/         20 xUnit tests (net10.0)
        ├── ModelTests.cs                              Model + contributor tests
        ├── WriterTests.cs                             YAML serialization tests
        ├── ReaderTests.cs                             YAML deserialization tests
        ├── VersionTests.cs                            Version parsing tests
        └── Fixtures/
            └── simple-pipeline.yml                    Test data

⬇ Download