Chapter 12Lesson 02~180 minutes

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.

YAML reuseincludeextends!referenceTemplate trust

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.

Assumptions: current GitLab Free-compatible CI syntax; any normal Runner capable of executing Alpine/Python-style shell commands. Validate with CI Lint before pushing. Record the exact GitLab version/offering if your instance is Self-Managed or Dedicated because include features can be version-sensitive.

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.

3. Extract a hidden template into a local include

Create ci/templates.yml. Because include:local reads from the same repository/revision, the dependency identity naturally travels with the source SHA.

# ci/templates.yml
.base_test:
  stage: test
  image: alpine:3.20.3
  before_script:
    - echo "prepare:$CI_COMMIT_SHA"
  variables:
    EVIDENCE_MODE: "chapter12"
# .gitlab-ci.yml
include:
  - local: /ci/templates.yml

stages: [test]

unit:
  extends: .base_test
  variables:
    TEST_MODE: unit
  script:
    - echo "run:$TEST_MODE:$EVIDENCE_MODE"

integration:
  extends: .base_test
  variables:
    TEST_MODE: integration
  script:
    - echo "run:$TEST_MODE:$EVIDENCE_MODE"

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?

Why did the anchor fail after moving its definition into another included file?

What proves that extends preserved the intended behavior?

Why is a project include at a full commit SHA safer than main?

A child defines before_script. Why did the parent before_script disappear?

Next lesson

Configuration, design choices, and tradeoffs

Choose reuse mechanisms and dependency identities from ownership, trust, merge behavior, portability, and rollback requirements.

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.

Current behavior used by this chapter: includes are resolved first and the root configuration is merged afterward; all include resolution has a 30-second time limit and the default nested-include limit is 150. A project include can pin a full 40-character commit SHA. Remote includes are public HTTP(S) GETs without authentication and can use 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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.