Another year, another CI tool. Due to work changes, I’m exploring GitLab for the first time. Here are some basic snippets I used recently and my first impression of the tool.

A CI pipeline in GitLab consists of stages and jobs. A stage can contain multiple jobs which run in parallel. Stages run sequentially (or, if you prefer, jobs of a stage run after the jobs of the previous stage have completed). The pipeline is defined in the file .gitlab-ci.yml.

Hello, world

Here’s a sample for a Maven project which runs mvn verify:

image: maven:3-jdk-11
stages:
  - verify_stage
verify_job:
  stage: verify_stage
  script:
    - mvn -B verify

Note that for such a simple configuration you don’t even need to define stages.

Caching

The next step will be to make the build a bit faster by caching the Maven dependencies. For GitLab, the files to be cached need to be within the working directory. For Maven, this isn’t the case, as the dependencies are downloaded in ~/.m2/repository. We can change this however with an environment variable:

image: maven:3-jdk-11
variables:
  # Use this directory instead of ~/.m2, so that GitLab can cache it
  MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository"
stages:
  - verify_stage
verify_job:
  stage: verify_stage
  cache:
    key: "$CI_JOB_NAME"
    paths:
      - .m2/repository/
  script:
    - mvn -B verify

With this modification, builds become faster because Maven dependencies are cached instead of downloaded from the internet. The cache key plays an important role, the documentation lists various examples. You can cache per branch, per job name, per combination of those, etc.

Using a service

Let’s say that your integration tests need a MySQL database. This is not a problem in the age of containers:

image: maven:3-jdk-11
variables:
  MYSQL_DATABASE: cool_db
  MYSQL_USER: cool_user
  MYSQL_PASSWORD: secret
  MYSQL_RANDOM_ROOT_PASSWORD: "1"
stages:
  - verify_stage
verify_job:
  stage: verify_stage
  services:
    - mysql:5
  script:
    - mvn -B verify

The database hostname will be mysql.

Allowing a job to fail

Let’s say we have a job that might fail but we don’t want that to be a big deal, but just a warning. For example, let’s say that we have a job that performs a SonarQube analysis and we don’t want to break the build if it fails. That’s done with the allow_failure setting:

image: maven:3-jdk-11
stages:
  - verify_stage
  - sonar_stage
verify_job:
  stage: verify_stage
  script:
    - mvn -B verify
sonar_job:
  stage: sonar_stage
  script:
    - mvn -P sonar mvn verify sonar:sonar
  allow_failure: true

Artifacts

You can specify the artifacts of your job:

image: maven:3-jdk-11
stages:
  - verify_stage
verify_job:
  stage: verify_stage
  script:
    - mvn -B verify
  artifacts:
    paths:
      - target/*.jar
    expire_in: "1 day"

Conditional execution: branches and tags

It’s possible to have a job executing only on certain conditions. The typical example is that you only want to deploy your app on production from the master branch, or publish your package from a tag.

image: maven:3-jdk-11
stages:
  - verify_stage
  - deploy_stage
verify_job:
  stage: verify_stage
  script:
    - mvn -B verify
deploy_job:
  stage: deploy_stage
  script:
    - mvn -B deploy
  only:
    refs:
      - master

For executing only on a tag, the only setting changes into:

only:
  - tags

Extending jobs

Let’s say that your git repository is a small monorepo that consists of two Maven projects living side by side. These aren’t modules of a common parent, they are totally independent projects. The configuration can be something like:

image: maven:3-jdk-11
stages:
  - verify_stage
verify_foo_job:
  stage: verify_stage
  script:
    - cd foo && mvn -B verify
verify_bar_job:
  stage: verify_stage
  script:
    - cd bar && mvn -B verify

This is simple enough, but soon you need to add caching, perhaps archive the same artifacts, and so on. To avoid repetition, you can define a template job and extend it:

image: maven:3-jdk-11
stages:
  - verify_stage
.verify_job:
  stage: verify_stage
  script:
    - cd ${PROJECT_NAME} && mvn -B verify
verify_foo_job:
  extends: .verify_job
  variables:
    PROJECT_NAME: foo
verify_bar_job:
  extends: .verify_job
  variables:
    PROJECT_NAME: bar

Now, all improvements on .verify_job get inherited by both jobs.

Conditional execution: changes

Now that we touched the monorepo, small as it may be, I’d like to show this feature that I like: building a job only if certain paths have changed.

There is no point in verifying the app foo, if the commit touches only the app bar. This is supported by GitLab:

image: maven:3-jdk-11
stages:
  - verify_stage
.verify_job:
  stage: verify_stage
  script:
    - cd ${PROJECT_NAME} && mvn -B verify
verify_foo_job:
  extends: .verify_job
  variables:
    PROJECT_NAME: foo
  only:
    changes:
      - foo
      - .gitlab-ci.yml
verify_bar_job:
  extends: .verify_job
  variables:
    PROJECT_NAME: bar
  only:
    changes:
      - bar
      - .gitlab-ci.yml

Notice that I add also the .gitlab-ci.yml as a safety measure. If I change the pipeline definition, then I want everything to run, regardless of what has changed.

This feature allows you to use a monorepo if you want to, but also have control over what gets built and deployed, without having to write something custom yourself.