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

The Problem

Everything.

Silent Failures

YAML doesn't validate keywords. If you write scrip: instead of script:, GitLab silently ignores the key. Your job runs with no commands. The pipeline passes. You deploy nothing. Nobody notices until a customer reports the bug three days later.

# Spot the bug. GitLab won't.
build:
  stage: build
  scrip:                    # <-- typo: should be "script:"
    - dotnet build
  artifact:                 # <-- typo: should be "artifacts:"
    paths:
      - bin/

Both scrip and artifact are treated as unknown keys and silently discarded. The job runs successfully — with zero commands and zero artifacts. The pipeline is green. Your deployment is empty.

Version Drift

GitLab releases monthly. Each release adds, modifies, or deprecates CI keywords. The run: keyword (an alternative to script:) appeared in GitLab 18.5. The manual_confirmation property arrived in 18.6. The inputs keyword on jobs landed in 18.6. If you target GitLab 18.3, using run: in your pipeline silently does nothing.

There's no way to know which keywords are available in which version without reading release notes. And when your GitLab instance gets upgraded, existing pipelines might break because deprecated keywords finally got removed.

The Flat Root Problem

.gitlab-ci.yml has an unusual structure that makes parsing especially tricky. The root YAML mapping contains both reserved keywords (stages, variables, include, default, workflow) and arbitrary job names as siblings:

stages: [build, test, deploy]    # reserved keyword
variables:                        # reserved keyword
  CI: "true"
build:                            # job (arbitrary name)
  script: echo building
test:                             # job (arbitrary name)
  script: echo testing
.template:                        # template (starts with .)
  image: node:20

Reserved keys, jobs, and templates all live at the same level. There's no jobs: wrapper. A parser must know which keys are reserved and which are job names — and that set of reserved keys changes across versions.

Union Types Everywhere

GitLab CI uses union types extensively. The script field can be a single string or a list of strings. The include field can be a string, a list, or an object. The environment field can be a string name or an object with name, url, on_stop, and kubernetes sub-properties. The allow_failure field can be a boolean or an object with an exit_codes property.

# All valid:
script: echo hello                    # string
script:                               # list
  - npm ci
  - npm run build

include: template.yml                 # string
include:                              # list of objects
  - local: .ci/build.yml
  - remote: https://example.com/ci.yml

environment: production               # string
environment:                          # object
  name: production
  url: https://app.example.com

In a statically typed language, these unions are a fundamental modeling challenge. You can't have a property that's simultaneously string and List<string> and object.

The Scale of the Problem

GitLab CI's JSON Schema defines 54 types across the definitions block. About 40 of those are type aliases (like script = oneOf[string, array]), and the remaining 14 are full object definitions with nested properties. A single job definition has over 30 properties spanning scripts, artifacts, environments, rules, caching, secrets, triggers, releases, and more.

Multiply that by 11 supported GitLab versions (18.0 through 18.10), each potentially adding or removing properties, and you have a combinatorial explosion that no human can track manually.

Diagram
The four pitfalls of hand-written GitLab CI YAML all funnel into pipeline failure; the generated path turns the JSON Schema into typed models, fluent builders, and version metadata.

What If the Compiler Could Catch It?

The dream: write .gitlab-ci.yml pipelines in C# with full IntelliSense, compile-time validation, and version-aware annotations — then serialize to valid YAML. Invalid property names won't compile. Removed keywords trigger [Obsolete] warnings. New keywords carry [SinceVersion("18.6.0")] annotations. And the whole thing updates automatically when GitLab releases a new schema.

That's what GitLab.Ci.Yaml does.


⬇ Download