Mono repo is a very common project configuration that involves grouping code of distinct projects onto one single repository. This structure eases the collaboration between those projects because it allows sharing of code very easily.On one of my recent projects, I was working on a mono repo but with only one single CI/CD flow. Therefore, all the jobs were executed without considering which part of the mono repo was modified. I wondered how we could optimize our CI/CD to better suit our needs and I learned a bunch of things worth sharing!
In this article, I will explain how I conceived and implemented an optimized GitLab CI/CD matching my mono repo structure.
Before going further, let me explain to you the configuration of my mono repo :I have a native folder (for the mobile app), a web folder (for the website), and a shared folder (to group all the shareable things between the native and the web app, like theme, HTTP calls, etc …)
At first, I did not think much about the CI/CD so I grouped all our jobs into a single CI/CD, executed each time a pull request was made or merged into master.Here is a simplified gitlab-ci.yml file to illustrate the initial situation :
But lately, I figured it would be more efficient to split our CI/CD to match our different workspaces for the following reasons :
We don’t necessarily want to run all the web tests if we are reviewing native code and vice-versa. However, tests from the shared folder should always be executed.
The web merged request would automatically be deployed to the staging website and the native merged request to the staging app
That way, it makes it easier to understand at first glance.
Now that we’ve seen why it is a good idea to optimize your mono repo CI/CD, let’s have a look at the following steps.
First of all, I suggest you list all your jobs and divide them depending on which part of your mono repo they are related to. By doing so, you can easily identify the common jobs (those you’ll execute no matter the code you are merging or deploying) from the specific jobs.
Here, I colored in red all the native-related jobs and blue the web-related jobs.
Then, extract common jobs and create the CI/CD stages.
Finally, determine which job to run depending on the trigger event.
On the diagram, we can see that we have 4 main conditions to trigger a job :
The modelization of the future CI/CD is now done, let’s see in the next section how to implement it!
💡 Small tip :
If you’re using VsCode, you can download a Yaml prettier extension. It will definitively save you if you are not familiar with YAML syntax!
My first action has been to create a CI folder matching the structure of my mono repo :
Matching your ci folder structure with your package's structure makes it easier to understand for anyone discovering your code for the first time.
As you can see, I did not get rid of the gitlab-ci.yml root file, even though according to our previous diagram, it won’t contain any jobs.
This is because GitLab requires a gitlab-ci.yml root file in order to run the CI/CD.
To make your default.gitlab-ci.yml files part of the CI/CD we will need to ‘include’ them at the beginning of your gitlab-ci.yml root file :
Here is the code corresponding to our example :
Now you can spread all your jobs according to the diagram we made earlier (in my example: native, web, or common).
Here is an example :
At this point, we have separated our jobs between different files but all of them are still executed each time the CI/CD is run.
According to our previous diagram, we have 4 different criteria to distinguish whether a job should be executed or not :
To decide whether or not a job should be run we are going to use the ‘rules’ property of GitLab ci jobs.
You can provide each job with a list of rules. If a rule evaluates to true, the remaining rules are skipped and the job will be run or not depending on the rule's instructions.
The first thing that stands out from the diagram is that all the common jobs are always supposed to be run, no matter which type of event triggered them (4 color chips under all of them).
To do so, we can use the always keyword :
For the native merge request trigger and web merge request trigger criteria, we’ve used the branch names as discriminants.
We decided that all the branches related to native code should follow the naming pattern ++code>native-*++/code> and all the web-related branches ++code>web-*++/code>
example : native-login-form
Now here is an example of rules for the native test job :
By doing so, this job will only be run when the merge request comes from a branch named following the ++code>^native-*/++/code> pattern
👀 As you can see, we used the ++code>$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME++/code> variable which is one of the numerous Predefined CI/CD variables provided by GitLab.
I deeply encourage you to have a look at the entire list of variables :
https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
To execute jobs only on master you have two options :
The only master section indicates that this job will run only when the Git reference for a pipeline is master.
🤔 According to the documentation, rules are replacing the only (and except) keywords to allow extending conditions to other variables than the one.
Finally, to trigger production deployment, we used tags like so :
To conclude this section, here is an example of two gitlab-ci.yml files containing an example of each case I mentioned earlier :
Here is the end of my journey to optimize my mono repo CI/CD. Hopefully, it will give you some keys or ideas to implement yours!
Let’s recap all the important things for you to remember :
Many times while I was working, I wished I could have run my pipeline locally to be able to test it without pushing on GitLab every single little change.
Here are a few options I did not have time to fully experiment but that are quite promising :
https://medium.com/@robmosca/setting-up-a-ci-cd-pipeline-for-a-frontend-monorepo-f37fc8789fe4
How to optimize the dependencies installation phase