React Native

Set up a gitlab CI/CD for react or React-Native monorepo

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.

Overlook of the initial situation

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 :

++pre>++code> stages: - prerequisite - tests - build - deploy before_script: - echo Before script section install_dependencies: stage: prerequisite script: - echo install_dependencies native_tests: stage: tests script: - echo native_tests web_tests: stage: tests script: - echo web_tests build_native: stage: build script: - echo build_native build_web: stage: build script: - echo build_web deploy_native: stage: deploy script: - echo deploy_native deploy_web: stage: deploy script: - echo deploy_web ++/pre>++/code>

But lately, I figured it would be more efficient to split our CI/CD to match our different workspaces for the following reasons :

🚀 To fasten the CI by choosing which job to run depending on the type of merge request

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.

⚙️ Automating the deployment on the correct platform

The web merged request would automatically be deployed to the staging website and the native merged request to the staging app

🤓 To clarify the CI/CD flow by matching it to the mono repo structure.

That way, it makes it easier to understand in one look at the code.

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.

Conception of the optimized CI/CD

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 :

  • native merge request
  • web merge request
  • code merged on master
  • tags (for deployment)

The modelization of the future CI/CD is now done, let’s see in the next section how to implement it!

Implementation of the CI/CD

💡 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!

1) Split into different files

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 :

++pre>++code> include: - local: ci/shared/default.gitlab-ci.yml - local: ci/web/default.gitlab-ci.yml - local: ci/native/default.gitlab-ci.yml ++/code>++/pre>

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 :

++code>++pre> ### gitlab-ci.yml include: - local: ci/shared/default.gitlab-ci.yml - local: ci/web/default.gitlab-ci.yml - local: ci/native/default.gitlab-ci.yml stages: - prerequisite - tests - build - deploy ### If you need specific variables for the ### following job we can put them here before_script: - echo Before script section ++/code>++/pre>
++code>++pre> ### ci/shared/default.gitlab-ci.yml ### If you need specific variables for the ### following job we can put them here install_dependencies: stage: prerequisite script: - echo install_dependencies ++/code>++/pre>
++code>++pre> ### ci/web/default.gitlab-ci.yml ### If you need specific variables for the ### following job we can put them here web_tests: stage: tests script: - echo web_tests build_web: stage: build script: - echo build_web deploy_web: stage: deploy script: - echo deploy_web ++/code>++/pre>
++code>++pre> ### ci/native/default.gitlab-ci.yml ### If you need specific variables for the ### following job we can put them here native_tests: stage: tests script: - echo native_tests build_native: stage: build script: - echo build_native deploy_native: stage: deploy script: - echo deploy_native ++/code>++/pre>

2) Triggers

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 :

  • native merge request trigger
  • web merge request trigger
  • merged on master trigger
  • production deployment trigger

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 :

++code>++pre> install_dependencies : - stage : prerequisite - rules : - when: always - script : - echo install_dependencies ++/code>++/pre>

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 ‘native-’ and all the web-related branches ‘web-

example : native-login-form

Now here is an example of rules for the native test job :

++code>++pre> native_test : - stage : tests - rules : - if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^native-*/ - script : - echo native_test ++/code>++/pre>

By doing so, this job will only be run when the merge request comes from a branch named following the /^native-*/ pattern

👀 As you can see, we used the $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME 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 job only on master you have two options :

++code>++pre> rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH // OR only: - master ++/code>++/pre>

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 :

++code>++pre> native_deployment : - stage : deploy - script : - echo native_deployment - only: - tags ++/code>++/pre>

To conclude this section, here is an example of two gitlab-ci.yml files containing an example of each case I mentioned earlier :

++code>++pre> ### ci/shared/default.gitlab-ci.yml ### If you need specific variables for the ### following job we can put them here install_dependencies: stage: prerequisite rules: - when: always script: - echo install_dependencies ++/code>++/pre>
++code>++pre> ### ci/native/default.gitlab-ci.yml ### If you need specific variables for the ### following job we can put them here native_tests: stage: tests rules : - if: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^native-*/ script: - echo native_tests build_native_snapshot: stage: build only : - master script: - echo build_native_snapshot build_native_release: stage: build only : - tags script: - echo build_native_release deploy_native_snapshot: stage: deploy only : - master script: - echo deploy_native_snapshot deploy_native_release: stage: deploy only : - tags script: - echo deploy_native_release ++/code>++/pre>

Conclusion

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 :

  • Identify the type of optimization your CI/CD needs
  • Conceive a CI/CD diagram matching your workspaces structure by spreading all your jobs
  • Separate your jobs into files for more readability
  • Use GitLab rules and variables to control whenever each job is run

🚀 To go further :

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 :

  • Gitlab runner: allows to test each job locally
  • However, this solution does not provide passing artifacts from job to job. Therefore, you can not test the jobs that depend on the result of others
  • gitlab-ci-local : this tool should allow you to run your entire CI/CD locally. Unfortunately, the documentation is not very clear and it makes it hard to use

Other ressources :

https://medium.com/@robmosca/setting-up-a-ci-cd-pipeline-for-a-frontend-monorepo-f37fc8789fe4

How to optimize the dependencies installation phase

Développeur Mobile ?

Rejoins nos équipes