GitLab CI/CD

Something like GitHub Actions, but for GitLab. Setting up project build.

Time to read: 8 min

About GitLab

GitLab is a popular web service for collaborative software development and maintenance. You can work with Git repositories, manage tasks, discuss changes with your team, write wiki documentation, evaluate quality, release versions, and even monitor running programs — all of this in one place.

In Brief

GitLab CI is a tool integrated into GitLab for automating routine tasks that arise during software development. The range of such tasks is vast and varies from project to project, but the main ones are testing, static analysis, code style checking, and application deployment. GitLab CI is a competitor to another popular tool, GitHub Actions. These two services are largely similar, but there are some differences.

Example

Suppose we agreed in the team on special code formatting rules using EditorConfig, installed it as a dev dependency, and made it available with the command npm run editorconfig. We can run the check every time before committing, but there will always be situations when this is forgotten, and incorrectly formatted code will end up in the repository. Here, GitLab CI/CD comes to the rescue — we just need to create a file .gitlab-ci.yml at the root of the project with the following content:

        
          
          EditorConfig:  image: node:lts  script:    - npm ci    - npm run editorconfig
          EditorConfig:
  image: node:lts
  script:
    - npm ci
    - npm run editorconfig

        
        
          
        
      

And now, every time new code enters the repository, it will be checked for compliance with the rules, and any errors will be visible in the GitLab interface.

How to Use

Key Concepts

The main entity in GitLab CI/CD is the pipeline — a conveyor consisting of:

  • jobs, describing what needs to be done;
  • stages, indicating when or in what sequence to perform the jobs.

Jobs in one stage usually run in parallel. If all jobs complete successfully, execution moves to the next stage, and so on. If any job fails, execution stops, and the entire pipeline (usually) is considered failed.

Creating .gitlab-ci.yml

GitLab CI is fully configured using a single file in YAML format, which should be created at the root of the project — .gitlab-ci.yml.

Jobs often can have similar properties, such as the environment image in which actions are performed, preliminary commands, etc. To avoid repeating them every time, they need to be declared in the default section. If a particular job needs different parameters, you can specify them within that job, and they will override the global parameters.

First, you need to specify the Docker image (more on this in the article “What is Docker”), in which jobs will be executed. In most cases, the official Node.js image node:lts will suffice — this means our commands will run inside a Linux operating system with Node.js, npm, and even Yarn installed. You can read about the letters lts in the section on Node.js versioning.

        
          
          default:  image: node:lts
          default:
  image: node:lts

        
        
          
        
      

Setting Up Preparation Commands

When working with CI/CD in front-end projects, it is often necessary to install dependencies before performing the main action. For this, we can specify them in the before_script section — these commands will be executed in each job before the main action.

        
          
          default:  image: node:lts  before_script:    - npm -v    - npm install
          default:
  image: node:lts
  before_script:
    - npm -v
    - npm install

        
        
          
        
      

Specifying Stages

Suppose we want to run code base checks first using EditorConfig and Stylelint, and then, if both complete successfully, run tests. In this example, we can identify two stages: code style and tests. Define the stages using the keyword stages:

        
          
          stages:  - Code Style  - Tests
          stages:
  - Code Style
  - Tests

        
        
          
        
      

Describing Jobs and Specifying Commands

Now let's specify all three jobs. For this, we first state the job name, specify its stage using the keyword stage, and pass a list of commands in script. In our example, each job will run one npm script.

        
          
          default:  image: node:lts  before_script:    - npm -v    - npm cistages:  - Code Style  - TestsEditorConfig:  stage: Code Style  script:    - npm run editorconfigStylelint:  stage: Code Style  script:    - npm run stylelintAutomated Tests:  stage: Tests  script:    - npm run test
          default:
  image: node:lts
  before_script:
    - npm -v
    - npm ci

stages:
  - Code Style
  - Tests

EditorConfig:
  stage: Code Style
  script:
    - npm run editorconfig

Stylelint:
  stage: Code Style
  script:
    - npm run stylelint

Automated Tests:
  stage: Tests
  script:
    - npm run test

        
        
          
        
      

Here’s a schematic representation of the above configuration:

Pipeline Diagram
The configuration first checks the code style and, if correct, runs the tests.

Advanced Usage

Manual Run

If we want to run a specific job manually, we need to add when: manual:

        
          
          job:  script: npm run deploy  when: manual
          job:
  script: npm run deploy
  when: manual

        
        
          
        
      

Continuing on Failure

By default, if any job fails, the entire pipeline is marked as failed and the remaining jobs will not execute. However, there are situations where you would want to avoid this behavior. For example, we added a job with tests in a newly released version of Node.js and simply want to see problems that may need to be fixed in the future. Here, allow_failure: true comes in handy:

        
          
          job:  image: node:latest  script: npm run test  allow_failure: true
          job:
  image: node:latest
  script: npm run test
  allow_failure: true

        
        
          
        
      

Conditional Job Execution

GitLab provides access to a large number of environment variables with useful information. For example, $CI_COMMIT_BRANCH contains the current branch, $CI_COMMIT_SHORT_SHA — the short commit hash, $CI_PIPELINE_SOURCE — the source of the current pipeline invocation, and so on. Using these, we can run specific jobs under certain conditions. To do this, you need to declare one or more rules sections.

This job will only execute for commits to the main branch:

        
          
          job:  script: npm run deploy-to-production  rules:    - if: '$CI_COMMIT_BRANCH == "main"'
          job:
  script: npm run deploy-to-production
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

        
        
          
        
      

Scheduled Runs

Unlike GitHub Actions, in GitLab CI/CD, scheduling pipelines is set up only in the web interface. To do this, open the repository page and select CI/CD → Schedules. A list of existing rules and a button to add a new one will appear. In the addition form, you can specify the rule name, choose an interval from a list or specify your own in the Cron syntax. The last important field is the branch — when the rule is triggered, the pipeline will run as if code were pushed to that branch. The difference is that the variable $CI_PIPELINE_SOURCE will contain the value schedule.

Series of Jobs

Another typical task is to run tests on different versions of Node.js. You can create a job for each version manually, or you can specify a list of variables:

        
          
          Unit Tests:  script: node -v  image: ${NODE_VERSION}  parallel:    matrix:      - NODE_VERSION: ["node:14", "node:16", "node:17"]
          Unit Tests:
  script: node -v
  image: ${NODE_VERSION}
  parallel:
    matrix:
      - NODE_VERSION: ["node:14", "node:16", "node:17"]

        
        
          
        
      

In the example above, we declared a list NODE_VERSION with three elements. GitLab will create three jobs named: “Unit Tests [node:14]”, “Unit Tests [node:16]”, and “Unit Tests [node:17]”, then replace all instances of the variable NODE_VERSION in each job. Therefore, image in each job will be different.