A journey into the depths of gitlab-ci
Tags: git, gitlab, ci, subtree
Published: 2021-03-03 21:48:03

# The beginning of an adventure 

After spending a lot of time on the awesome exercism.io platform, I was able to learn a lot of new languages and received great advice. After having accumulated a lot of exercism, I tried to automate and industrialize it. I was able to discover advanced features proposed by gitlab-ci. Here is my feedback

### Exercism
Exercism is a community site, where people wishing to learn a new language can receive expert advice from volunteer mentors.
To learn a language, it is advisable to follow a path that corresponds to a logical sequence of exercises to have a regular progression in the language.

Here is the list of all the tracks (learning paths) available on the platform.

![All tracks available on exercism.io](https://dev-to-uploads.s3.amazonaws.com/i/60xuj1vhv8x8p8wik608.png)

Exercism offers a cli to download and upload these exercises. This system is good but does not replace a versioning with git. So I use git to version my exercises.

The exercism folder is composed of subfolders for each language, and each language contains a folder for each exercism.

Here is an example of the architecture of an exercism folder.

```
exercism
+-- c
|   +-- hello-world
|       +-- hello-world.c
|       +-- hello-world-test.c
+-- go
|   +-- leap
|   |   +-- leap.go
|   |   +-- leap_test.go
|   +-- hamming
|       +-- hamming.go
|       +-- hamming_test.go
+-- ruby
    +-- sieve
        +-- sieve.rb
        +-- sieve_test.rb
```

Each exercise has tests to validate. The exercises use the [TDD method](https://en.wikipedia.org/wiki/Test-driven_development). Once the tests have been validated, the solution can be submitted and the return of a mentor is requested.

After accumulating many solutions, I wanted to have an IC that would allow me to stabilize and keep my exercises up to date if new tests were added.

Moreover, the way of executing the tests is similar depending on the tracks/languages. For example the exercises in go will be tested with the command `go test` or for java exercises `gradle test`.

Wanting to follow the [DRY principle](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), I would like to have a CI template that I could apply to all the exercises in the same language and add this CI to all the new exercises.

I compared the different continuous integration solutions available for open source projects (Jenkins, CircleCI, Travis CI, Codeship Gitlab CI). I chose gitlab-ci because it offers the most interesting specifications and features.

# :fox_face: The Powerful Feature : Parent Child Pipelines 

Gitlab offers a free CI, which can be easily integrated into a git project and even integrated into a Github project. Cool, my repo is on  [github](https://github.com/EmilienMottet/exercism-sol)

### [Parent Child Pipelines] (https://docs.gitlab.com/ee/ci/parent_child_pipelines.html) 
They allow to execute a CI from a CI. It is therefore possible to create a meta-CI. Despite some limitations that we will see later, Parent Child Pipelines allow me to spread my CI over 3 nested levels :  
- At the root of the exercises folder which will execute a CI for each language. The simplest file, it just has to trigger the ci of the languages: 
```yml
emacs-lisp:
  stage: test
  trigger:
    project: EmilienMottet/exercism-emacs-lisp-sol
    strategy: depend
  rules:
    - changes:
        - emacs-lisp/*

x86-64-assembly:
  stage: test
  trigger:
    project: EmilienMottet/exercism-x86-64-assembly-sol
    strategy: depend
  rules:
    - changes:
        - x86-64-assembly/*
```
[Full file](https://gitlab.com/EmilienMottet/exercism-sol/-/blob/master/.gitlab-ci.yml)
- At the root of each language that will execute a CI for each exercise:
```json
{
   "generate_c_gitlab_ci": {
      "artifacts": {
         "paths": [
            ".c-*-gitlab-ci.yml"
         ]
      },
      "image": {
         "entrypoint": [
            ""
         ],
         "name": "bitnami/jsonnet:latest"
      },
      "needs": [
         {
            "job": "build_vars",
            "pipeline": "$PARENT_PIPELINE_ID"
         }
      ],
      "script": [
         "jsonnet -m . --ext-str exercism_projects=\"$( echo $DIR_TO_BE_TESTED | sed -En 's/ /\\n/p' )\" --ext-str lang=\"c\" \".c-gitlab-ci.jsonnet\""
      ],
      "stage": "build"
   },
   "test_c_armstrong-numbers": {
      "stage": "test",
      "trigger": {
         "include": [
            {
               "artifact": ".c-armstrong-numbers-gitlab-ci.yml",
               "job": "generate_c_gitlab_ci"
            }
         ],
         "strategy": "depend"
      }
   },
   "test_c_hello-world": {
      "stage": "test",
      "trigger": {
         "include": [
            {
               "artifact": ".c-hello-world-gitlab-ci.yml",
               "job": "generate_c_gitlab_ci"
            }
         ],
         "strategy": "depend"
      }
   }
}
```
[Template file](https://gitlab.com/EmilienMottet/exercism-sol/-/blob/master/c/.generate-config.jsonnet)

- At the root of each exercise to run the tests provided by the platform :
```json
{
   "default": {
      "image": "gcc:latest"
   },
   "test-c-armstrong-numbers-exercism": {
      "script": [
         "cd armstrong-numbers",
         "make"
      ]
   }
}
```
[Template file](https://gitlab.com/EmilienMottet/exercism-sol/-/blob/master/c/.c-gitlab-ci.jsonnet)


Note the use of the rules field which allows each new push to execute the CI task only if there has been a modification in the given path.

To be DRY, I need to templating my CI, an example is proposed with [jsonnet](https://jsonnet.org/). Here is an example proposed by the gitlab team: https://gitlab.com/gitlab-org/project-templates/jsonnet.

# A template for each situation
![jsonnet logo](https://dev-to-uploads.s3.amazonaws.com/i/10fl7agsugown8fautyp.png)

A gitlab-ci.yml file can be described in jsonnet format.

For example :
```jsonnet
local exercism_projects = std.map(function(x) std.strReplace(x, '/', ''), std.split(std.extVar('exercism_projects'), '\n'));
local lang = std.extVar('lang');

local CTestJob(name) = {
  ['.' + lang + '-' + name + '-gitlab-ci.yml']: {
    default: {
      image: 'gcc:latest',
    },
    ['test-' + lang + '-' + name + '-exercism']: {
      script: [
        'cd ' + name,
        'make',
      ],
    },
  },
};


std.foldl(function(x, y) x + y, std.map(CTestJob, exercism_projects), {})
```

This template allows you to describe gitlab-ci.yml for C exercisms.

Morality, jsonnet templates to avoid repetition, it's perfect!

# The almost perfect IC, multi project pipeline

Parent Child Pipeline are awesome but have several flaws, 2 were notable to me:
- Only 2 levels for Nested child pipelines. In my case I need 3 levels. The limitation can be bypassed by using multi-project pipelines.
- Management of files modified by the last commit to avoid rebuilding all exercisms.

For the management of modified files, it is possible to recalculate the modified files using a pre-step.

```yml
build_vars:
  stage: .pre
  script:
    - |
      if [ "$CI_PIPELINE_TRIGGERED" = "true" ]; then
        DIR_TO_BE_TESTED=$(ls -d */)
      else
        DIR_TO_BE_TESTED=$(git diff --name-only $CI_COMMIT_SHA^ $CI_COMMIT_SHA */ | cut -d'/' -f1 | sort | uniq)
        if [ -z $DIR_TO_BE_TESTED ]; then
          DIR_TO_BE_TESTED=$(ls -d */)
        fi
      fi
    - echo DIR_TO_BE_TESTED=$DIR_TO_BE_TESTED >> build.env
    - cat build.env
  artifacts:
    reports:
      dotenv: build.env

```

Once the exercisms has been modified, it can be exported to artifact to share it with the job that will be generated afterwards.

# Submodule vs subtree

Because of the limitation of the number of nested and also a wish to separate the different tracks, I created a git repository for each track (manual action that could be automated with ansible).

The classic way to manage these nested projects would be to use the git submodule. However, keeping a consistent set of git submodule up to date can become quite restrictive.

There is an alternative ! The subtree :
```man
git-subtree - Merge subtrees together and split repository into subtrees
```

The advantage I found in subtrees is that I can mirror only the tracks and automate this one simply with the git hooks. And this without modifying my existing one.

To create a subtree nothing could be simpler :
```terminal
git subtree add --prefix=clojure --squash git@github.com:EmilienMottet/exercism-clojure-sol.git master
```

Then to make sure the subtrees is up to date, just add a hook to the post-commit..

Here is my hook file `.git/hooks/post-commit` :
```sh
#!/bin/sh

subtree_push(){
    git subtree push -P "$1" git@github.com:EmilienMottet/exercism-"$1"-sol.git master
}

for dir in */; do
    subtree_push "$( basename "$dir" )" 2> /dev/null &
done


for dir in */; do
    wait
done &
```

At each commit, each subtree is updated in the background.

# Conclusion

I hope this article has made you want to try exercism.io and, why not, join this community.

Industrialize, as quickly as possible. Exercism has a version management system, however, complementing it with git allows you to go further: 
- For example, I can do my exercises on several machines 
- The automated tasks save me a lot of time
- have a backup system 
- Just manage the history
- share my work and make it visible on github.com. 

Test yourself, the basis of exercise is the TDD. It is important to test yourself and your code all the time. Moreover, I want to keep my exercism compatible with the latest version of each language and in the future with new tests added by the community. Having up to date exercism allows you to always have up to date exercism that make a very good example, and it allows you to have a lot of stars ;)


DRY ! Do not repeat, Do not repeat (oops)! At first, factorized allows to gain clarity. Allows for sanctuarisation and transmission of knowledge. How many times you make a set of commands that are almost automatic, but after several months without doing them, you end up forgetting them. Taking the time to template, this time is quickly gained!

Thank you for reading :)