GitLab CI template: YAML anchor vs. ‘extends’ keyword

Andrzej Wojciechowski

Creating a CI pipeline can get tedious quite quickly. Fortunately, there are a couple of ways to avoid creating multiple similar jobs. In case of GitLab’s .gitlab-ci.yml files, we can use job templates. Based on templates, we can easily define the actual jobs. The two most common methods to use a GitLab CI template are YAML anchors and ‘extends’ keyword. These are very similar, but there is at least one not obvious difference.

CI job template

A GitLab’s documentation defines how to create hidden jobs. It also specifies that these can be used “as templates for reusable configuration” with YAML anchors or ‘extends’ keyword. In my previous post about a CI for LaTeX documents I’ve used and example of hidden job template. It greatly simplifies the definition of multiple similar jobs.

Imagine that we want to create a CI stage with multiple unit-tests and a second stage with multiple documentations files created. We can define each job independently, but that would result in a lot of repeated definition. What’s even a lot worse, in case of any change, we’d need to update each job individually. That can easily become a big headache.

Defining a job template

Creating a hidden job template is very simple. Simply start a job’s name with a dot (.) and define all of the common jobs’ parameters. You can even use variables that are not defined in a job template but will be created later in actual jobs. Just like in the example from my previous post.

Defining an actual job based on template

The GitLab’s documentation mentions two ways to use a hidden job template to define an actual job. We can use GitLab CI template with YAML anchors or ‘extends’ keyword. In most cases, these two can be used interchangeably. However, there is at least one import difference that I’d like to highlight.

yaml-list:
  - value1
  - value2
  - value3

yaml-dictionary:
  keyX: valueX
  keyY: valueY
  keyZ: valueZ

The YAML anchor is a standard YAML feature. The ‘extends’ keyword is GitLab CI-specific keyword. Both can be used to merge the YAML arrays. But with an important difference. In both cases, a list (with elements denoted with starting dash -) with the same name is defined in both a template job and in the actual job (using a template job), the list defined in the template is overridden. But in case of dictionaries (with elements not denoted with starting dash -), the two methods behave differently. If YAML anchors are used, the dictionaries behave like lists (they are overridden). But if ‘extends’ keyword method is used, the dictionaries from a template job and an actual job (using a template job) are merged (and only if the individual keys match, they are overridden like with lists). It sounds complicated, but it becomes clear if look at the example:

A sample .gitlab-ci.yml file:

stages:
   - test

########################################
# hidden jobs used as templates

.job_template: &job_anchor
   stage: test
   tags:
      - default_tag1
      - default_tag2
   variables:
      TEST_VAR1: "test1"
      TEST_VAR2: 42
   script:
      - echo "TEST_VAR1 = $TEST_VAR1"
      - echo "TEST_VAR2 = $TEST_VAR2"
   artifacts:
      when: on_failure
      expire_in: 1 week
      paths:
         - build/

########################################
job with extends keyword:
   extends: .job_template
   tags:
      - aaaa
   variables:
      TEST_VAR1: 11
      TEST_VAR3: 314
   script:
      - echo "TEST_VAR3 = $TEST_VAR3"
   artifacts:
      expire_in: 2 week

job with YAML anchor:
   <<: *job_anchor
   tags:
      - bbbb
   variables:
      TEST_VAR1: 11
      TEST_VAR3: 314
   script:
      - echo "TEST_VAR3 = $TEST_VAR3"
   artifacts:
      expire_in: 5 day

We defined a hidden job template called job_template, with anchor job_anchor. It defines a stage, two tags, two variables two lines of script and artifacts configuration. We also defined two separate actual jobs: one that uses a template job by ‘extends’ keyword (called job with extends keyword) and a second one that uses a template job by YAML anchor (called job with YAML anchor). Both of them define a different tag, two variables (one with the same name as a variable in template job), one line of script and a different value of one artifact configuration key.

A final merged jobs look like this:

---
stages:
- test

########################################
# hidden jobs used as templates

.job_template:
  stage: test
  tags:
  - default_tag1
  - default_tag2
  variables:
    TEST_VAR1: test1
    TEST_VAR2: 42
  script:
  - echo "TEST_VAR1 = $TEST_VAR1"
  - echo "TEST_VAR2 = $TEST_VAR2"
  artifacts:
    when: on_failure
    expire_in: 1 week
    paths:
    - build/

########################################
job with extends:
  stage: test
  tags:
  - aaaa
  variables:
    TEST_VAR1: 11
    TEST_VAR2: 42
    TEST_VAR3: 314
  script:
  - echo "TEST_VAR3 = $TEST_VAR3"
  artifacts:
    when: on_failure
    expire_in: 2 week
    paths:
    - build/
  extends: ".job_template"

job with anchor:
  stage: test
  tags:
  - bbbb
  variables:
    TEST_VAR1: 11
    TEST_VAR3: 314
  script:
  - echo "TEST_VAR3 = $TEST_VAR3"
  artifacts:
    expire_in: 5 day

We can see that in both cases the tags and script lines are overridden. In a job using YAML anchors, all of the variables from a template jobs are removed, and only the variables from the actual jobs are present in the final (merged) CI configuration. Also the artifacts configuration from a template job is removed, and only the configuration from the actual job definition are present.

In a job using ‘extends’ keyword, three variables are present: two from the job’s definition and one from the template. One of the template’s variables was overridden by a variable with the same name from the actual job definition. Similar thing happened to artifacts configuration. All configuration from the template stayed intact, except the one parameter that was overridden in the actual job definition.