.gitlab-ci.yml Structure, Pipeline Compilation, Keywords, Defaults, and Configuration Validation: Diagnostics, Failure Modes, Security, and Performance
Most CI configuration incidents are diagnosed faster by asking where configuration stopped becoming executable: YAML parse, GitLab schema, include resolution, merge/inheritance, pipeline creation, job inclusion, queueing, or runtime. This lesson engineers representative failures and repairs them without destroying the first evidence that explains the cause.
Learning objectives
- Diagnose bad YAML typing, unsupported/deprecated patterns, include-resolution failure, hidden inheritance, and valid configuration that creates no pipeline.
- Preserve source SHA, configuration revision, lint output, expanded configuration, pipeline/job identity, and first-failure logs before repair.
- Separate compilation/rule failures from runner, shell, token, artifact, deployment, and external-system failures.
- Treat remote includes and configuration generators as executable supply-chain dependencies with explicit trust boundaries.
- Apply the least destructive correction and re-run only the smallest validation or execution scope needed to prove the repair.
1. Evidence-first diagnostic ladder
flowchart TD
A[Preserve source SHA + original error] --> B[Parse / GitLab lint]
B --> C[Resolve includes]
C --> D[Inspect merged defaults + inheritance]
D --> E[Simulate pipeline creation]
E --> F[Inspect final jobs]
F --> G[Queue + runner eligibility]
G --> H[Runtime script/tool/network]
H --> I[Artifacts/deployment/external state]
This order minimizes false leads. A missing include can never be
fixed by adding runner capacity. A when: never workflow
does not become runnable because the shell executor is healthy.
Conversely, once a job is running, reformatting YAML is unlikely to
explain a command exit code unless the compiled job shows different
commands than expected.
2. Failure case: bad YAML typing or structure
stages: verify # wrong type: stages expects a list
verify:
stage: verify
script:
- echo ok
Preserve the lint error text and the exact file revision. This is a pre-pipeline failure. There is no pipeline ID or runner log to inspect. Repair to:
stages:
- verify
3. Failure case: supported today but deprecated configuration
# Legacy/deprecated global pattern
image: alpine:3.22
before_script:
- echo preflight
verify:
script:
- echo verify
Current GitLab documentation deprecates globally defined
image, services, cache,
before_script, and after_script. The safer
current pattern moves supported values under default. A
deprecated construct is not the same as an immediate runtime
failure; treat it as version/migration debt and preserve warnings.
default:
image: alpine:3.22
before_script:
- echo preflight
4. Failure case: include resolution fails
include:
- local: /.gitlab/ci/does-not-exist.yml
verify:
script:
- echo verify
CI Lint in project context must resolve local includes. If the file is missing at the relevant revision, configuration cannot compile. Check the exact path, case, repository revision, and include context before changing runner settings.
git ls-tree -r --name-only HEAD .gitlab/ci/
git show HEAD:.gitlab-ci.yml
5. Failure case: hidden inheritance changes runtime behavior
This failure is more subtle because the configuration can validate and a pipeline can run. The job silently opts out of a default bootstrap command:
default:
before_script:
- printf 'ready\n' > .chapter02-ready
verify:
inherit:
default: false
script:
- test -f .chapter02-ready
The job fails at runtime because the marker was never created. The
first useful evidence is the compiled job: it shows
inherit:default:false and no inherited
before_script. Runner logs then confirm the expected
consequence. Fix the configuration, not the runner.
6. Failure case: configuration is valid but no pipeline is created
workflow:
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
- when: never
verify:
script:
- echo verify
A push can produce no pipeline by design. Static lint success does
not contradict that result. Inspect source context and simulate
pipeline creation. The absence of a pipeline is evidence about
workflow, not about runner availability.
7. Configuration dependencies are executable supply-chain dependencies
Remote/project includes can inject commands into jobs that later receive repository credentials, job tokens, protected variables, or deployment authority. Treat those includes like executable code dependencies. Review ownership, pin immutable refs where practical, use integrity/version controls where supported, minimize forwarded secrets, and keep privileged jobs isolated from untrusted configuration.
8. Configuration complexity can become a reliability/performance problem
GitLab documents a time limit for resolving all includes and a default cap on include count. Large nested graphs also impose human review cost even before platform limits are reached. Measure the actual need for abstraction: split by stable domain boundary, not simply because YAML can be split.
| Signal | Likely design response |
|---|---|
| Full configuration is hard to review | Reduce indirection or consolidate tiny include layers. |
| Many includes repeat the same policy | Move toward a versioned component/template contract. |
| Pipeline creation is slow before any job queues | Inspect include graph, conditional includes, generated config, and server-side compilation. |
| Job runtime is slow after start | This is no longer a configuration-compilation performance problem; move to runner/script/cache diagnostics. |
9. Failure taxonomy: preserve the boundary
| Observed state | Next evidence | Do not start with |
|---|---|---|
| YAML/schema invalid | Lint error + source revision | Runner logs |
| Include unresolved | Include path/ref/access + expanded config | Shell commands |
| Expanded job differs from expectation | Merge/default/inherit evidence | Network troubleshooting |
| No pipeline |
Pipeline source + workflow:rules simulation
|
Runner capacity |
| Job pending | Runner eligibility/capacity | Rewriting YAML formatting |
| Job running then fails | Job trace + tool/network/exit status | Pipeline-creation rules |
10. Least-destructive repair procedure
- Copy the original failing CI files and lint/simulation output into an evidence folder.
- Record the exact source SHA and pipeline source/ref if a pipeline exists.
- Inspect Full configuration / compiled YAML before editing.
- Change the smallest configuration unit that explains the failure.
- Re-run static lint first. If the failure involved pipeline creation, re-run simulation next.
- Run a disposable pipeline only if runtime evidence is still required.
- Compare the repaired evidence to the preserved first failure; do not simply declare “green.”
11. Intentionally broken diagnosis exercise
Given this pair of files, explain why verify fails and
which artifact proves it:
# .gitlab/ci/base.yml
default:
before_script:
- printf 'prepared\n' > .prepared
# .gitlab-ci.yml
include:
- local: /.gitlab/ci/base.yml
verify:
inherit:
default: false
script:
- test -f .prepared
The configuration can compile. The expanded verify job
demonstrates that before_script is absent. A runtime
failure then confirms the causal consequence. The repair is either
remove the opt-out or explicitly create the prerequisite in the job;
changing runner permissions would be unrelated.
Knowledge check
A job never appears because its include file cannot be resolved. Should you inspect runner tags first?
No. Configuration compilation failed before any job could be queued.
Why is a deprecated keyword warning different from an invalid-key error?
Deprecated syntax can still work for compatibility while being scheduled for future removal; invalid syntax prevents valid compilation now.
A job runs but a default before_script is missing.
What should be inspected before blaming the shell?
The compiled job and its inherit/override state.
What proves that a no-pipeline result is intentional rather than a runner problem?
Pipeline-source context plus workflow:rules and
lint/simulation evidence showing the pipeline creation decision.
Why pin or integrity-protect reusable external configuration?
Because CI configuration is executable dependency code; mutable external content can change job behavior without a source change in the consumer repository.
Official references and version notes
-
CI/CD YAML syntax reference
— current global/job keyword semantics, including
default,inherit, andinclude. - Use CI/CD configuration from other files — include resolution, merge order, overrides, nesting, and snapshot behavior.
- Validate GitLab CI/CD configuration — CI Lint, syntax checking, and pipeline simulation.
- Pipeline editor — included-file tree, visualization, continuous validation, and Full configuration.
-
Deprecated CI/CD keywords
— current migration guidance, including global
image/services/cache/before_script/after_scriptand legacyonly/except. -
glab ci lintandglab ci config— current CLI surfaces for validation and compiled configuration inspection.
Version-sensitive statements in this chapter were rechecked
against current primary GitLab documentation on
2026-09-11. Core CI Lint, Pipeline Editor
validation, default, inherit, and local
includes are documented across GitLab Free, Premium, and Ultimate
on GitLab.com, GitLab Self-Managed, and GitLab Dedicated. The labs
deliberately avoid depending on paid policy features, production
credentials, external cloud accounts, or privileged runner
administration.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.