YAML Reuse with include, extends, Anchors, !reference, Hidden Jobs, and Template Architecture: Concepts, Architecture, and Mental Model
Reusable CI configuration is executable dependency code, not just a way to make YAML shorter. This lesson builds a mental model from the root .gitlab-ci.yml through the include graph, hidden jobs, anchors, extends and !reference expansion, merged configuration, caller overrides, and the final jobs GitLab actually compiles.
Learning objectives
- Explain why CI reuse is simultaneously a compilation problem, an interface-design problem, and a supply-chain trust problem.
- Trace the root CI file through include resolution, hidden jobs, anchors, extends, !reference, merged configuration, overrides, and final jobs.
- Distinguish file-local YAML anchors from GitLab-aware reuse mechanisms that can cross include boundaries.
- Inspect include source/ref/integrity and the expanded configuration before changing pipeline behavior.
- Recognize which reuse choices affect repository state, compiled configuration, authorization, and downstream execution evidence.
1. The practical problem: duplicated YAML hides dependencies instead of removing them
Copying the same image, setup, rules, cache, and script fragments into many jobs feels simple when a pipeline is small. As the pipeline grows, copies drift. One job receives a security fix while another keeps the old behavior; a branch-specific edit changes only one copy; reviewers must compare long blocks instead of reasoning about one shared contract.
Reuse helps only when you know exactly where the effective configuration comes from. GitLab does not execute the source fragments independently. It first resolves included configuration, applies GitLab/YAML reuse mechanisms, merges definitions, applies caller overrides, evaluates pipeline/job rules, and only then creates final jobs. Therefore the object to audit is the compiled configuration, not merely the root file.
2. Keep five reuse states separate
| State | Question to answer | Evidence |
|---|---|---|
| Root source | Which repository/ref/SHA supplied the root .gitlab-ci.yml? | Project path, ref, source SHA |
| Include graph | Which local/project/remote/template files were fetched, under which identities? | Include type, project/URL, ref/SHA, integrity |
| Reuse primitives | Which hidden jobs, anchors, extends parents, and !reference paths were selected? | Source YAML + expanded configuration |
| Merged configuration | What effective keys/arrays/variables/rules exist after merging and overrides? | CI Lint or pipeline editor expanded YAML |
| Final execution | Which jobs/rules entered the graph and on which runner/executor? | Pipeline/job records, traces, runner metadata |
These states fail independently. A remote include can fail before a pipeline exists. A valid merged job can later be omitted by rules. A correct job can wait forever for a runner. Reuse diagnosis therefore begins in the configuration layer and moves outward only after compilation is proven.
3. Mental model: from root file to final jobs
Read the diagram left to right. The root file names dependencies. GitLab resolves the include graph. File-local YAML features and GitLab reuse features shape candidate definitions. GitLab merges them, applies the root/caller overrides, validates the result, and only then produces final jobs that can enter pipeline creation and execution.
The key causality is that changing an include ref or an inherited parent changes the compiled program even when the root file itself is unchanged.
4. Include is dependency resolution, not textual paste
Current GitLab supports local, project, remote, template, and component includes, plus subkeys such as inputs, rules, and integrity where applicable. Includes are evaluated first; then the root configuration is merged and can override included keys. The resolution step has a 30-second time limit and a default maximum of 150 includes including nested includes.
| Include type | Ownership/trust question | Reproducibility guidance |
|---|---|---|
| local | Same repository/revision? | Strongest simple choice; file is tied to the same source revision |
| project | Which GitLab project and ref? | Prefer full 40-character SHA or a protected release tag |
| remote | Who controls the public URL? | Pin content with integrity; remote fetch has no authentication |
| template | Which GitLab-shipped template version applies? | Re-check template/deprecation behavior for deployed GitLab version |
| component | What versioned interface/input contract? | Covered deeply in Chapter 13; prefer explicit version and inputs |
5. Pipeline configuration is a snapshot
When GitLab creates a pipeline, it resolves and stores the configuration snapshot used for that pipeline. Retrying a job does not refetch changed include files; rerunning/creating a new pipeline resolves the includes again. This explains an important diagnostic pattern: “the template changed after my failure” does not explain why a retry of the same job suddenly should compile differently—it does not.
For audit evidence, preserve the pipeline ID, source SHA, and expanded configuration together. A moving include branch can produce different future pipelines from the same root-file text.
7. YAML anchors are file-local structural reuse
Anchors and aliases are YAML features, not GitLab cross-file
references. They are useful for sharing maps or arrays inside one
file, but they cannot cross an include boundary. When
duplicate keys occur, later YAML keys override earlier ones. For
cross-file reuse, use extends or
!reference instead.
.common_scripts: &common_scripts
- python --version
- python -m pip --version
lint:
script:
- *common_scripts
- python -m compileall src
8. extends is GitLab-aware hash inheritance
extends can reuse jobs defined in included files.
GitLab performs a reverse deep merge by key. Hashes such as
variables can merge; scalar values and arrays such as
script, tags, and rules are
replaced when a child provides that key. That replacement behavior
is one of the most important failure modes in this chapter.
GitLab supports multiple inheritance levels (up to eleven), but the documentation recommends avoiding more than about three because the compiled behavior becomes difficult to review.
9. !reference selects configuration by path across files
!reference is a GitLab custom YAML tag that can select
a keyword or nested configuration from another job, including a job
loaded through an include. This is useful when you need an array or
a precise subsection rather than a whole-job hash merge. It is
resolved during configuration parsing, not at shell runtime.
# templates.yml
.rules_for_tests:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_PIPELINE_SOURCE == "push"'
# .gitlab-ci.yml
include:
- local: templates.yml
test:
script: echo test
rules: !reference [.rules_for_tests, rules]
Do not confuse !reference with variables or CI/CD
inputs. Current GitLab documentation describes parsing constraints
between !reference and inputs because references are
resolved at a different phase.
10. Read-only inspection before editing reuse
Start by locating the root source SHA and every include. Then open
CI Lint or the pipeline editor’s expanded configuration. The
expanded view resolves includes, anchors, extends, and
!reference, allowing you to inspect the final job
rather than mentally simulate inheritance.
printf 'source=%s
ref=%s
sha=%s
pipeline=%s
' "$CI_PIPELINE_SOURCE" "$CI_COMMIT_REF_NAME" "$CI_COMMIT_SHA" "$CI_PIPELINE_ID"
Do not dump all variables or tokens while “debugging templates.” Include dependencies are configuration state; secret values are unrelated and should stay redacted.
11. Common wrong models
- “include is just copy/paste.” No. It has access, resolution, snapshot, limit, merge, and trust semantics.
- “anchors work across files.” They do not.
- “extends appends arrays.” It does not; array values are generally replaced.
- “main is pinned.” A branch is moving. Prefer a full SHA or protected version tag for cross-project executable configuration.
- “remote HTTPS means trusted.” HTTPS protects transport to a host; it does not prove that the content is the reviewed template. Use immutable identity/integrity where practical.
Knowledge check
Why is the expanded configuration stronger evidence than the root .gitlab-ci.yml alone?
Because includes, inheritance, references, defaults, and caller overrides can change the effective job. The expanded configuration shows what GitLab actually compiled.
Why can a retry of one job keep using an old included template after that template changed?
The pipeline stores a configuration snapshot when it is created. Job retries do not refetch include files; a new pipeline does.
Why is a full commit SHA preferable to ref: main for include:project?
A full SHA is immutable and identifies exactly which reviewed template content was compiled, while main can move.
When should you prefer !reference over an anchor?
When the reused configuration comes from another included file or when selecting a precise GitLab configuration section. YAML anchors are file-local.
What is the first thing to inspect when an inherited script unexpectedly disappears?
The expanded job and its inheritance chain. A child array value such as script can replace, rather than append to, the parent array.
Version and compatibility note
GitLab and GitLab Runner evolve continuously. Treat version-sensitive YAML, runner/executor behavior, APIs, security features, policy controls, tiers, and deprecations as assumptions to verify against the current official GitLab documentation before production use. Preserve the exact project/ref/SHA, compiled configuration, pipeline/job IDs, runner/tool versions, and external target evidence used for any reproducible lab or incident record.
Official references and version notes
Documentation verification date: 2026-09-11. CI reuse and include behavior evolves across GitLab releases, especially include subkeys, integrity/caching, inputs/components, and template deprecations. Re-check the deployed GitLab version before relying on newer include features on Self-Managed or Dedicated installations.
-
CI/CD YAML syntax reference
— current
includetypes/subkeys, merge behavior,extends, hidden jobs, defaults, and pipeline syntax. - Use CI/CD configuration from other files — local/project/remote/template include behavior, variables, rules, overrides, troubleshooting, and trust guidance.
-
Optimize GitLab CI/CD configuration files
— anchors, hidden jobs,
extends, reverse deep merge, and!reference. -
Pipeline editor
— validation and expanded configuration showing includes, anchors,
extends, and!referenceresolved. - CI Lint — validate and simulate configuration before creating a real pipeline.
- CI/CD inputs — typed configuration interfaces that become increasingly important in Chapter 13.
integrity with a
base64 SHA-256. YAML anchors cannot cross files.
extends reverse-deep-merges hashes but array values are
replaced, not concatenated. GitLab supports up to eleven inheritance
levels but recommends avoiding deep inheritance;
!reference can select configuration from included files
and has parsing constraints with CI/CD inputs.
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.