47
How to set up monorepo build in GitLab CI
If you migrate your multirepo to a monorepo, or if your project is getting big enough to consider running only part of continuous integration (CI) - then it can make sense to run only those parts of CI that could have been affected by the change. This article will show how to achieve it on the GitLab platform, using a simple repository as an example.
The project is split into folders. For distinguishing if a given part of the project was modified, we use rules:changes so every part of the project that we want to be able to run in separation from the rest should be placed in one folder.
My assumptions are as follow:
- you want to run only changed sections of CI in the merge requests (MR) - mainly to save CI resources. So we can avoid running whole 30 minutes of jobs for a change we know is not likely to affect them. This is especially important if we have code in the repo that is not depending on each other - for example, our main application in one place and some landing pages in other folders.
- after changes are merged to master/main, we want to build everything no matter if it was changed or not. In this way, our main branch is indeed continuously integrated, and we keep on checking on even less commonly changed parts of the project.
My project structure is simple:
$ git ls-files
.gitlab-ci.yml
README.md
backend/README.md
frontend/README.md
I have 2 folders, backend
& frontend
. Each would host files of a given part of our project. This approach scales for any number of sub-projects - we could have company-website
, slack-bot
, or whatnot inside.
The configuration starts with defining the stages:
stages:
- build
- test
- deploy
This one is copied from GitLab's starting CI template. We can customize it with adding or removing stages. As we define needs:
, there is no speed penalty for adding more stages - each job is executed as soon as its requirements are defined in needs:
are met. For example, in my project, I ended up adding pre-build
to run some preparation scripts before building docker images in my project.
variables:
RULES_CHANGES_PATH: "**/*"
The default value for our changes configuration - by default, the job that extends our base config will be executed for any changes.
.base-rules:
rules:
...
Our base config. I define it in a way that requires us to add it with extends: .base-rule
- probably we could define those rules on the top level, but it's something a headache to configure everything in a way that works as expected in every case. I found it easier to have control over if the .base-rules
are set or not.
.base-rules:
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: always
...
The rules
are checked in order. Our 1 rule - if it's master/main, CI should always run the job.
- if: '$CI_PIPELINE_SOURCE == "push"'
when: never
Here, we avoid duplicated jobs for merge requests. Without, GitLab would create 1 pipeline for the branch and a "detached pipeline" for the MR. As the branch pipeline doesn't seem to support changes:
, we disable branch one & delay starting CI until an MR is created.
- if: $CI_COMMIT_TAG
when: never
Similarly, we don't need CI running for a tag.
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- $RULES_CHANGES_PATH
In MRs, we start jobs for when there are changes in the path defined in the RULES_CHANGES_PATH
variable.
- when: manual
allow_failure: true
Otherwise, we all the job to be triggered manually.
Now, we define 2 more jobs to be extended from:
.backend:
extends: .base-rules
variables:
RULES_CHANGES_PATH: "backend/**/*"
.frontend:
extends: .base-rules
variables:
RULES_CHANGES_PATH: "frontend/**/*"
In this way, we avoid duplicating the same path definition in each job we define for one or the other part - a possible source of errors.
On top of that all, we can define our jobs as:
backend-build:
stage: build
extends: .backend
needs: []
script:
- echo "Compiling the backend code..."
frontend-build:
stage: build
extends: .frontend
needs: []
script:
- echo "Compiling the frontend code..."
So, in the end, the complete config files are like this:
stages:
- build
- test
- deploy
variables:
RULES_CHANGES_PATH: "**/*"
.base-rules:
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: always
- if: '$CI_PIPELINE_SOURCE == "push"'
when: never
- if: $CI_COMMIT_TAG
when: never
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
changes:
- $RULES_CHANGES_PATH
- when: manual
allow_failure: true
.backend:
extends: .base-rules
variables:
RULES_CHANGES_PATH: "backend/**/*"
.frontend:
extends: .base-rules
variables:
RULES_CHANGES_PATH: "frontend/**/*"
backend-build:
stage: build
extends: .backend
needs: []
script:
- echo "Compiling the backend code..."
frontend-build:
stage: build
extends: .frontend
needs: []
script:
- echo "Compiling the frontend code..."
backend-test:
stage: test
extends: .backend
needs: ["backend-build"]
script:
- echo "Testing the backend code..."
frontend-test:
stage: test
extends: .frontend
needs: ["frontend-build"]
script:
- echo "Testing the frontend code..."
backend-deploy:
stage: deploy
extends: .backend
needs: ["backend-test"]
script:
- echo "Deploying the backend code..."
frontend-deploy:
stage: deploy
extends: .frontend
needs: ["frontend-test"]
script:
- echo "Deploying the frontend code..."
With a setup like this, you can have your backend CI run for backend MR:
frontend, for MR with frontend changes:
and each commit will trigger all CI jobs once it's merged to the main branch:
You can find the repo I used to write this article here.
In this article, we have seen how to set up partially split CI for branches in GitLab. Please leave a comment if you find this article helpful or have some questions about this approach.
47