skip to content

In GitLab CI, how does extends: combine a hidden .template job with the job that extends it, and why can a YAML anchor not do the same thing across included files?

level: middleimportance: must knowfreq 58%

answer

  1. dot prefix means never runs
  2. GitLab-level merge vs YAML-parser-level
  3. deep merge for maps, replace for arrays
  4. anchors die at the file boundary
  5. child keys win; null removes

basics

~20 s

extends performs a recursive merge of the template job's keys into the extending job, with the extending job's own values winning. YAML anchors are resolved by the YAML parser inside a single file, so they cannot reference a node defined in an included file; extends can, because GitLab merges all included files first.

solid answer

~50 s

A job name starting with a dot — `.build-template` — is a **hidden job**: GitLab never runs it, it exists only to be reused. `extends:` names one or more of those templates, and GitLab merges them into the job recursively: nested hash keys are merged, and the extending job's own values win on conflict. Arrays such as `script:` are **not** merged — the child's array replaces the parent's. A YAML anchor with `<<:` looks similar but is a different layer: anchors and aliases are resolved by the YAML parser while it reads one document, so an anchor defined in an included file simply does not exist in your file, and the merge key is shallow. `extends` runs after GitLab has flattened every include into one configuration, which is why it is the only one of the two that works across repositories — the reason shared-template repositories are built on it.

go deeper

for a junior

Recognise that a job name beginning with a dot never runs and exists as a template, and that extends: pulls that template's keys into a real job. Say that the real job's own values win.

for a middle

Explain the merge precisely: maps merged recursively, arrays replaced, child wins, null removes a key, multiple parents merged in order. State that anchors are parsed per file while extends works on the merged configuration.

for a senior

Argue why a shared-template repository must be built on extends rather than anchors, and show how you debug an unexpected merged job using the pipeline editor's merged-configuration view rather than reading five files.

for a principal

Own the depth and shape of the inheritance graph across an organisation: how many levels a template hierarchy may have, whether teams override or compose, and when to move from extends-based templates to versioned components with declared inputs.

## Hidden jobs: the template unit Any job whose name starts with a dot is hidden. GitLab parses it, validates nothing about it as a job, and never schedules it: ```yaml .node-job: image: node:22 before_script: - npm ci tags: [docker] ``` That is the reusable unit. It can hold any job keys, including keys that would be invalid on their own, and it costs nothing at runtime. ## What `extends` actually does ```yaml test: extends: .node-job script: - npm test ``` GitLab merges `.node-job` into `test`. The rules that matter: **It is a deep merge of hash maps.** If both the template and the job define `variables:`, the two maps are merged key by key — you keep the template's variables and add your own, rather than losing them. **The extending job wins on conflict.** Setting `image: node:20` in `test` overrides the template's `image` and leaves everything else intact. **Arrays are replaced, not concatenated.** `script:`, `tags:`, `rules:` and friends are arrays. If the job defines `script:`, the template's `script:` is gone. This is the single most common surprise, and the reason `!reference` exists. **Multiple parents are allowed.** `extends: [.node-job, .protected-only]` merges several templates. Conflicts between the parents are resolved by their order in the list, so relying on subtle overlaps between parents is fragile — keep templates disjoint. **Inheritance can nest.** A template may itself extend another template; GitLab supports several levels (documented as up to eleven), but two or three is the practical readable limit. Deep chains make it genuinely hard to answer "where does this `image` come from", and the pipeline editor's merged-YAML view becomes the only reliable answer. ## Why YAML anchors are a different animal YAML has its own reuse mechanism: ```yaml .node-job: &node_job image: node:22 before_script: - npm ci test: <<: *node_job script: - npm test ``` This works — but it works at a **lower layer**. `&node_job` defines an anchor and `*node_job` is an alias to it, both resolved by the YAML parser as it reads that one document. Two consequences follow, and they are what interviewers are testing: 1. **Anchors do not cross file boundaries.** Each included file is parsed as its own document. An anchor defined in `platform/ci-templates` is meaningless in your `.gitlab-ci.yml`, because by the time GitLab merges the two, YAML parsing is long finished. `extends`, by contrast, operates on the already-merged configuration, so it can name a hidden job that came from any included file. For a platform team, that is decisive: shared templates are built on `extends`, not anchors. 2. **The merge key `<<:` is shallow.** It copies the top-level keys of the mapping. Nested maps are taken wholesale, not merged, so you cannot add one variable to an inherited `variables:` block — you replace the whole block. `extends` merges it. Anchors still have a place: reusing a *value* or a fragment inside a single file, where you want plain YAML semantics and no GitLab-specific behaviour. Mixing both in the same file is legal, and GitLab resolves anchors first, then `extends`. ## Overriding, and the trick for removing a key Because the child wins, overriding is easy. *Removing* an inherited key is the awkward case: set it to `null` (or the YAML `~`) in the child and GitLab drops it from the merged job. ```yaml test: extends: .node-job before_script: null # drop the inherited npm ci script: - npm test ``` ## How to verify what you actually got Never reason about a five-level `extends` chain in your head. GitLab's pipeline editor has a merged-configuration view (and the CI lint endpoint can expand includes), which shows the fully resolved YAML with every include and every `extends` applied. That view is the ground truth, and it is the answer to "why is this job running on the wrong runner" far more often than any amount of reading. ## The interview shape The question is usually asked as "we have fifty repos with the same pipeline — what do you do?", and the expected answer is: hidden jobs in a template project, pulled in with `include:project` at a pinned ref, consumed with `extends`. Anchors are the answer a candidate gives when they have only ever worked inside a single `.gitlab-ci.yml`.

  • If a template and the job that extends it both define variables:, what does the merged job get?
    Both sets. `variables:` is a hash map, and `extends` merges maps recursively, so the job keeps the template's variables and adds its own; where the same variable name appears in both, the extending job's value wins. That is different from a YAML `<<:` merge key, which would replace the whole nested map.
  • How do you remove a key that a template gave you, such as an inherited before_script?
    Set it to `null` (or `~`) in the extending job. GitLab drops keys whose merged value is null, so `before_script: null` produces a job with no before_script. Setting it to an empty array is not the same thing and is easier to misread — explicit null is the idiom.
  • Can a hidden job extend another hidden job?
    Yes — templates commonly form a small hierarchy, and GitLab supports several levels of inheritance (documented up to eleven). Keep it to two or three: deep chains make it very hard to see where a key came from, and the pipeline editor's merged view becomes the only way to answer that.

saying these in an interview costs you the question

  • Claiming YAML anchors work across included files
  • Expecting script: arrays to be concatenated by extends
  • Thinking a hidden job runs if nothing extends it
  • Believing <<: merges nested maps recursively
  • Treating extends and anchors as the same mechanism

context