.gitlab-ci.yml Structure, Pipeline Compilation, Keywords, Defaults, and Configuration Validation: Guided Hands-On Workflow and Core Operations
Build Chapter 02 configuration progressively in a disposable project: begin with one visible job, introduce safe defaults, split configuration with a local include, inspect the fully expanded configuration, then deliberately create one structural failure and one valid-but-no-pipeline semantic failure. The goal is to prove which validation layer catches which class of problem.
Learning objectives
- Create a minimal configuration, add defaults, and split reusable configuration into a local include without introducing hidden external dependencies.
-
Inspect fully expanded configuration with the Pipeline Editor or
current
glabcommands before running jobs. - Diagnose a structural validation error separately from a configuration that is valid but intentionally creates no pipeline.
- Predict which jobs and inherited settings should exist before pipeline creation, then compare predictions with compiled evidence.
- Complete the lab without real secrets, production runners, paid features, or uncontrolled external side effects.
1. Scenario and preflight
Create or select a disposable project such as
GROUP/glci-ch02-config-lab. Record the current revision
and existing CI state before writing files. If the project already
has valuable CI configuration, use a dedicated disposable project
instead of overwriting it.
LAB_BRANCH="ch02/config-compilation"
mkdir -p ch02-evidence .gitlab/ci
glab auth status
git status --short --branch
git rev-parse HEAD | tee ch02-evidence/head-before.txt
test ! -e .gitlab-ci.yml || { echo "Existing .gitlab-ci.yml detected; use a disposable project/branch."; exit 2; }
2. Start from an empty file and one visible job
verify_config:
script:
- printf 'source=%s\n' "$CI_PIPELINE_SOURCE"
- printf 'sha=%s\n' "$CI_COMMIT_SHA"
This is intentionally small. It defines one visible job and one script. Do not add images, services, variables, caches, artifacts, or deployment state yet. First validate the smallest GitLab CI document and preserve the result.
glab ci lint .gitlab-ci.yml | tee ch02-evidence/01-minimal-lint.txt
3. Add safe defaults, then inspect what changed
default:
retry: 1
interruptible: true
before_script:
- printf 'preflight=chapter02\n'
verify_config:
script:
- printf 'source=%s\n' "$CI_PIPELINE_SOURCE"
- printf 'sha=%s\n' "$CI_COMMIT_SHA"
The important step is not typing default; it is proving
the compiled job now contains those inherited settings. Use the
Pipeline Editor Full configuration tab or the current CLI compiler.
glab ci lint .gitlab-ci.yml --include-jobs \
| tee ch02-evidence/02-defaults-lint.txt
glab ci config compile \
| tee ch02-evidence/02-defaults-compiled.yml
4. Split one local include without changing the trust boundary
Create a repository-local file. A local include is resolved from the same repository/branch context as the file that includes it, so this exercise teaches compilation without introducing a second project or public URL.
# .gitlab/ci/base.yml
default:
retry: 1
interruptible: true
before_script:
- printf 'preflight=chapter02\n'
# .gitlab-ci.yml
include:
- local: /.gitlab/ci/base.yml
stages:
- verify
verify_config:
stage: verify
script:
- printf 'source=%s\n' "$CI_PIPELINE_SOURCE"
- printf 'sha=%s\n' "$CI_COMMIT_SHA"
standalone_check:
stage: verify
inherit:
default: false
script:
- printf 'standalone=true\n'
Predict first: verify_config should contain the
inherited defaults; standalone_check should not. Then
inspect the expanded result and the included-file tree in Pipeline
Editor.
5. Static lint, compiled configuration, and pipeline simulation answer different questions
# Static validation and jobs reported by current glab:
glab ci lint .gitlab-ci.yml --include-jobs \
| tee ch02-evidence/03-lint-with-jobs.txt
# Fully merged/compiled configuration:
glab ci config compile \
| tee ch02-evidence/03-compiled.yml
# Simulate pipeline creation for a branch/ref when supported by your current glab:
glab ci lint .gitlab-ci.yml --dry-run --ref "$LAB_BRANCH" --include-jobs \
| tee ch02-evidence/03-simulation.txt
CI Lint simulation is more than a syntax check: it asks GitLab to
simulate pipeline creation and can expose problems involving rules
or graph semantics. The UI simulation documented by GitLab uses a
push event on the default branch; CLI
--ref lets you select a branch/tag context for its dry
run.
6. Failure injection A: create a structural/type error
Preserve the working file first, then make stages the
wrong YAML type. GitLab expects a sequence of stage names, not one
scalar.
cp .gitlab-ci.yml ch02-evidence/04-good-before-structural.yml
# Deliberately broken fragment:
stages: verify
verify_config:
stage: verify
script:
- echo "this configuration should not pass GitLab CI validation"
Run CI Lint and save the failure. Do not “fix while reading” and lose the original diagnostic text. The correct conclusion is configuration validation failed before runner assignment.
glab ci lint .gitlab-ci.yml \
> ch02-evidence/04-structural-error.txt 2>&1 || true
cat ch02-evidence/04-structural-error.txt
cp ch02-evidence/04-good-before-structural.yml .gitlab-ci.yml
7. Failure injection B: valid configuration that intentionally creates no pipeline
Now create a different class of problem. The YAML and GitLab keywords are valid, but pipeline creation is suppressed.
workflow:
rules:
- when: never
verify_config:
script:
- echo "this job is valid but no pipeline should be created"
A static configuration check can be valid because the configuration is structurally legitimate. Pipeline simulation or a real source event is where the no-pipeline outcome becomes visible. This is the exact reason validation state and pipeline-creation state must remain separate.
8. Compare compiled evidence with runtime evidence
Restore the working include-based configuration. If an eligible disposable runner exists, commit it on the lab branch and create a pipeline. If no runner exists, stop at lint/simulation and record that limitation; do not register a privileged runner merely to make the lab green.
git switch -c "$LAB_BRANCH"
git add -- .gitlab-ci.yml .gitlab/ci/base.yml
git diff --cached --check
git diff --cached
git commit -m "ch02: add compiled configuration lab"
LAB_SHA="$(git rev-parse HEAD)"
printf '%s\n' "$LAB_SHA" | tee ch02-evidence/lab-sha.txt
# Optional only in an authorized disposable project:
git push -u origin "$LAB_BRANCH"
Exact commit SHA and root/include file paths.
Lint result and expanded/compiled YAML.
Pipeline source/ref/SHA and final job names, if created.
Runner/executor metadata only if jobs actually start.
9. Challenge: pick the owning layer
Classify each symptom before changing anything:
| Symptom | First owning layer | Why |
|---|---|---|
CI Lint reports stages has the wrong type.
|
Configuration validation | No pipeline/job exists yet. |
| Static lint is valid; simulation yields no pipeline. | Pipeline creation / workflow rules | Runner capacity is irrelevant. |
| Pipeline and job exist; job stays pending. | Runner eligibility/capacity | Configuration already materialized the job. |
| Job starts; shell command exits 1. | Runtime script/tool layer | Compilation and runner assignment already succeeded. |
10. Cleanup and verification
Preserve evidence long enough to compare the failures, then remove only the disposable branch/project state you created. Confirm exact branch identity before deletion.
git status --short
git rev-parse HEAD
git ls-remote --heads origin "$LAB_BRANCH"
# Optional cleanup after verification in a disposable project:
# git push origin --delete "$LAB_BRANCH"
# git switch main
# git branch -D "$LAB_BRANCH"
Knowledge check
Why deliberately save the structural-error output before restoring the file?
The first error message is evidence of the failing layer. Repairing first can erase the best explanation of the original failure.
A file passes static CI Lint but
workflow:rules contains only
when: never. What should simulation show?
The configuration can be valid while pipeline creation is suppressed, so no runnable job graph should materialize.
Why use a local include in this chapter instead of a remote URL?
It teaches include resolution and merge behavior while keeping configuration in one repository/revision and avoiding a new external trust dependency.
A job lacks the expected before_script. What
evidence should you inspect before runner logs?
The fully expanded/compiled job and its
inherit state.
If no runner is available, is the lab incomplete?
No. Compilation, linting, full-configuration inspection, and pipeline simulation are core Chapter 02 outcomes; runtime is an optional extra evidence layer.
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.