YAML Reuse with include, extends, Anchors, !reference, Hidden Jobs, and Template Architecture: Guided Hands-On Workflow and Core Operations
This guided lab starts with duplicated jobs, extracts reusable configuration into hidden jobs and a local include, compares anchors, extends, and !reference, inspects the expanded configuration, and finishes with a safe simulation of a project include pinned to an immutable revision.
Learning objectives
- Refactor repeated jobs into a hidden template and a local include without changing intended execution.
- Compare YAML anchors, extends, and !reference using the same small pipeline so their merge behavior is observable.
- Inspect CI Lint/pipeline editor expanded configuration and identify the final effective job definitions.
- Model a project include pinned to a full SHA and explain the access/trust prerequisites without requiring a second paid project.
- Complete a challenge that selects the correct reuse/configuration layer instead of copying syntax blindly.
1. Disposable lab and preflight
Create a throwaway project named
glci-ch12-reuse-lab and branch
glci/ch12-reuse. The lab uses only local files for
mandatory execution; the project-include portion is a safe
simulation unless you already have a second disposable project you
are authorized to use. No credentials, registry pushes, runners,
cloud resources, or production namespaces are created.
2. Start with deliberate duplication so the refactor has a measurable target
Create two jobs that intentionally repeat image, setup, and variables. Run once and capture the pipeline/job IDs, source SHA, and successful output. This establishes behavior that the refactor must preserve.
stages: [test]
unit:
stage: test
image: alpine:3.20.3
variables:
TEST_MODE: unit
before_script:
- echo "prepare:$CI_COMMIT_SHA"
script:
- echo "run:$TEST_MODE"
integration:
stage: test
image: alpine:3.20.3
variables:
TEST_MODE: integration
before_script:
- echo "prepare:$CI_COMMIT_SHA"
script:
- echo "run:$TEST_MODE"
The duplication is small enough to understand. Do not optimize first; record the baseline final jobs first.
4. Inspect the expanded YAML before running
Use CI Lint or the pipeline editor’s expanded configuration. Verify that each job has the expected image, before_script, merged variables, and its own script. This is the core operational habit of the chapter: prove the compiled contract before relying on execution.
| Job | Inherited keys expected | Child keys expected |
|---|---|---|
| unit | stage, image, before_script, EVIDENCE_MODE | TEST_MODE=unit, script |
| integration | stage, image, before_script, EVIDENCE_MODE | TEST_MODE=integration, script |
5. Compare a file-local anchor
Now create an anchor in the root file for one array. Anchors are convenient when the reused structure is truly local to one YAML document.
.common_steps: &common_steps
- echo "sha=$CI_COMMIT_SHA"
- echo "pipeline=$CI_PIPELINE_ID"
audit:
image: alpine:3.20.3
script:
- *common_steps
- echo "audit complete"
Move that anchor definition into ci/templates.yml while
leaving the alias in the root file. Validation should fail because
YAML anchors cannot cross file boundaries. Preserve the validation
error as evidence, then restore the anchor to the same file.
6. Compare !reference for a cross-file array
For cross-file array reuse, define a named rules block in the include and select it explicitly.
# ci/templates.yml
.test_rules:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_PIPELINE_SOURCE == "push"'
# .gitlab-ci.yml
unit:
extends: .base_test
rules: !reference [.test_rules, rules]
script:
- echo "unit"
The expanded configuration should show the concrete rules array on
unit. This makes the origin explicit without pretending
arrays were appended by extends.
7. Observe a safe override and an unsafe assumption
First override one scalar/hash value deliberately:
unit:
extends: .base_test
image: alpine:3.20.3
variables:
TEST_MODE: unit
EVIDENCE_MODE: "unit-override"
script:
- echo "$TEST_MODE:$EVIDENCE_MODE"
The child variable overrides only that variable while other parent
variables remain. Next, add a child
before_script expecting it to append. Inspect expanded
YAML: the child array replaces the parent array. This is the
deliberate lesson, not an error to hide.
8. Simulate a version-pinned project include
Model the configuration you would use for a second trusted GitLab project. Use a fake project path and a fake-looking but syntactically full SHA in the lesson; do not try to fetch it in the mandatory lab.
include:
- project: 'training-ci/reuse-library'
ref: '0123456789abcdef0123456789abcdef01234567'
file: '/templates/base-test.yml'
In a real authorized disposable setup, replace those values with the exact project and full commit SHA, then verify that the user creating the pipeline has sufficient access. Record the template project, SHA, and file path in the evidence packet.
9. Understand remote include integrity without making the lab depend on the internet
A remote include is fetched as an unauthenticated public HTTP(S)
resource. Current GitLab supports an integrity value
using a base64-encoded SHA-256. The safe pattern is to compute the
reviewed file’s digest out of band and pin that digest in
configuration.
include:
- remote: 'https://example.invalid/ci/reviewed.yml'
integrity: 'sha256-BASE64_SHA256_OF_REVIEWED_CONTENT'
The example uses an intentionally non-routable documentation domain so it cannot mutate or depend on a real external system. Treat it as architecture, not as a runnable mandatory path.
10. Challenge: choose the right reuse layer
You have three repeated things: (a) two script lines in the same file, (b) a complete test-job template shared by files in the same repository, and (c) a versioned CI building block used by twenty projects. Choose the simplest mechanism for each and justify how you will prove the effective configuration and dependency identity.
A strong answer usually keeps (a) local with an anchor or direct
duplication if trivial, uses a hidden job plus
extends/!reference for (b), and moves (c)
toward a versioned project include or, after Chapter 13, a CI/CD
component with explicit inputs.
11. Cleanup and rollback
Delete only the disposable branch/project created for the lab after saving the evidence packet. No shared templates, protected refs, runners, or instance settings were changed. If you tested a second disposable project, remove only that exact project after confirming no other learner uses it.
Knowledge check
Why validate the expanded YAML before pushing the refactor?
Because source fragments can look reasonable while merge/inheritance semantics produce a different effective job. Expanded YAML proves the compiled job contract before runner execution.
Why did the anchor fail after moving its definition into another included file?
YAML anchors are scoped to one YAML document/file and cannot cross include boundaries.
What proves that extends preserved the intended behavior?
The expanded configuration shows the expected inherited/overridden keys, and the new pipeline at the same intended source behavior produces equivalent job evidence.
Why is a project include at a full commit SHA safer than main?
The dependency cannot silently move between pipeline creations; the SHA identifies exactly which template revision was reviewed.
A child defines before_script. Why did the parent before_script disappear?
extends merges hashes by key but array values are replaced when the child defines the same key; they are not automatically concatenated.
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.