diff --git a/.gitignore b/.gitignore
index 43a8d1508996bab2a79fd3dd26f21cbc919a37d3..530db921a751c03ed4845be0de44338fd51a3ba4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,9 @@
 aws.xml
 workspace.xml
+.gitlab-ci-local
+Wiki/.idea
+
+ExerciseChecker/src/config/Version.ts
 
 ############################ MacOS
 # General
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 471286723583c6d63f77dba3973d953982d876ae..2f80fec09fcd49c2fe534a7a9f345d89d862e0c3 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,350 +1,8 @@
-variables:
-    GIT_SUBMODULE_STRATEGY: recursive
-    GIT_SUBMODULE_FORCE_HTTPS: "true"
+include: "ExerciseChecker/.gitlab-ci/**.yml"
 
-    DOCKER_HOST: tcp://docker:2375
-    DOCKER_TLS_CERTDIR:
-    DOCKER_DRIVER: overlay2
-
-    PROJECT_NAME: DojoExerciseChecker
-
-    VERSION_DEV_SUFFIX: '-dev'
-
-    GITLAB_API_PROJECT_URL: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}
-
-    DOCKERFILE: Dockerfile_ExerciseChecker
-    DOCKER_PLATFORMS: linux/amd64,linux/arm64/v8
-
-    PROJECT_FOLDER: ExerciseChecker
-
-    PACKAGE_REGISTRY_URL: "${GITLAB_API_PROJECT_URL}/packages/generic/${PROJECT_NAME}"
-
-    WIKI_FOLDER: Wiki
-
-
-.get_version:
-    script:
-        - IS_DEV=$([[ $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH ]] && echo false || echo true)
-        - VERSION=$(jq -r .version $PROJECT_FOLDER/package.json)$([[ $IS_DEV == true ]] && echo $VERSION_DEV_SUFFIX || echo '')
-
-
-.get_packages_url:
-    script:
-        # Wiki
-        - WIKI_ARCHIVE_NAME="${PROJECT_NAME}_Wiki_${VERSION}.tar.xz"
-        - PACKAGE_URL_WIKI="${PACKAGE_REGISTRY_URL}_Wiki/${VERSION}/${WIKI_ARCHIVE_NAME}"
-
-
-.init_dind_script:
-    script:
-        # Install dependencies
-        - apk update
-        - apk add git
-        - apk add jq
-        - apk add curl
-
-        # Init docker buildx
-        - docker login -u $DOCKER_REGISTRY_USER -p $DOCKER_REGISTRY_PASSWORD $DOCKER_REGISTRY
-        - docker buildx create --use
-
-        # Get version from package.json
-        - !reference [.get_version, script]
-        - CONTAINER_IMAGE=$DOCKER_REGISTRY_IMAGE:$VERSION
-
-
-.build_script:
-    script:
-        - !reference [ .init_dind_script, script ]
-        - mkdir -p $ARTIFACTS_FOLDER
-
-        # Decrypt env vars for production
-        - apk add npm sed
-        - cd $PROJECT_FOLDER
-        - sed -i -r "s/\{\{VERSION\}\}/${VERSION}/g" src/app.ts
-        - |
-            if [ $CI_COMMIT_REF_PROTECTED == "true" ]; then
-                echo "Decrypt production env vars"
-                sed -i -r "s/(DOTENV_KEY[ ]*:[ ]*[\'\"\`])[^'\"\`]*([\'\"\`])([ ]*\,)?//g" src/app.ts
-                sed -i -r "s/,[\ \n]*\}/\}/g" src/app.ts
-                npx dotenv-vault local decrypt "${DOTENV_PROD_KEY}" > .env
-               
-            fi
-        - cd ..
-
-        # Need to build for each platform separately because of multi-stage builds (docker buildx don't use cache same way as docker build)
-        - >
-            platform_array=$(echo $DOCKER_PLATFORMS | tr "," "\n");
-            for platform in $platform_array; do
-              echo "Buildind for : $platform"
-              docker buildx build --pull --platform $platform --file $DOCKERFILE --tag $CONTAINER_IMAGE .
-            done
-
-
-.clean_release:
-    script:
-        # Delete release if it already exists
-        - 'curl --request DELETE --header "JOB-TOKEN: $CI_JOB_TOKEN" "${GITLAB_API_PROJECT_URL}/releases/${VERSION}"'
-
-        # Delete tag if it already exists (use private-token because job-token don't have permission to delete tags)
-        - 'curl --request DELETE --header "PRIVATE-TOKEN: $GITLAB_PROJECT_ACCESS_TOKEN" "${GITLAB_API_PROJECT_URL}/repository/tags/${VERSION}"'
-
-
-.clean_packages:
-    script:
-        # Get all packages of the project
-        - 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" "${GITLAB_API_PROJECT_URL}/packages" > gitlabPackages.json'
-
-        # Filter and select packages to delete (based on version)
-        - packagesToDelete=`jq -r '.[] | select(.version=="'${VERSION}'") | ._links.delete_api_path' gitlabPackages.json`
-
-        # Delete packages by calling Gitlab API
-        - >
-            for deletePath in $packagesToDelete; do
-              echo "Deleting package at path : ${deletePath}"
-              curl --request DELETE --header "JOB-TOKEN: $CI_JOB_TOKEN" "${deletePath}"
-            done
-    
-
-stages:
-    - test
-    - build
-    - clean
-    - upload
-    - release
-
-
-test:build:
-    stage: test
-    tags:
-        - build
-        - dind
-    image: docker:latest
-    services:
-        - docker:dind
-    script:
-        - !reference [ .build_script, script ]
-    rules:
-        - if: '$CI_COMMIT_TAG =~ "/^$/" && $CI_COMMIT_REF_PROTECTED != "true"'
-
-
-build:version:
-    stage: build
-    tags:
-        - build
-        - dind
-    image: docker:latest
-    services:
-        - docker:dind
-    script:
-        - !reference [ .build_script, script ]
-
-        # Here docker buildx can use cached images created in previous step
-        - docker buildx build --platform $DOCKER_PLATFORMS --file $DOCKERFILE --push --tag $CONTAINER_IMAGE .
-    rules:
-        - if: '$CI_COMMIT_REF_PROTECTED == "true"'
-
-
-clean:release:
-    stage: clean
-    tags:
-        - gitlab_clean
-    image: registry.gitlab.com/gitlab-ci-utils/curl-jq:latest
+lint_pass:
+    only:
+        variables:
+            - $FOO == "bar"
     script:
-        - !reference [.get_version, script]
-        - !reference [.clean_release, script]
-    rules:
-        - if: '$CI_COMMIT_REF_PROTECTED == "true"'
-
-
-clean:packages:
-    stage: clean
-    tags:
-        - gitlab_clean
-    image: registry.gitlab.com/gitlab-ci-utils/curl-jq:latest
-    script:
-        - !reference [.get_version, script]
-        - !reference [.clean_packages, script]
-    rules:
-        - if: '$CI_COMMIT_REF_PROTECTED == "true"'
-
-
-clean:dev:release:
-    stage: clean
-    tags:
-        - gitlab_clean
-    image: registry.gitlab.com/gitlab-ci-utils/curl-jq:latest
-    script:
-        - !reference [.get_version, script]
-        - VERSION="${VERSION}${VERSION_DEV_SUFFIX}"
-        - !reference [.clean_release, script]
-    rules:
-        - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
-
-
-clean:dev:packages:
-    stage: clean
-    tags:
-        - gitlab_clean
-    image: registry.gitlab.com/gitlab-ci-utils/curl-jq:latest
-    script:
-        - !reference [.get_version, script]
-        - VERSION="${VERSION}${VERSION_DEV_SUFFIX}"
-        - !reference [.clean_packages, script]
-    rules:
-        - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
-
-
-clean:dev:dockerhub:
-    stage: clean
-    tags:
-        - clean
-        - dind
-    image: docker:latest
-    services:
-        - docker:dind
-    script:
-        - !reference [.init_dind_script, script]
-        - VERSION="${VERSION}${VERSION_DEV_SUFFIX}"
-
-        # Remove dev tag
-        ## Login to dockerhub (get JWT token)
-        - GET_TOKEN_DATA=$(jq --null-input --arg username "$DOCKER_REGISTRY_USER" --arg password "$DOCKER_REGISTRY_PASSWORD" '{"username":$username,"password":$password}')
-        - >
-            TOKEN=`curl --data "${GET_TOKEN_DATA}" \
-              --header "Content-Type: application/json" \
-              --header "JOB-TOKEN: $CI_JOB_TOKEN" \
-              "https://hub.docker.com/v2/users/login/" | jq -r .token`
-
-        ## Delete tag
-        - 'curl --header "Authorization: JWT ${TOKEN}" --request DELETE "https://hub.docker.com/v2/repositories/${DOCKER_REGISTRY_IMAGE}/tags/${VERSION}/"'
-    rules:
-         - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
-
-
-upload:packages:wiki:
-    stage: upload
-    tags:
-        - gitlab_package
-    image: registry.gitlab.com/gitlab-ci-utils/curl-jq:latest
-    script:
-        # Install dependencies
-        - apk update
-        - apk add xz
-
-        - !reference [.get_version, script]
-        - !reference [.get_packages_url, script]
-
-        # Create archive
-        - mkdir -p $ARTIFACTS_FOLDER
-        - WIKI_ARCHIVE_PATH="${ARTIFACTS_FOLDER}/${WIKI_ARCHIVE_NAME}"
-        - tar -v -c -C "${CI_PROJECT_DIR}/${WIKI_FOLDER}" -J -f "${WIKI_ARCHIVE_PATH}" . # Ubuntu: tar --verbose --create --cd wiki-test-2 --xz --file file.tar.bz2
-
-        # Send package
-        - 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file ${WIKI_ARCHIVE_PATH} "${PACKAGE_URL_WIKI}";'
-    rules:
-        - if: '$CI_COMMIT_REF_PROTECTED == "true"'
-
-
-release:dockerhub:latest:
-    stage: release
-    tags:
-        - release
-        - dind
-    image: docker:latest
-    services:
-        - docker:dind
-    script:
-        - !reference [.init_dind_script, script]
-
-        # Push latest tag
-        - docker buildx imagetools create $CONTAINER_IMAGE --tag $DOCKER_REGISTRY_IMAGE:latest
-    rules:
-         - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
-
-
-release:wiki:
-    stage: release
-    tags:
-        - release
-    image: alpine:latest
-    script:
-        - apk update
-        - apk add git
-        - apk add jq
-
-        - !reference [.get_version, script]
-
-        # Define URL for the wiki in terms of project-agnostic predefined variables
-        - WIKI_URL="${CI_SERVER_PROTOCOL}://project_${CI_PROJECT_ID}_bot:${GITLAB_PROJECT_ACCESS_TOKEN}@${CI_SERVER_HOST}:${CI_SERVER_PORT}/${CI_PROJECT_PATH}.wiki.git"
-
-        # Clone this project's wiki under /tmp
-        - rm -rf "/tmp/${CI_PROJECT_NAME}.wiki"
-        - cd /tmp
-        - git clone "${WIKI_URL}"
-
-        # Enter the cloned repo
-        - cd "${CI_PROJECT_NAME}.wiki"
-
-        # Update the file
-        - mv .git/ ../
-        - rm -rf ./*
-        - mv ../.git/ ./
-        - cp "${CI_PROJECT_DIR}/.gitignore" .
-        - cp -R "${CI_PROJECT_DIR}/${WIKI_FOLDER}/." .
-
-        # Set committer info
-        - git config user.name "$GITLAB_USER_NAME"
-        - git config user.email "$GITLAB_USER_EMAIL"
-
-        # Commit the gitignore file
-        - git add ".gitignore"
-        - git commit -m "Add gitignore file" || true
-
-        # Commit the file
-        - git add .
-        - git commit -m "${VERSION}" || true
-
-        # Push the change back to the master branch of the wiki
-        - git push origin "HEAD:main"
-    rules:
-        - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
-
-
-release:gitlab:
-    stage: release
-    tags:
-        - release
-    image: registry.gitlab.com/gitlab-ci-utils/curl-jq:latest
-    script:
-        - !reference [.get_version, script]
-        - echo 'Running release_job'
-        - !reference [.get_packages_url, script]
-
-        # Extract description from CHANGELOG.md
-        - CHANGELOG_LINE_START=`awk '/##\ [0-9]+\.[0-9]+\.[0-9]+/{print NR; exit;}' CHANGELOG.md`
-        - CHANGELOG_LINE_END=`awk '/##\ [0-9]+\.[0-9]+\.[0-9]+/{ count++; if(count>1) {print NR; exit;}}' CHANGELOG.md`
-        - DESCRIPTION=`awk 'NR > '$CHANGELOG_LINE_START' && NR < '$CHANGELOG_LINE_END'' CHANGELOG.md`
-
-        # Create Release (can't be done by release_step of gitlab image because it don't have access to env var defined in script_step)
-        - >
-            RELEASE_DATA=$(jq --null-input --arg version "$VERSION" --arg description "# Changelog (version $VERSION) $DESCRIPTION" --arg tag_name "$VERSION" --arg ref "$CI_COMMIT_SHORT_SHA" '{
-                "name": $version,
-                "description": $description,
-                "tag_name": $tag_name,
-                "ref": $ref,
-                "assets": {
-                    "links": [
-                        {
-                          "name": "Wiki",
-                          "url": "'${PACKAGE_URL_WIKI}'",
-                        }
-                    ]   
-                }
-            }')
-        - >
-            curl --data "${RELEASE_DATA}" \
-              --header "Content-Type: application/json" \
-              --header "JOB-TOKEN: $CI_JOB_TOKEN" \
-              --request POST "${GITLAB_API_PROJECT_URL}/releases"
-    rules:
-        - if: '$CI_COMMIT_REF_PROTECTED == "true"'
\ No newline at end of file
+        - echo
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 97411b8b9a7741ac37fa46c151e40fb83b48ec6e..9deade6d9271a41db904e6617d50df6985c69185 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,7 +14,19 @@
 **⚠️ Deprecation:**
 -->
 
-## 2.1.0 (?)
+## 2.2.0 (2023-10-16)
+
+### ✨ Feature
+- `results.json` file is now optional (if the teaching staff don't want to provide test details)
+    - The exercise will be considered as valid if the container exit code is 0
+    - The `results.json` file will be construct / completed with the container exit code
+    - The `volume` argument of `dojo_assignment.json` is now optional (if the teaching staff don't want to provide `results.json` file or other files)
+
+### 🔨 Internal / Developers
+- Enhancement in pipelines by splitting them into several files
+
+
+## 2.1.0 (2023-09-29)
 
 ### 🎨 Interface
 - **💥 Breaking:** Renamed `dojo.enonce` (or `dojo.assignment`) file to `dojo_assignment.json`
diff --git a/ExerciseChecker/.env.vault b/ExerciseChecker/.env.vault
index 191e70d5b3df7008634ca6ccb25494aaa513b4ab..805a49510414790d46a647460afab00ef34e66f8 100644
--- a/ExerciseChecker/.env.vault
+++ b/ExerciseChecker/.env.vault
@@ -4,8 +4,8 @@
 #/--------------------------------------------------/
 
 # development
-DOTENV_VAULT_DEVELOPMENT="Ir7CPeMh8qd9jAgmy8RaSTlY3OA4Kw6xHB+j7AJjfM07H2koRKpOUP5Fsa+XBYp1rjTCl5p2063/H3ed2VmhUwIHxa+CUwHOagy+kKLYHBVzEdSrE6EK96+ihyGwjkrFRdRRWar74TxaCsxoNpLUz0HtK1fbJZMFUhgJOklBcw8BdH6mrq3RI8+tkSx5nOTJTGo0OhilsAZTeAbOdcZQNIcIfbZVMLD1X3YocqitLyAURzfyEegp3XOHfpOr+45/Be4HaakylijLggn73sS/fDr/yo2IRHTz7il/Y8IO3kUMJnSXoqQM1EPzqiYEtIFsDW2aVjLnbDdM2BxlNe+Cv50yglPINfkOMJ3ZBsELjkDBI9N5wLW9qyoF5zxaw4jfnGTPuVsM1FhozEGTBg64GtJ82WI0ftDjt06E1jkMB9hWbYS5pKBWHq/w2A+TfatZjn9QmgEuiO5Q8RW6srojTsuuMjJwVC9YCzjzvxC0YJulfMCBYuPW1cVagp6CkA5soo7iaEfJmt++GhWkYGBMq9EeezTl5GEarpUBYdLYlwfICsgP1E+9lF5dud+zWnrGzBVeePBmcVO86C/SZici/3HkORY68NoqbswV5AXJyACFT6Qf+z3k8CLkrCvfti6xvAAf4ivorbnihB0RX4znE4e70qCU2w3QjS+cwuHuC8sEp4SpqxR+kP2+ktfA5pw08yoFg4zrSTwuvJN8OnLyryo0VODCWA1V6Bom/imtCLn2nRuYQhO0AYiAtvyWOTbeduQ7rixeYc98j80sVHU7PY4hFbF7NVnWUY+nhzJ3rbK1y9q7iLLkpivsRmeosIIhVtoSR+Lur/hYZ2ReFyA6OHofxdcIqKiphG4HLZl9lCq6a+UlRAOqN98XABGmJ1KhCTsFxTTuGahMU1r556O/4NJFnubPU1DH80n71AB4HdD2qCxs4Tm4fDVs3cDv7fak"
+DOTENV_VAULT_DEVELOPMENT="+tvA0uHhVW7Czj260K5arwAA4mmVBQC/83i6YXPo7fEEoiBvXZCPSovZ23phpqYx5LmqgtTQALb5gisUqSJfrjwDYABKk0tfrkCvrFayUxv5NuFSd2RVliE1JJ5nZgVeAdcy01igGgQOdFzS5Ze/Gy5iEP+C7Nq8HflazeQrlAXlIGNlPe77v+bSIce1Itf9dj8/sB5xz6wwRY5TSF+CM1OI8dTTrKOruxKXVQP0ztpUsGHUjQvowYR7JHeXQOivn8phUTWjR4vZskTBGGe/s0DiIQ5qQq+HR2LFxVEmzOI0eHY5vSEkZ9pAYmUX9GB0uJcAwdInSiLWkgC12t+7FuMfHM7JtE6pI0NECQwZIWm1DZSurYr8wIiYCg4Ds8xsHFPE+CmJma3jfZzx9aE/RN2AM7fbvKWVLakPB3OQK9SrPaM6jWVRfkaEqUDqggX4gJy98SAHznYhGamsQBxWa7wXRrlXC844SDCoZVsql22UNy/hRnXjGKonHhlm6MQE4y07btcfRkPF696Mvjue8Y/loTS0GGLZXFr+3LcRLcJTTwDT88tdNbQd5QiaFTTMY7pDAFeDke5M7VqYlFcrAtb8+9TGtZOXBgnYXmSpF+KKw4JwY6v7rw9wAGE3W++xI70stmCZU+M5/TDLO5SoWzcqlWJEe2VMI6QZmegzPGymc5g47ujdwvWKg1JZlLcrXjDMBi2/2Mv4H95PYpt6rEpY+ZHtNaO/kI6hRbqUvIZ8frL1dM5DihtoIaHpADnSQ0YfWlk01LA6Jm1BFBJr7RUHLw/S+avzMlbHnjAvxL36dUagvwynDQFapwhFrqLiq9vTmM+8tPQeOuGeutJlJgvDgGxd6hmiHDImrknZtNQiZ0hkVCoVgwopRe/bxlfGAqbpZUvs6eu9CFT6poWOVUvq2lilRlUQXESs7CP599cpLYgu3zViQB7wG659bAK+CNWsJlDoYN4dfT5Bw+P/6pTta/9hegT6f0/ui8p4oRK2bsJMCv8+502M52beU4GjBIjNTepFauCi9LcxIMzXclscedj0qlE="
 
 # production
-DOTENV_VAULT_PRODUCTION="NEaGKlOKDlL652EG43Iid/7P4ybOxkmc3t+6xgKS11Kevzs7zocA8cjJqfkjBAFdMu1oZbiLO+dzXgqEVHmu+66Qr8CDMufQsNXgpO6viLUx/FlDpeY1IYJVma/W1GwEP+5AW2BhrT7MJnBidzx5wHomjy+CAYlMoV3c4Q/obJQcomq65jn5KSgaiFcyLqitbUESJhxAS5adRXvtkdbqaXR5yCRtXF2OS50HW544/9qf9e9hqwELHrd1fYOJqYuE/zbZV3ZGgwgK7Ui0tv8MU4uN6LzpTTZVWdK4cdMuQTtX23rTWg9J9WcAj5M8jpo309VFPPxN3SfzoQHVb9+RlL+XEH92EwRFFevtKdE07Wz5ZbUbtiKAqkqaKiusjSALasoT+mW5LFWjFdFCDJKof98w00kR7c6PoTDBw50Zh5/z+UPn4uXT8ZOKV7ooxGQLATrBP+4k7pyBx2bYCo/o0tbgSJ4uTv+Wo2eVwUXrptxwmp6Tow50NAdcvKJCQmDe43e6PMsyX+9GO6CbRywvS/2aLffXRly+txqkr0B9/suMwuQwyEv8LOVR9Q2z3MC6leQ3VAaGxNGLKEdtJFgn+31JnciGy6HQABDZ6Agc/Tux4PBOOWy0yq0mGQztIk/0dj+WMro="
+DOTENV_VAULT_PRODUCTION="bhLJF6f7KLRNI6witxN4RNPaylwg/Z+3VB52htArsNhYUck4OhUMaKZRCAdAo90JCd+ap3xyvipfqdGsbwXBq79VVcKDNzW9pSkYGBZvNpTPk2xnGAfZmAhgZmz4AX1Jbmb6rjpTRDemL8ocsEqVlb0KsxhcPjALjVvHeDxbWUiFc4MsnNo7L26PRNFpV1aVQ6F1u90WymMsEBE6lGnArXbnyREMM3tqq7fBxczg2um8dt50MRTYCWpG+7/Hdp7TnusoRSYU5Qs6Y0kI3UQxLUXuP+QN9ktI7+6RJ1aki31XojTggtKPXskVvhzZeU5k5ZGYJSn46BTQNFqx7KpLPKo9cTNNZiIV7wVmNyUGmOQfCAdyN14TN8/0LDs3QnglQd4XUkYHlTPFQFbUFlGIvh8ErtOcgH3cu7sfO0z2diZFvHI5fAjuUJdjTSGtP07aI1pXUMHU4YHUX7W8ycf4wP9k91tynd0YLLeXagEuSnfXx5ev/iguvh9BZV4jy328igHB4Cxh0yY24hClzIKc2fqghvLMkafBUID/JMjG/N/AdHxaw/UiXsvK4oJUHpYKLC83UeGugMonfppZdIzPpTrji487SI04zIVCJGxf1bXMGJWHNycff5Ehi5MNHqiDDkwY6OzsGPUyzrTC4/KHGyQwMbdBmrmLEWqlzDoLprm9j0CuAavAHj3eaFnuiOKhs1B8Qb3LosCDPJuErJavI/g2gj59YkiIDLjnUQ=="
 
diff --git a/ExerciseChecker/.gitlab-ci/00_vars.yml b/ExerciseChecker/.gitlab-ci/00_vars.yml
new file mode 100644
index 0000000000000000000000000000000000000000..46cb27b058b7ab53199e24bc6288172dcdf94417
--- /dev/null
+++ b/ExerciseChecker/.gitlab-ci/00_vars.yml
@@ -0,0 +1,22 @@
+variables:
+    GIT_SUBMODULE_STRATEGY: recursive
+    GIT_SUBMODULE_FORCE_HTTPS: "true"
+
+    DOCKER_HOST: tcp://docker:2375
+    DOCKER_TLS_CERTDIR:
+    DOCKER_DRIVER: overlay2
+
+    PROJECT_NAME: DojoExerciseChecker
+
+    VERSION_DEV_SUFFIX: '-dev'
+
+    GITLAB_API_PROJECT_URL: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}
+
+    DOCKERFILE: Dockerfile_ExerciseChecker
+    DOCKER_PLATFORMS: linux/amd64,linux/arm64/v8
+
+    PROJECT_FOLDER: ExerciseChecker
+
+    PACKAGE_REGISTRY_URL: "${GITLAB_API_PROJECT_URL}/packages/generic/${PROJECT_NAME}"
+
+    WIKI_FOLDER: Wiki
\ No newline at end of file
diff --git a/ExerciseChecker/.gitlab-ci/01_functions.yml b/ExerciseChecker/.gitlab-ci/01_functions.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f0675a514e20658a7d87932f65073487c89cc028
--- /dev/null
+++ b/ExerciseChecker/.gitlab-ci/01_functions.yml
@@ -0,0 +1,84 @@
+.get_version:
+    script:
+        - IS_DEV=$([[ $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH ]] && echo false || echo true)
+        - VERSION=$(jq -r .version $PROJECT_FOLDER/package.json)$([[ $IS_DEV == true ]] && echo $VERSION_DEV_SUFFIX || echo '')
+
+
+.get_packages_url:
+    script:
+        # Wiki
+        - WIKI_ARCHIVE_NAME="${PROJECT_NAME}_Wiki_${VERSION}.tar.xz"
+        - PACKAGE_URL_WIKI="${PACKAGE_REGISTRY_URL}_Wiki/${VERSION}/${WIKI_ARCHIVE_NAME}"
+
+
+.init_dind_script:
+    script:
+        # Install dependencies
+        - apk update
+        - apk add git
+        - apk add jq
+        - apk add curl
+
+        # Init docker buildx
+        - |
+            if [ $CI_COMMIT_REF_PROTECTED == "true" ]; then
+                docker login -u $DOCKER_REGISTRY_USER -p $DOCKER_REGISTRY_PASSWORD $DOCKER_REGISTRY
+            fi
+        - docker buildx create --use
+
+        # Get version from package.json
+        - !reference [ .get_version, script ]
+        - CONTAINER_IMAGE=$DOCKER_REGISTRY_IMAGE:$VERSION
+
+
+.build_script:
+    script:
+        - !reference [ .init_dind_script, script ]
+        - mkdir -p $ARTIFACTS_FOLDER
+
+        # Decrypt env vars for production
+        - apk add npm sed
+        - cd $PROJECT_FOLDER
+        - sed -i -r "s/\{\{VERSION\}\}/${VERSION}/g" src/app.ts
+        - |
+            if [ $CI_COMMIT_REF_PROTECTED == "true" ]; then
+                echo "Decrypt production env vars"
+                sed -i -r "s/(DOTENV_KEY[ ]*:[ ]*[\'\"\`])[^'\"\`]*([\'\"\`])([ ]*\,)?//g" src/app.ts
+                sed -i -r "s/,[\ \n]*\}/\}/g" src/app.ts
+                npx dotenv-vault local decrypt "${DOTENV_PROD_KEY}" > .env
+            
+            fi
+        - cd ..
+
+        # Need to build for each platform separately because of multi-stage builds (docker buildx don't use cache same way as docker build)
+        - >
+            platform_array=$(echo $DOCKER_PLATFORMS | tr "," "\n");
+            for platform in $platform_array; do
+              echo "Buildind for : $platform"
+              docker buildx build --pull --platform $platform --file $DOCKERFILE --tag $CONTAINER_IMAGE .
+            done
+
+
+.clean_release:
+    script:
+        # Delete release if it already exists
+        - 'curl --request DELETE --header "JOB-TOKEN: $CI_JOB_TOKEN" "${GITLAB_API_PROJECT_URL}/releases/${VERSION}"'
+
+        # Delete tag if it already exists (use private-token because job-token don't have permission to delete tags)
+        - 'curl --request DELETE --header "PRIVATE-TOKEN: $GITLAB_PROJECT_ACCESS_TOKEN" "${GITLAB_API_PROJECT_URL}/repository/tags/${VERSION}"'
+
+
+.clean_packages:
+    script:
+        # Get all packages of the project
+        - 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" "${GITLAB_API_PROJECT_URL}/packages" > gitlabPackages.json'
+
+        # Filter and select packages to delete (based on version)
+        - packagesToDelete=`jq -r '.[] | select(.version=="'${VERSION}'") | ._links.delete_api_path' gitlabPackages.json`
+
+        # Delete packages by calling Gitlab API
+        - >
+            for deletePath in $packagesToDelete; do
+              echo "Deleting package at path : ${deletePath}"
+              curl --request DELETE --header "JOB-TOKEN: $CI_JOB_TOKEN" "${deletePath}"
+            done
\ No newline at end of file
diff --git a/ExerciseChecker/.gitlab-ci/03_stages.yml b/ExerciseChecker/.gitlab-ci/03_stages.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1b20f1ed1fa05c19ee6d8145ccd33bcc39c85d9d
--- /dev/null
+++ b/ExerciseChecker/.gitlab-ci/03_stages.yml
@@ -0,0 +1,6 @@
+stages:
+    - test
+    - build
+    - clean
+    - upload
+    - release
\ No newline at end of file
diff --git a/ExerciseChecker/.gitlab-ci/04_stageTest.yml b/ExerciseChecker/.gitlab-ci/04_stageTest.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a1b564295624124e216ae7bba18ac6950590eccb
--- /dev/null
+++ b/ExerciseChecker/.gitlab-ci/04_stageTest.yml
@@ -0,0 +1,12 @@
+test:build:
+    stage: test
+    tags:
+        - build
+        - dind
+    image: docker:latest
+    services:
+        - docker:dind
+    script:
+        - !reference [ .build_script, script ]
+    rules:
+        -   if: '$CI_COMMIT_TAG =~ "/^$/" && $CI_COMMIT_REF_PROTECTED != "true"'
\ No newline at end of file
diff --git a/ExerciseChecker/.gitlab-ci/05_stageBuild.yml b/ExerciseChecker/.gitlab-ci/05_stageBuild.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e2acdd1042c77d84b45e63e3b8b2cdaf217d92bc
--- /dev/null
+++ b/ExerciseChecker/.gitlab-ci/05_stageBuild.yml
@@ -0,0 +1,15 @@
+build:version:
+    stage: build
+    tags:
+        - build
+        - dind
+    image: docker:latest
+    services:
+        - docker:dind
+    script:
+        - !reference [ .build_script, script ]
+
+        # Here docker buildx can use cached images created in previous step
+        - docker buildx build --platform $DOCKER_PLATFORMS --file $DOCKERFILE --push --tag $CONTAINER_IMAGE .
+    rules:
+        -   if: '$CI_COMMIT_REF_PROTECTED == "true"'
\ No newline at end of file
diff --git a/ExerciseChecker/.gitlab-ci/06_stageClean.yml b/ExerciseChecker/.gitlab-ci/06_stageClean.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f8b6dba94adccbefeda89205f59a704b7f6d3875
--- /dev/null
+++ b/ExerciseChecker/.gitlab-ci/06_stageClean.yml
@@ -0,0 +1,75 @@
+clean:release:
+    stage: clean
+    tags:
+        - gitlab_clean
+    image: registry.gitlab.com/gitlab-ci-utils/curl-jq:latest
+    script:
+        - !reference [ .get_version, script ]
+        - !reference [ .clean_release, script ]
+    rules:
+        -   if: '$CI_COMMIT_REF_PROTECTED == "true"'
+
+
+clean:packages:
+    stage: clean
+    tags:
+        - gitlab_clean
+    image: registry.gitlab.com/gitlab-ci-utils/curl-jq:latest
+    script:
+        - !reference [ .get_version, script ]
+        - !reference [ .clean_packages, script ]
+    rules:
+        -   if: '$CI_COMMIT_REF_PROTECTED == "true"'
+
+
+clean:dev:release:
+    stage: clean
+    tags:
+        - gitlab_clean
+    image: registry.gitlab.com/gitlab-ci-utils/curl-jq:latest
+    script:
+        - !reference [ .get_version, script ]
+        - VERSION="${VERSION}${VERSION_DEV_SUFFIX}"
+        - !reference [ .clean_release, script ]
+    rules:
+        -   if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
+
+
+clean:dev:packages:
+    stage: clean
+    tags:
+        - gitlab_clean
+    image: registry.gitlab.com/gitlab-ci-utils/curl-jq:latest
+    script:
+        - !reference [ .get_version, script ]
+        - VERSION="${VERSION}${VERSION_DEV_SUFFIX}"
+        - !reference [ .clean_packages, script ]
+    rules:
+        -   if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
+
+
+clean:dev:dockerhub:
+    stage: clean
+    tags:
+        - clean
+        - dind
+    image: docker:latest
+    services:
+        - docker:dind
+    script:
+        - !reference [ .init_dind_script, script ]
+        - VERSION="${VERSION}${VERSION_DEV_SUFFIX}"
+
+        # Remove dev tag
+        ## Login to dockerhub (get JWT token)
+        - GET_TOKEN_DATA=$(jq --null-input --arg username "$DOCKER_REGISTRY_USER" --arg password "$DOCKER_REGISTRY_PASSWORD" '{"username":$username,"password":$password}')
+        - >
+            TOKEN=`curl --data "${GET_TOKEN_DATA}" \
+              --header "Content-Type: application/json" \
+              --header "JOB-TOKEN: $CI_JOB_TOKEN" \
+              "https://hub.docker.com/v2/users/login/" | jq -r .token`
+
+        ## Delete tag
+        - 'curl --header "Authorization: JWT ${TOKEN}" --request DELETE "https://hub.docker.com/v2/repositories/${DOCKER_REGISTRY_IMAGE}/tags/${VERSION}/"'
+    rules:
+        -   if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
\ No newline at end of file
diff --git a/ExerciseChecker/.gitlab-ci/07_stageUpload.yml b/ExerciseChecker/.gitlab-ci/07_stageUpload.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4c9bd456380ebbbbaa2e0498321c89e76427e16c
--- /dev/null
+++ b/ExerciseChecker/.gitlab-ci/07_stageUpload.yml
@@ -0,0 +1,22 @@
+upload:packages:wiki:
+    stage: upload
+    tags:
+        - gitlab_package
+    image: registry.gitlab.com/gitlab-ci-utils/curl-jq:latest
+    script:
+        # Install dependencies
+        - apk update
+        - apk add xz
+
+        - !reference [ .get_version, script ]
+        - !reference [ .get_packages_url, script ]
+
+        # Create archive
+        - mkdir -p $ARTIFACTS_FOLDER
+        - WIKI_ARCHIVE_PATH="${ARTIFACTS_FOLDER}/${WIKI_ARCHIVE_NAME}"
+        - tar -v -c -C "${CI_PROJECT_DIR}/${WIKI_FOLDER}" -J -f "${WIKI_ARCHIVE_PATH}" . # Ubuntu: tar --verbose --create --cd wiki-test-2 --xz --file file.tar.bz2
+
+        # Send package
+        - 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file ${WIKI_ARCHIVE_PATH} "${PACKAGE_URL_WIKI}";'
+    rules:
+        -   if: '$CI_COMMIT_REF_PROTECTED == "true"'
\ No newline at end of file
diff --git a/ExerciseChecker/.gitlab-ci/08_stageRelease.yml b/ExerciseChecker/.gitlab-ci/08_stageRelease.yml
new file mode 100644
index 0000000000000000000000000000000000000000..325d173cd88c4336e573d1dd33f6ffbe7745c653
--- /dev/null
+++ b/ExerciseChecker/.gitlab-ci/08_stageRelease.yml
@@ -0,0 +1,103 @@
+release:dockerhub:latest:
+    stage: release
+    tags:
+        - release
+        - dind
+    image: docker:latest
+    services:
+        - docker:dind
+    script:
+        - !reference [ .init_dind_script, script ]
+
+        # Push latest tag
+        - docker buildx imagetools create $CONTAINER_IMAGE --tag $DOCKER_REGISTRY_IMAGE:latest
+    rules:
+        -   if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
+
+
+release:wiki:
+    stage: release
+    tags:
+        - release
+    image: alpine:latest
+    script:
+        - apk update
+        - apk add git
+        - apk add jq
+
+        - !reference [ .get_version, script ]
+
+        # Define URL for the wiki in terms of project-agnostic predefined variables
+        - WIKI_URL="${CI_SERVER_PROTOCOL}://project_${CI_PROJECT_ID}_bot:${GITLAB_PROJECT_ACCESS_TOKEN}@${CI_SERVER_HOST}:${CI_SERVER_PORT}/${CI_PROJECT_PATH}.wiki.git"
+
+        # Clone this project's wiki under /tmp
+        - rm -rf "/tmp/${CI_PROJECT_NAME}.wiki"
+        - cd /tmp
+        - git clone "${WIKI_URL}"
+
+        # Enter the cloned repo
+        - cd "${CI_PROJECT_NAME}.wiki"
+
+        # Update the file
+        - mv .git/ ../
+        - rm -rf ./*
+        - mv ../.git/ ./
+        - cp "${CI_PROJECT_DIR}/.gitignore" .
+        - cp -R "${CI_PROJECT_DIR}/${WIKI_FOLDER}/." .
+
+        # Set committer info
+        - git config user.name "$GITLAB_USER_NAME"
+        - git config user.email "$GITLAB_USER_EMAIL"
+
+        # Commit the gitignore file
+        - git add ".gitignore"
+        - git commit -m "Add gitignore file" || true
+
+        # Commit the file
+        - git add .
+        - git commit -m "${VERSION}" || true
+
+        # Push the change back to the master branch of the wiki
+        - git push origin "HEAD:main"
+    rules:
+        -   if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
+
+
+release:gitlab:
+    stage: release
+    tags:
+        - release
+    image: registry.gitlab.com/gitlab-ci-utils/curl-jq:latest
+    script:
+        - !reference [ .get_version, script ]
+        - echo 'Running release_job'
+        - !reference [ .get_packages_url, script ]
+
+        # Extract description from CHANGELOG.md
+        - CHANGELOG_LINE_START=`awk '/##\ [0-9]+\.[0-9]+\.[0-9]+/{print NR; exit;}' CHANGELOG.md`
+        - CHANGELOG_LINE_END=`awk '/##\ [0-9]+\.[0-9]+\.[0-9]+/{ count++; if(count>1) {print NR; exit;}}' CHANGELOG.md`
+        - DESCRIPTION=`awk 'NR > '$CHANGELOG_LINE_START' && NR < '$CHANGELOG_LINE_END'' CHANGELOG.md`
+
+        # Create Release (can't be done by release_step of gitlab image because it don't have access to env var defined in script_step)
+        - >
+            RELEASE_DATA=$(jq --null-input --arg version "$VERSION" --arg description "# Changelog (version $VERSION) $DESCRIPTION" --arg tag_name "$VERSION" --arg ref "$CI_COMMIT_SHORT_SHA" '{
+                "name": $version,
+                "description": $description,
+                "tag_name": $tag_name,
+                "ref": $ref,
+                "assets": {
+                    "links": [
+                        {
+                          "name": "Wiki",
+                          "url": "'${PACKAGE_URL_WIKI}'",
+                        }
+                    ]   
+                }
+            }')
+        - >
+            curl --data "${RELEASE_DATA}" \
+              --header "Content-Type: application/json" \
+              --header "JOB-TOKEN: $CI_JOB_TOKEN" \
+              --request POST "${GITLAB_API_PROJECT_URL}/releases"
+    rules:
+        -   if: '$CI_COMMIT_REF_PROTECTED == "true"'
\ No newline at end of file
diff --git a/ExerciseChecker/package-lock.json b/ExerciseChecker/package-lock.json
index d23a2e213bdbd4d41e61a63691411928a7b7f6ea..9fbb82057e944f1175429e8f46a8abf09e272532 100644
--- a/ExerciseChecker/package-lock.json
+++ b/ExerciseChecker/package-lock.json
@@ -1,12 +1,13 @@
 {
     "name": "dojo_exercise_checker",
-    "version": "2.1.0",
+    "version": "2.2.0",
     "lockfileVersion": 3,
     "requires": true,
     "packages": {
         "": {
             "name": "dojo_exercise_checker",
-            "version": "2.1.0",
+            "version": "2.2.0",
+            "license": "AGPLv3",
             "dependencies": {
                 "ajv": "^8.12.0",
                 "axios": "^1.4.0",
@@ -14,12 +15,14 @@
                 "chalk": "^4.1.2",
                 "dockerode": "^3.3.5",
                 "dotenv": "^16.3.1",
+                "dotenv-expand": "^10.0.0",
                 "fs-extra": "^11.1.1",
                 "http-status-codes": "^2.2.0",
                 "json5": "^2.2.3",
                 "ora": "^5.4.1",
                 "tar-stream": "^3.1.6",
-                "winston": "^3.10.0"
+                "winston": "^3.10.0",
+                "yaml": "^2.3.2"
             },
             "bin": {
                 "dirmanager": "dist/app.js"
@@ -31,6 +34,7 @@
                 "@types/node": "^18.17.1",
                 "@types/tar-stream": "^2.2.2",
                 "dotenv-vault": "^1.25.0",
+                "genversion": "^3.1.1",
                 "pkg": "^5.8.1",
                 "tiny-typed-emitter": "^2.1.0",
                 "ts-node": "^10.9.1",
@@ -61,9 +65,9 @@
             }
         },
         "node_modules/@babel/helper-validator-identifier": {
-            "version": "7.22.15",
-            "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.15.tgz",
-            "integrity": "sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ==",
+            "version": "7.22.20",
+            "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+            "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
             "dev": true,
             "engines": {
                 "node": ">=6.9.0"
@@ -299,9 +303,9 @@
             "dev": true
         },
         "node_modules/@oclif/plugin-help": {
-            "version": "5.2.19",
-            "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-5.2.19.tgz",
-            "integrity": "sha512-gf6/dFtzMJ8RA4ovlBCBGJsZsd4jPXhYWJho+Gh6KmA+Ev9LupoExbE0qT+a2uHJyHEvIg4uX/MBW3qdERD/8g==",
+            "version": "5.2.20",
+            "resolved": "https://registry.npmjs.org/@oclif/plugin-help/-/plugin-help-5.2.20.tgz",
+            "integrity": "sha512-u+GXX/KAGL9S10LxAwNUaWdzbEBARJ92ogmM7g3gDVud2HioCmvWQCDohNRVZ9GYV9oKwZ/M8xwd6a1d95rEKQ==",
             "dev": true,
             "dependencies": {
                 "@oclif/core": "^2.15.0"
@@ -365,9 +369,9 @@
             }
         },
         "node_modules/@oclif/plugin-not-found": {
-            "version": "2.4.1",
-            "resolved": "https://registry.npmjs.org/@oclif/plugin-not-found/-/plugin-not-found-2.4.1.tgz",
-            "integrity": "sha512-LqW7qpw5Q8ploRiup2jEIMQJXcxHP1tpwj45GApKQMe7GRdGdRdjBT9Tu+U2tdEgMqgMplAIhOsYCx2nc2nMSw==",
+            "version": "2.4.3",
+            "resolved": "https://registry.npmjs.org/@oclif/plugin-not-found/-/plugin-not-found-2.4.3.tgz",
+            "integrity": "sha512-nIyaR4y692frwh7wIHZ3fb+2L6XEecQwRDIb4zbEam0TvaVmBQWZoColQyWA84ljFBPZ8XWiQyTz+ixSwdRkqg==",
             "dev": true,
             "dependencies": {
                 "@oclif/core": "^2.15.0",
@@ -433,9 +437,9 @@
             }
         },
         "node_modules/@oclif/plugin-update": {
-            "version": "3.2.3",
-            "resolved": "https://registry.npmjs.org/@oclif/plugin-update/-/plugin-update-3.2.3.tgz",
-            "integrity": "sha512-JVKwp4ysG9GU4RmG59MZYMunz8onRI+wEQzJThyYkUFd0VfZviYt2FHsyoNtxi30l0tInC8APgKp1pCCO4e+FQ==",
+            "version": "3.2.4",
+            "resolved": "https://registry.npmjs.org/@oclif/plugin-update/-/plugin-update-3.2.4.tgz",
+            "integrity": "sha512-41G7NTKND+yTpb8LHlvlMIcNoaEUIIJuEwju9igL+ME/pN/53opeXgFV2IjjeFiexXj50OfesY9OQ6lqOZHw+g==",
             "dev": true,
             "dependencies": {
                 "@oclif/core": "^2.11.8",
@@ -553,9 +557,9 @@
             }
         },
         "node_modules/@oclif/plugin-warn-if-update-available": {
-            "version": "2.1.0",
-            "resolved": "https://registry.npmjs.org/@oclif/plugin-warn-if-update-available/-/plugin-warn-if-update-available-2.1.0.tgz",
-            "integrity": "sha512-liTWd/qSIqALsikr88CAB9o2xGFt0LdT5REbhxtrx16/trRmkxQ+0RHK1FieGZAzEENx/4D3YcC/Y67a0uyO0g==",
+            "version": "2.1.1",
+            "resolved": "https://registry.npmjs.org/@oclif/plugin-warn-if-update-available/-/plugin-warn-if-update-available-2.1.1.tgz",
+            "integrity": "sha512-y7eSzT6R5bmTIJbiMMXgOlbBpcWXGlVhNeQJBLBCCy1+90Wbjyqf6uvY0i2WcO4sh/THTJ20qCW80j3XUlgDTA==",
             "dev": true,
             "dependencies": {
                 "@oclif/core": "^2.15.0",
@@ -624,9 +628,9 @@
             }
         },
         "node_modules/@oclif/screen": {
-            "version": "3.0.6",
-            "resolved": "https://registry.npmjs.org/@oclif/screen/-/screen-3.0.6.tgz",
-            "integrity": "sha512-nEv7dFPxCrWrvK6dQ8zya0/Kb54EXVcwIKV9capjSa89ZDoOo+qH0YSo4/eQVECXgW3eUvgKLDIcIt62YBk0HA==",
+            "version": "3.0.7",
+            "resolved": "https://registry.npmjs.org/@oclif/screen/-/screen-3.0.7.tgz",
+            "integrity": "sha512-jQBPHcMh5rcIPKdqA6xlzioLOmkaVnjg2MVyjMzBKV8hDhLWNSiZqx7NAWXpP70v2LFvGdVoV8BSbK9iID3eHg==",
             "dev": true,
             "engines": {
                 "node": ">=12.0.0"
@@ -657,18 +661,18 @@
             "dev": true
         },
         "node_modules/@types/cli-progress": {
-            "version": "3.11.2",
-            "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.2.tgz",
-            "integrity": "sha512-Yt/8rEJalfa9ve2SbfQnwFHrc9QF52JIZYHW3FDaTMpkCvnns26ueKiPHDxyJ0CS//IqjMINTx7R5Xa7k7uFHQ==",
+            "version": "3.11.3",
+            "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.3.tgz",
+            "integrity": "sha512-/+C9xAdVtc+g5yHHkGBThgAA8rYpi5B+2ve3wLtybYj0JHEBs57ivR4x/zGfSsplRnV+psE91Nfin1soNKqz5Q==",
             "dev": true,
             "dependencies": {
                 "@types/node": "*"
             }
         },
         "node_modules/@types/docker-modem": {
-            "version": "3.0.3",
-            "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.3.tgz",
-            "integrity": "sha512-i1A2Etnav7uHizZ87vUf4EqwJehY3JOcTfBS0pGBlO+HQ0jg2lUMCaJRg9VQM8ldZkpYdIfsenxcTOCpwxPXEg==",
+            "version": "3.0.4",
+            "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.4.tgz",
+            "integrity": "sha512-INK4TOrJ9hbgaSqHA1HaEOCcYVftJRH0v03gCg6R57JGKgltkDvdFYBtoN4lHrJ3h8aF1upvEPN2eWVLIvKStQ==",
             "dev": true,
             "dependencies": {
                 "@types/node": "*",
@@ -676,9 +680,9 @@
             }
         },
         "node_modules/@types/dockerode": {
-            "version": "3.3.19",
-            "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.19.tgz",
-            "integrity": "sha512-7CC5yIpQi+bHXwDK43b/deYXteP3Lem9gdocVVHJPSRJJLMfbiOchQV3rDmAPkMw+n3GIVj7m1six3JW+VcwwA==",
+            "version": "3.3.20",
+            "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.20.tgz",
+            "integrity": "sha512-Q+1e3z6SPWXR/Sk+WIyJFVsSDg78S7MDaGcwAh1WKlveO1tVO8TF1rOzJir5GLnqzEdUbclFKlw/4rhwESxwPw==",
             "dev": true,
             "dependencies": {
                 "@types/docker-modem": "*",
@@ -686,9 +690,9 @@
             }
         },
         "node_modules/@types/fs-extra": {
-            "version": "11.0.1",
-            "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.1.tgz",
-            "integrity": "sha512-MxObHvNl4A69ofaTRU8DFqvgzzv8s9yRtaPPm5gud9HDNvpB3GPQFvNuTWAI59B9huVGV5jXYJwbCsmBsOGYWA==",
+            "version": "11.0.2",
+            "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.2.tgz",
+            "integrity": "sha512-c0hrgAOVYr21EX8J0jBMXGLMgJqVf/v6yxi0dLaJboW9aQPh16Id+z6w2Tx1hm+piJOLv8xPfVKZCLfjPw/IMQ==",
             "dev": true,
             "dependencies": {
                 "@types/jsonfile": "*",
@@ -696,30 +700,30 @@
             }
         },
         "node_modules/@types/js-yaml": {
-            "version": "4.0.5",
-            "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz",
-            "integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==",
+            "version": "4.0.6",
+            "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.6.tgz",
+            "integrity": "sha512-ACTuifTSIIbyksx2HTon3aFtCKWcID7/h3XEmRpDYdMCXxPbl+m9GteOJeaAkiAta/NJaSFuA7ahZ0NkwajDSw==",
             "dev": true
         },
         "node_modules/@types/jsonfile": {
-            "version": "6.1.1",
-            "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.1.tgz",
-            "integrity": "sha512-GSgiRCVeapDN+3pqA35IkQwasaCh/0YFH5dEF6S88iDvEn901DjOeH3/QPY+XYP1DFzDZPvIvfeEgk+7br5png==",
+            "version": "6.1.2",
+            "resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.2.tgz",
+            "integrity": "sha512-8t92P+oeW4d/CRQfJaSqEwXujrhH4OEeHRjGU3v1Q8mUS8GPF3yiX26sw4svv6faL2HfBtGTe2xWIoVgN3dy9w==",
             "dev": true,
             "dependencies": {
                 "@types/node": "*"
             }
         },
         "node_modules/@types/node": {
-            "version": "18.17.15",
-            "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.15.tgz",
-            "integrity": "sha512-2yrWpBk32tvV/JAd3HNHWuZn/VDN1P+72hWirHnvsvTGSqbANi+kSeuQR9yAHnbvaBvHDsoTdXV0Fe+iRtHLKA==",
+            "version": "18.18.3",
+            "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.3.tgz",
+            "integrity": "sha512-0OVfGupTl3NBFr8+iXpfZ8NR7jfFO+P1Q+IO/q0wbo02wYkP5gy36phojeYWpLQ6WAMjl+VfmqUk2YbUfp0irA==",
             "dev": true
         },
         "node_modules/@types/ssh2": {
-            "version": "1.11.13",
-            "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.11.13.tgz",
-            "integrity": "sha512-08WbG68HvQ2YVi74n2iSUnYHYpUdFc/s2IsI0BHBdJwaqYJpWlVv9elL0tYShTv60yr0ObdxJR5NrCRiGJ/0CQ==",
+            "version": "1.11.14",
+            "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.11.14.tgz",
+            "integrity": "sha512-O/U38mvV4jVVrdtZz8KpmitkmeD/PUDeDNNueQhm34166dmaqb1iZ3sfarSxBArM2/iX4PZVJY3EOta0Zks9hw==",
             "dev": true,
             "dependencies": {
                 "@types/node": "^18.11.18"
@@ -911,9 +915,9 @@
             }
         },
         "node_modules/axios": {
-            "version": "1.5.0",
-            "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz",
-            "integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==",
+            "version": "1.5.1",
+            "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.1.tgz",
+            "integrity": "sha512-Q28iYCWzNHjAm+yEAot5QaAMxhMghWLFVf7rRdwhUI+c2jix2DUXjAHXVi+s1ibs3mjPO/cCgbA++3BjD0vP/A==",
             "dependencies": {
                 "follow-redirects": "^1.15.0",
                 "form-data": "^4.0.0",
@@ -1152,9 +1156,9 @@
             }
         },
         "node_modules/cli-spinners": {
-            "version": "2.9.0",
-            "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz",
-            "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==",
+            "version": "2.9.1",
+            "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.1.tgz",
+            "integrity": "sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==",
             "engines": {
                 "node": ">=6"
             },
@@ -1257,6 +1261,15 @@
                 "node": ">= 0.8"
             }
         },
+        "node_modules/commander": {
+            "version": "7.2.0",
+            "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+            "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
+            "dev": true,
+            "engines": {
+                "node": ">= 10"
+            }
+        },
         "node_modules/concat-map": {
             "version": "0.0.1",
             "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -1439,6 +1452,14 @@
                 "url": "https://github.com/motdotla/dotenv?sponsor=1"
             }
         },
+        "node_modules/dotenv-expand": {
+            "version": "10.0.0",
+            "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz",
+            "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==",
+            "engines": {
+                "node": ">=12"
+            }
+        },
         "node_modules/dotenv-vault": {
             "version": "1.25.0",
             "resolved": "https://registry.npmjs.org/dotenv-vault/-/dotenv-vault-1.25.0.tgz",
@@ -1513,12 +1534,6 @@
                 "is-arrayish": "^0.2.1"
             }
         },
-        "node_modules/error-ex/node_modules/is-arrayish": {
-            "version": "0.2.1",
-            "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
-            "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
-            "dev": true
-        },
         "node_modules/escalade": {
             "version": "3.1.1",
             "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
@@ -1709,15 +1724,24 @@
                 "node": ">=8"
             }
         },
+        "node_modules/find-package": {
+            "version": "1.0.0",
+            "resolved": "https://registry.npmjs.org/find-package/-/find-package-1.0.0.tgz",
+            "integrity": "sha512-yVn71XCCaNgxz58ERTl8nA/8YYtIQDY9mHSrgFBfiFtdNNfY0h183Vh8BRkKxD8x9TUw3ec290uJKhDVxqGZBw==",
+            "dev": true,
+            "dependencies": {
+                "parents": "^1.0.1"
+            }
+        },
         "node_modules/fn.name": {
             "version": "1.1.0",
             "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
             "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="
         },
         "node_modules/follow-redirects": {
-            "version": "1.15.2",
-            "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
-            "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
+            "version": "1.15.3",
+            "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
+            "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
             "funding": [
                 {
                     "type": "individual",
@@ -1804,11 +1828,21 @@
                 "node": ">=14.14"
             }
         },
-        "node_modules/function-bind": {
-            "version": "1.1.1",
-            "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
-            "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
-            "dev": true
+        "node_modules/genversion": {
+            "version": "3.1.1",
+            "resolved": "https://registry.npmjs.org/genversion/-/genversion-3.1.1.tgz",
+            "integrity": "sha512-/H861PMsihhjgX2qqhTN8egM11V04imhA+3JRFY3jjPua2Sy1NqaqqQPjSP8rdM9jZoKpFhVj9g3Fs9XPCjBYQ==",
+            "dev": true,
+            "dependencies": {
+                "commander": "^7.2.0",
+                "find-package": "^1.0.0"
+            },
+            "bin": {
+                "genversion": "bin/genversion.js"
+            },
+            "engines": {
+                "node": ">=10.0.0"
+            }
         },
         "node_modules/get-caller-file": {
             "version": "2.0.5",
@@ -1872,13 +1906,10 @@
             "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
         },
         "node_modules/has": {
-            "version": "1.0.3",
-            "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
-            "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+            "version": "1.0.4",
+            "resolved": "https://registry.npmjs.org/has/-/has-1.0.4.tgz",
+            "integrity": "sha512-qdSAmqLF6209RFj4VVItywPMbm3vWylknmB3nvNiUIs72xAimcM8nVYxYr7ncvZq5qzk9MKIZR8ijqD/1QuYjQ==",
             "dev": true,
-            "dependencies": {
-                "function-bind": "^1.1.1"
-            },
             "engines": {
                 "node": ">= 0.4.0"
             }
@@ -1909,9 +1940,9 @@
             }
         },
         "node_modules/http-status-codes": {
-            "version": "2.2.0",
-            "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.2.0.tgz",
-            "integrity": "sha512-feERVo9iWxvnejp3SEfm/+oNG517npqL2/PIA8ORjyOZjGC7TwCRQsZylciLS64i6pJ0wRYz3rkXLRwbtFa8Ng=="
+            "version": "2.3.0",
+            "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz",
+            "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="
         },
         "node_modules/https-proxy-agent": {
             "version": "5.0.1",
@@ -2052,9 +2083,10 @@
             }
         },
         "node_modules/is-arrayish": {
-            "version": "0.3.2",
-            "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
-            "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
+            "version": "0.2.1",
+            "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+            "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
+            "dev": true
         },
         "node_modules/is-core-module": {
             "version": "2.9.0",
@@ -2614,6 +2646,15 @@
                 "node": ">=8"
             }
         },
+        "node_modules/parents": {
+            "version": "1.0.1",
+            "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz",
+            "integrity": "sha512-mXKF3xkoUt5td2DoxpLmtOmZvko9VfFpwRwkKDHSNvgmpLAeBo18YDhcPbBzJq+QLCHMbGOfzia2cX4U+0v9Mg==",
+            "dev": true,
+            "dependencies": {
+                "path-platform": "~0.11.15"
+            }
+        },
         "node_modules/parse-json": {
             "version": "4.0.0",
             "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
@@ -2652,6 +2693,15 @@
             "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
             "dev": true
         },
+        "node_modules/path-platform": {
+            "version": "0.11.15",
+            "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz",
+            "integrity": "sha512-Y30dB6rab1A/nfEKsZxmr01nUotHX0c/ZiIAsCTatEe1CmS5Pm5He7fZ195bPT7RdquoaL8lLxFCMQi/bS7IJg==",
+            "dev": true,
+            "engines": {
+                "node": ">= 0.8.0"
+            }
+        },
         "node_modules/path-type": {
             "version": "4.0.0",
             "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -2926,9 +2976,9 @@
             }
         },
         "node_modules/resolve": {
-            "version": "1.22.4",
-            "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.4.tgz",
-            "integrity": "sha512-PXNdCiPqDqeUou+w1C2eTQbNfxKSuMxqTCuvlmmMsk1NWHL5fRrhY6Pl0qEYYc6+QqGClco1Qj8XnjPego4wfg==",
+            "version": "1.22.6",
+            "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz",
+            "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==",
             "dev": true,
             "dependencies": {
                 "is-core-module": "^2.13.0",
@@ -3143,6 +3193,11 @@
                 "is-arrayish": "^0.3.1"
             }
         },
+        "node_modules/simple-swizzle/node_modules/is-arrayish": {
+            "version": "0.3.2",
+            "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+            "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
+        },
         "node_modules/slash": {
             "version": "3.0.0",
             "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -3673,6 +3728,14 @@
             "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
             "dev": true
         },
+        "node_modules/yaml": {
+            "version": "2.3.2",
+            "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.2.tgz",
+            "integrity": "sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==",
+            "engines": {
+                "node": ">= 14"
+            }
+        },
         "node_modules/yargs": {
             "version": "16.2.0",
             "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
diff --git a/ExerciseChecker/package.json b/ExerciseChecker/package.json
index 0a3e7d1c606ac714ee1f3d3837183e52b1a56a8a..4553b1e9794ce1707fb2e2fadf0702abe61fcfbb 100644
--- a/ExerciseChecker/package.json
+++ b/ExerciseChecker/package.json
@@ -1,6 +1,9 @@
 {
     "name"           : "dojo_exercise_checker",
-    "version"        : "2.1.0",
+    "description"    : "App that check an exercise of the Dojo project",
+    "version"        : "2.2.0",
+    "license"        : "AGPLv3",
+    "author"         : "Michaël Minelli <dojo@minelli.me>",
     "main"           : "dist/app.js",
     "bin"            : {
         "dirmanager": "./dist/app.js"
@@ -18,9 +21,11 @@
         ]
     },
     "scripts"        : {
-        "build"    : "npx tsc",
-        "start:dev": "npx ts-node src/app.ts",
-        "test"     : "echo \"Error: no test specified\" && exit 1"
+        "dotenv:build": "npx dotenv-vault local build",
+        "genversion"  : "npx genversion -s -e src/config/Version.ts",
+        "build"       : "npm run genversion; npx tsc",
+        "start:dev"   : "npm run genversion; npx ts-node src/app.ts",
+        "test"        : "echo \"Error: no test specified\" && exit 1"
     },
     "dependencies"   : {
         "ajv"              : "^8.12.0",
@@ -29,12 +34,14 @@
         "chalk"            : "^4.1.2",
         "dockerode"        : "^3.3.5",
         "dotenv"           : "^16.3.1",
+        "dotenv-expand"    : "^10.0.0",
         "fs-extra"         : "^11.1.1",
         "http-status-codes": "^2.2.0",
         "json5"            : "^2.2.3",
         "ora"              : "^5.4.1",
         "tar-stream"       : "^3.1.6",
-        "winston"          : "^3.10.0"
+        "winston"          : "^3.10.0",
+        "yaml"             : "^2.3.2"
     },
     "devDependencies": {
         "@types/dockerode"  : "^3.3.19",
@@ -43,6 +50,7 @@
         "@types/node"       : "^18.17.1",
         "@types/tar-stream" : "^2.2.2",
         "dotenv-vault"      : "^1.25.0",
+        "genversion"        : "^3.1.1",
         "pkg"               : "^5.8.1",
         "tiny-typed-emitter": "^2.1.0",
         "ts-node"           : "^10.9.1",
diff --git a/ExerciseChecker/src/app.ts b/ExerciseChecker/src/app.ts
index 3d1df1d5211e9a544a1e773a05fab76fb0687e1f..63eef070ae3635dcf2c0245dd9e39337469df870 100644
--- a/ExerciseChecker/src/app.ts
+++ b/ExerciseChecker/src/app.ts
@@ -1,29 +1,30 @@
 // Read from the .env file
 // ATTENTION : This lines MUST be the first of this file (except for the path import)
 const path = require('node:path');
-require('dotenv').config({
-                             path      : path.join(__dirname, '../.env'),
-                             DOTENV_KEY: 'dotenv://:key_bebfddf18e3dd9a0bafafe0e383313f75add1da6fbe41ea5fde51f37ef1776aa@dotenv.local/vault/.env.vault?environment=development'
-                         });
+const myEnv = require('dotenv').config({
+                                           path      : path.join(__dirname, '../.env'),
+                                           DOTENV_KEY: 'dotenv://:key_bebfddf18e3dd9a0bafafe0e383313f75add1da6fbe41ea5fde51f37ef1776aa@dotenv.local/vault/.env.vault?environment=development'
+                                       });
+require('dotenv-expand').expand(myEnv);
 require('./shared/helpers/TypeScriptExtensions'); // ATTENTION : This line MUST be the second of this file
 
-import ClientsSharedConfig         from './sharedByClients/config/ClientsSharedConfig';
-import Styles                      from './types/Style';
-import Icon                        from './sharedByClients/types/Icon';
-import RecursiveFilesStats         from './shared/helpers/recursiveFilesStats/RecursiveFilesStats';
-import Toolbox                     from './shared/helpers/Toolbox';
-import ExerciseCheckerError        from './shared/types/Dojo/ExerciseCheckerError';
-import { exec }                    from 'child_process';
-import util                        from 'util';
-import fs                          from 'fs-extra';
-import HttpManager                 from './managers/HttpManager';
-import DojoBackendManager          from './managers/DojoBackendManager';
-import Config                      from './config/Config';
-import ArchiveHelper               from './shared/helpers/ArchiveHelper';
-import ExerciseDockerCompose       from './sharedByClients/helpers/Dojo/ExerciseDockerCompose';
-import ExerciseResultsValidation   from './sharedByClients/helpers/Dojo/ExerciseResultsValidation';
-import ExerciseAssignment          from './sharedByClients/models/ExerciseAssignment';
-import ClientsSharedExerciseHelper from './sharedByClients/helpers/Dojo/ClientsSharedExerciseHelper';
+import ClientsSharedConfig                  from './sharedByClients/config/ClientsSharedConfig';
+import Styles                               from './types/Style';
+import Icon                                 from './sharedByClients/types/Icon';
+import RecursiveFilesStats                  from './shared/helpers/recursiveFilesStats/RecursiveFilesStats';
+import Toolbox                              from './shared/helpers/Toolbox';
+import ExerciseCheckerError                 from './shared/types/Dojo/ExerciseCheckerError';
+import { exec }                             from 'child_process';
+import util                                 from 'util';
+import fs                                   from 'fs-extra';
+import HttpManager                          from './managers/HttpManager';
+import DojoBackendManager                   from './managers/DojoBackendManager';
+import Config                               from './config/Config';
+import ArchiveHelper                        from './shared/helpers/ArchiveHelper';
+import ExerciseDockerCompose                from './sharedByClients/helpers/Dojo/ExerciseDockerCompose';
+import ExerciseResultsSanitizerAndValidator from './sharedByClients/helpers/Dojo/ExerciseResultsSanitizerAndValidator';
+import ExerciseAssignment                   from './sharedByClients/models/ExerciseAssignment';
+import ClientsSharedExerciseHelper          from './sharedByClients/helpers/Dojo/ClientsSharedExerciseHelper';
 
 
 (async () => {
@@ -35,7 +36,10 @@ import ClientsSharedExerciseHelper from './sharedByClients/helpers/Dojo/ClientsS
 
     let exerciseAssignment: ExerciseAssignment | undefined;
     let exerciseDockerCompose: ExerciseDockerCompose;
-    let exerciseResultsValidation: ExerciseResultsValidation;
+    let exerciseResultsValidation: ExerciseResultsSanitizerAndValidator;
+
+    let haveResultsVolume: boolean;
+
 
     /*
      //////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 1:
@@ -55,6 +59,8 @@ import ClientsSharedExerciseHelper from './sharedByClients/helpers/Dojo/ClientsS
             fs.mkdirSync(path.dirname(filePath), { recursive: true });
             fs.writeFileSync(filePath, immutableFile.content, { encoding: 'base64' });
         });
+
+        haveResultsVolume = exerciseAssignment.assignmentFile.result.volume !== undefined;
     }
 
 
@@ -65,12 +71,16 @@ import ClientsSharedExerciseHelper from './sharedByClients/helpers/Dojo/ClientsS
      - Get logs from linked services
      */
     {
+        let composeFileOverride: string[] = [];
         const composeOverridePath: string = path.join(Config.folders.project, 'docker-compose-override.yml');
+        if ( haveResultsVolume ) {
+            const composeOverride = fs.readFileSync(path.join(__dirname, '../assets/docker-compose-override.yml'), 'utf8').replace('{{VOLUME_NAME}}', exerciseAssignment.assignmentFile.result.volume!).replace('{{MOUNT_PATH}}', Config.folders.resultsExercise);
+            fs.writeFileSync(composeOverridePath, composeOverride);
 
-        const composeOverride = fs.readFileSync(path.join(__dirname, '../assets/docker-compose-override.yml'), 'utf8').replace('{{VOLUME_NAME}}', exerciseAssignment.assignmentFile.result.volume).replace('{{MOUNT_PATH}}', Config.folders.resultsExercise);
-        fs.writeFileSync(composeOverridePath, composeOverride);
+            composeFileOverride = [ composeOverridePath ];
+        }
 
-        exerciseDockerCompose = new ExerciseDockerCompose(ClientsSharedConfig.dockerCompose.projectName, exerciseAssignment.assignmentFile, Config.folders.project, [ composeOverridePath ]);
+        exerciseDockerCompose = new ExerciseDockerCompose(ClientsSharedConfig.dockerCompose.projectName, exerciseAssignment.assignmentFile, Config.folders.project, composeFileOverride);
 
         try {
             await new Promise<void>((resolve, reject) => {
@@ -92,7 +102,7 @@ import ClientsSharedExerciseHelper from './sharedByClients/helpers/Dojo/ClientsS
             });
         } catch ( error ) { }
 
-        fs.rmSync(composeOverridePath);
+        fs.rmSync(composeOverridePath, { force: true });
         fs.writeFileSync(path.join(Config.folders.resultsDojo, 'dockerComposeLogs.txt'), exerciseDockerCompose.allLogs);
 
         if ( !exerciseDockerCompose.success ) {
@@ -104,7 +114,7 @@ import ClientsSharedExerciseHelper from './sharedByClients/helpers/Dojo/ClientsS
 
     //////////////////////////////////////////////////////////////////////////////////////////////////////////// Step 3: Check content requirements and content size
     {
-        exerciseResultsValidation = new ExerciseResultsValidation(Config.folders.resultsDojo, Config.folders.resultsExercise);
+        exerciseResultsValidation = new ExerciseResultsSanitizerAndValidator(Config.folders.resultsDojo, Config.folders.resultsExercise, exerciseDockerCompose.exitCode);
 
         try {
             await new Promise<void>((resolve) => {
@@ -146,7 +156,7 @@ import ClientsSharedExerciseHelper from './sharedByClients/helpers/Dojo/ClientsS
                 liteStats               : true
             });
 
-            await DojoBackendManager.sendResults(exerciseDockerCompose.exitCode, commit, exerciseResultsValidation.exerciseResults!, files, await ArchiveHelper.getBase64(Config.folders.resultsVolume));
+            await DojoBackendManager.sendResults(exerciseDockerCompose.exitCode, commit, exerciseResultsValidation.exerciseResults, files, await ArchiveHelper.getBase64(Config.folders.resultsVolume));
         } catch ( error ) {
             console.error(Styles.ERROR(`${ Icon.ERROR } Error while uploading the results`));
             console.error(JSON.stringify(error));
@@ -161,7 +171,7 @@ import ClientsSharedExerciseHelper from './sharedByClients/helpers/Dojo/ClientsS
      - Exit with container exit code
      */
     {
-        ClientsSharedExerciseHelper.displayExecutionResults(exerciseResultsValidation.exerciseResults!, exerciseDockerCompose.exitCode, Styles, `\n\n${ Icon.INFO }️ More detailed logs and resources may be available in artifacts`);
+        ClientsSharedExerciseHelper.displayExecutionResults(exerciseResultsValidation.exerciseResults, exerciseDockerCompose.exitCode, Styles, `\n\n${ Icon.INFO }️ More detailed logs and resources may be available in artifacts`);
 
         process.exit(exerciseDockerCompose.exitCode);
     }
diff --git a/ExerciseChecker/src/config/Config.ts b/ExerciseChecker/src/config/Config.ts
index 9072ff8f963c7dd4cd392cd07ff6e3397caa6ce8..42c446229456f1c92c316a4e7b27b54779f73c7a 100644
--- a/ExerciseChecker/src/config/Config.ts
+++ b/ExerciseChecker/src/config/Config.ts
@@ -13,6 +13,12 @@ class Config {
         id: string; secret: string;
     };
 
+    public readonly dockerhub: {
+        repositories: {
+            exerciseChecker: string
+        }
+    };
+
     constructor() {
         this.appName = process.env.APP_NAME || '';
 
@@ -28,6 +34,12 @@ class Config {
             id    : process.env.DOJO_EXERCISE_ID || '',
             secret: process.env.DOJO_SECRET || ''
         };
+
+        this.dockerhub = {
+            repositories: {
+                exerciseChecker: process.env.DOCKERHUB_EXERCISE_CHECKER_REPOSITORY || ''
+            }
+        };
     }
 
     private resetResultsVolume(): void {
diff --git a/ExerciseChecker/src/managers/HttpManager.ts b/ExerciseChecker/src/managers/HttpManager.ts
index fb113963ab8d37459ff72bf05d33827598889edd..54d6990ad2c31f3a0307a56b13f305b0285ee0db 100644
--- a/ExerciseChecker/src/managers/HttpManager.ts
+++ b/ExerciseChecker/src/managers/HttpManager.ts
@@ -2,6 +2,11 @@ import axios, { AxiosRequestHeaders } from 'axios';
 import FormData                       from 'form-data';
 import ClientsSharedConfig            from '../sharedByClients/config/ClientsSharedConfig';
 import Config                         from '../config/Config';
+import { version }                    from '../config/Version';
+import boxen                          from 'boxen';
+import DojoStatusCode                 from '../shared/types/Dojo/DojoStatusCode';
+import DojoBackendResponse            from '../shared/types/Dojo/DojoBackendResponse';
+import { StatusCodes }                from 'http-status-codes';
 
 
 class HttpManager {
@@ -12,6 +17,19 @@ class HttpManager {
         this.registerResponseInterceptor();
     }
 
+    private requestError(message: string) {
+        console.log(boxen(message, {
+            title         : 'Request error',
+            titleAlignment: 'center',
+            borderColor   : 'red',
+            borderStyle   : 'bold',
+            margin        : 1,
+            padding       : 1,
+            textAlignment : 'left'
+        }));
+        process.exit(1);
+    }
+
     private registerRequestInterceptor() {
         axios.interceptors.request.use((config) => {
             if ( config.data instanceof FormData ) {
@@ -26,6 +44,9 @@ class HttpManager {
                 }
 
                 config.headers.Authorization = `ExerciseSecret ${ Config.exercise.secret }`;
+
+                config.headers['client'] = 'DojoExerciseChecker';
+                config.headers['client-version'] = version;
             }
 
             return config;
@@ -36,6 +57,25 @@ class HttpManager {
         axios.interceptors.response.use((response) => {
             return response;
         }, (error) => {
+            if ( error.response ) {
+                if ( error.response.status === StatusCodes.METHOD_NOT_ALLOWED && error.response.data ) {
+                    const data: DojoBackendResponse<{}> = error.response.data;
+
+                    switch ( data.code ) {
+                        case DojoStatusCode.CLIENT_NOT_SUPPORTED:
+                            this.requestError('Client not recognized by the server. Please contact the administrator.');
+                            break;
+                        case DojoStatusCode.CLIENT_VERSION_NOT_SUPPORTED:
+                            this.requestError(`ExerciseChecker version not supported by the server.\nPlease check that the CI/CD pipeline use the "${ Config.dockerhub.repositories.exerciseChecker }:latest" image.\nIf yes, try again later and if the problem persists, please contact the administrator.`);
+                            break;
+                        default:
+                            break;
+                    }
+                }
+            } else {
+                this.requestError('Error connecting to the server. Please check your internet connection. If the problem persists, please contact the administrator.');
+            }
+
             return Promise.reject(error);
         });
     }
diff --git a/ExerciseChecker/src/shared b/ExerciseChecker/src/shared
index 8d7e3ca0cca10e874ac48e19e47da8a1491ccba7..efe1bf313f57d1826faf935c183d37a0835f8c2d 160000
--- a/ExerciseChecker/src/shared
+++ b/ExerciseChecker/src/shared
@@ -1 +1 @@
-Subproject commit 8d7e3ca0cca10e874ac48e19e47da8a1491ccba7
+Subproject commit efe1bf313f57d1826faf935c183d37a0835f8c2d
diff --git a/ExerciseChecker/src/sharedByClients b/ExerciseChecker/src/sharedByClients
index 4ff3846e9415a6122b0b966be089eec3f0117f4f..d9379b055a4626e4b35cf4cc4a7429040a4aeaf7 160000
--- a/ExerciseChecker/src/sharedByClients
+++ b/ExerciseChecker/src/sharedByClients
@@ -1 +1 @@
-Subproject commit 4ff3846e9415a6122b0b966be089eec3f0117f4f
+Subproject commit d9379b055a4626e4b35cf4cc4a7429040a4aeaf7