One of the most frequently asked questions during many interviews for a programmer position is to explain the SOLID principles. If you want to be a good programmer, you probably know what they are. These principles can be applied in a similar form to many branches of software development, not only the implementation itself. One of the key principles is the "don't repeat yourself" rule, which is also highly desirable when creating configurations. In this article, I would like to describe four simple ways to remove code duplication from GitLab CI configuration.

Job extension

GitLab Ci reference contains the keyword "extends". This word allows the configuration of a Job to be used in another Job. For example:

.codeCheck:
  timeout: 5m
  only:
    - master

codeStyleCheck:
  stage: check
  extends: .codecheck
  script:
    - vendor/bin/phpcs

In this case, Job codeStyleCheck extends Job .codeCheck. The settings from the current Job will be used (stage, scripts) and the settings from the extended Job will be added (timeout, only). This allows us to extract values that will always be the same and repeat them in exactly the same form as in the extended Job.

This solution has one disadvantage - the configurations are joined together by simple object merging operation. If any key from extended Job exists in the current Job, it will be overwritten by value from the current Job. For this reason, this type of extension is not suitable when you only want to use part of a setting value, e.g. the scripts key:

.codeCheck:
  timeout: 5m
  only:
    - master
  script:
    - composer install

codeStyleCheck:
  stage: check
  extends: .codeCheck
  script:
    - vendor/bin/phpcs

In the above case, only the vendor/bin/phpcs command will be executed.

Using anchors

If we have a lot of repeating commands and we would like to group them to use them multiple times, we can use anchors for this purpose:

.prepareVendors:
  script:
    - &prepare_vendors |
       composer install
       rm -rf ./cache

codeStyleCheck:
  stage: check
  script:
    - *prepare_vendors
    - vendor/bin/phpcs

It can also be written a little differently:

.prepareVendors: &prepare_vendors
  - composer install
  - rm -rf ./cache

codeStyleCheck:
  stage: check
  script:
    - *prepare_vendors
    - vendor/bin/phpcs

Anchor consists of an "&" character and a name. To use it, we refer using the asterisk and the anchor name. To use an anchor, we don't need to inherit the configuration from another Job - this is an alternative to this solution. In this case, three commands will be executed: composer install, rm -rf cache and vendor/bin/phpcs.

More about anchors can you read here.

Special keys "before_scripts" and "after_scripts"

This special sections allows you to define commands that will be executed before (after) each configured Job, e.g.:

before_scripts:
  - composer run install
  - rm -rf ./cache

codeStyleCheck:
  timeout: 5m
  only:
    - master
  script:
    - vendor/bin/phpcs

In this case, three commands will be executed: composer install, rm -rf cache and vendor/bin/phpcs also.

It is an ideal solution when every Job has some commands that repeat themselves (e.g. we have a CI based on "docker in docker" and we run the same service in each Job).

Using external scripts

CI runs code from the repository. When there is nothing to prevent you from adding a script to the project, you can execute some of the commands from external script. For example, we can define a command in Composer:

scripts: {
  "prepareVendors": "rm -rf ./cache"
}

And then use it in a pipeline configuration:

codeStyleCheck:
  timeout: 5m
  only:
    - master
  scripts:
    - composer run prepareVendors

That is all for today, thanks for reading! This is my first English entry, therefore I will be grateful for your feedback!