Introduction

GitLab is a second most-popular DevOps platform that provides a complete solution for source version control, project management, issue tracking, and powerful CI/CD system.

GitLab is constantly working to extend and improve CI/CD functionality, and provides comprehensive documentation for all the new features.

I’ve been using GitLab for over a decade now for professional and personal projects. In this article I’d like to share some of my findings about useful features and workarounds for some limitations I know of.

Features

Referencing Scripts from Other Jobs

When writing CI/CD pipelines sometimes it is handy to reuse commands between different jobs. GitLab features the !reference custom YAML tag to accomplish this. Below commands from script of .configure-and-build are reused in build-project and build-sub-project jobs.

.configure-and-build:
    script:
        - cmake -G Xcode -B build
        - cmake --build build

build-project:
    script:
        - !reference [.configure-and-build, script]

build-sub-project:
    script:
        - cd sub-project
        - !reference [.configure-and-build, script]

Using Job Scripts as Functions

The above approach can be extended to use variables as “arguments” to referenced script commands. In below example GENERATOR and BUILD_DIR variables are used to customize behavior of .configure-and-build script. In build-project-macos job .configure-and-build script is invoked multiple times with different GENERATOR and BUILD_DIR values.


.configure-and-build:
    script:
        - cmake -G ${GENERATOR} -B build
        - cmake --build ${BUILD_DIR}

build-project-windows:
    ...
    variables:
        GENERATOR: "Visual Studio 16 2019"
        BUILD_DIR: "build"
    script:
        - !reference [.configure-and-build, script]

build-project-macos:
    ...
    script:
        - export GENERATOR="Xcode"
        - export BUILD_DIR="build_xcode"
        - !reference [.configure-and-build, script]
        - export GENERATOR="Ninja"
        - export BUILD_DIR="build_ninja"
        - !reference [.configure-and-build, script]
        - export GENERATOR="Unix Makefiles"
        - export BUILD_DIR="build_make"
        - !reference [.configure-and-build, script]

Multi-line Command Invocation

This is not strictly GitLab CI/CD feature, but rather feature of yaml language which is used to describe CI/CD pipelines in GitLab.

Sometimes it is desired to split long command into multiple lines for readability, but unfortunately bash and powershell use different characters to split multiline command - \ and ` respectively. Therefore, it is not possible to re-use commands for both platforms. Consider following example, where both windows and macos build jobs have to be duplicated:


build-macos:
    ...
    script: |
        cmake -G Xcode \
            -B build \
            -DOPTION1=YES \
            -DANOTHER_OPTION=Test \
            -DYET_ANOTHER_OPTION=1 \
            -DAND_ANOTHER_OPTION=ON         

build-windows:
    ...
    script: |
        cmake -G "Visual Studio 16 2019" `
            -B build `
            -DOPTION1=YES `
            -DANOTHER_OPTION=Test `
            -DYET_ANOTHER_OPTION=1 `
            -DAND_ANOTHER_OPTION=ON        

To reduce duplication a “Folded Block Scalar” > can be used to folds newlines to spaces, which unlocks the ability to write multi-line commands that are compatible with both bash and powershell.

.build:
    ...
    script: 
        - > 
            cmake -G "${GENERATOR}"
            -B build
            -DOPTION1=YES
            -DANOTHER_OPTION=Test
            -DYET_ANOTHER_OPTION=1
            -DAND_ANOTHER_OPTION=ON

build-macos:
    ...
    extends:
        - .build 
    variables:
        GENERATOR: "Xcode"

build-windows:
    ...
    extends:
        - .build 
    variables:
        GENERATOR: "Visual Studio 16 2019"

Exclusive Use of Runners

Sometimes it is desirable to restrict concurrent use of specific runner for a specific job. For example, you might have a runner that can execute unit-tests concurrently, but due to limitations of the underlying framework can only run one instance of end-to-end test application.

Limiting the number of concurrent jobs for the whole runner is suboptimal, since unit-tests can safely be executed in parallel. To resolve this GitLab resource_group keyword can be used to disable execution of a specific job from different pipelines concurrently.

Depending on runner concurrency configuration, multiple unit-test instances can be executed in parallel, but only one instance of test-e2e will be executed concurrently due to presence of resource_group key.

unit-test:
    ...
    tags:
        - test-machine-tag
    script:
        - echo "running unit-tests non-exclusively"
        - sleep 60

test-e2e:
    ...
    tags:
        - test-machine-tag
    resource_group: "test-e2e"
    script:
        - echo "running e2e tests exclusively"
        - sleep 60

Customizing Job Execution Using Rules

GitLab provides powerful rules keyword that is used to customize “when” job is run. Additionally rules supports specifying value for variables on specific conditions that can help to customize “how” job is being executed.

Consider the example below where variable SIGN will be set to YES only when build job is executed for tagged commit, in other cases it will be NO.

build-macos:
    variables:
        SIGN: "NO"
    rules:
        - if: "$CI_COMMIT_TAG"
          variables:
            SIGN: "YES"
        - when: always
    script: |
        echo "Executables will be signed: ${SIGN}"        

Clearing Working Directory for Specific Job

Recently GitLab added option to clean build directory before downloading artifacts and executing job scripts. To achieve this, GIT_STRATEGY variable should be set to empty. Per GitLab documentation:

Use the empty Git strategy when:

  • You do not need the repository data to be present.
  • You want a clean, controlled, or customized starting state every time a job runs.

Example:

job:
    ...
    variables:
        GIT_STRATEGY: empty
    script:
        - ls -al

Workarounds

Spaces in Variables

Consider a scenario where you want to have a variable that has list of file paths to be passed to some script and file paths may contain spaces. Example below contains list of files to be passed to signing script:

job:
    variables:
        FILES_FOR_SIGNING: "'Test App 1/Test App 1' 'Test App 2/Test App 2'"
    script:
        - echo $FILES_FOR_SIGNING
        - ./sign.sh $FILES_FOR_SIGNING

sign.sh script:

#!/bin/sh

if [ $# -eq 0 ]; then
    echo "No arguments provided"
    exit 1
fi

i=1
for arg in "$@"; do
    echo "$i: $arg"
    i=$((i + 1))
done

Output of pipeline execution will look like below - with arguments split by spaces, without quotes being taken into account.

$ echo $FILES_FOR_SIGNING
'Test App 1/Test App 1' 'Test App 2/Test App 2'
$ ./sign.sh $FILES_FOR_SIGNING
1: 'Test
2: App
3: 1/Test
4: App
5: 1'
6: 'Test
7: App
8: 2/Test
9: App
10: 2'

I have tried different combinations of quotes and escaped quotes, but the result was the same all the time. But, if you have noticed, echo $FILES_FOR_SIGNING produces string with single quotes preserved. Combining it with eval solves the problem!

Replacing ./sign.sh $FILES_FOR_SIGNING with eval $(echo ./sign.sh $FILES_FOR_SIGNING) solved the issue and the output was:

$ eval $(echo ./sign.sh $FILES_FOR_SIGNING)
1: Test App 1/Test App 1
2: Test App 2/Test App 2

A note on security: using eval in certain conditions is considered unsafe, even with current example FILES_FOR_SIGNING can be easily constructed so that it can execute arbitrary commands, use with caution!

Colon+Space in Values

I have stumbled upon this issue several times and each time it took me a while to figure out what is wrong. In short - if you want to use colon and space : in the value, whole value string must be quoted.

Obviously, this will work fine:

job:
    variables:
        ANIMALS: "Animals: dog, cat, mouse"
    script:
        - echo $ANIMALS

But this will not, as it is not compliant with above rule:

job:
    script:
        - echo "Animals: dog, cat, mouse"

To make it compliant, whole line should be quoted, no matter it is dictionary or a list value, like this:

job:
    script:
        - 'echo "Animals: dog, cat, mouse"'

Conclusion

GitLab is a powerful and extensive tool that offers a wide range of features for creating advanced CI/CD pipelines. Throughout this article, we’ve explored several techniques and workarounds that can help you create more efficient and flexible pipelines, streamline your development process and accelerate your software delivery. Remember that GitLab is constantly evolving, so it’s worth staying up-to-date with the latest features and best practices.