Voir la partie 1 pour le contexte
Code
Comme dit précédemment, on part d’un code fonctionnel avec une landing page. Le point de départ est le commit 0760482f du repo https://gitlab.com/watycorp/bakeryproject.
Je vais donc créer mon ticket et ma MR pour rendre le site plus “responsive” :
https://gitlab.com/watycorp/bakeryproject/-/merge_requests/1
Je récupère la nouvelle branche afin de changer le code. Grâce àgit fetch
je récupère les informations et je peux git switch
.
Je change mon code et j’intègre mes tests (commits 700ac7d et 4f17f4e).
Il y a 3 tests principalement:- npm run test -> lance les tests jest (unitaires)
- npm run test:e2e -> lance les tests cypress (fonctionnel et E2E)
- npm run test:all -> lance les deux cités l’un après l’autre
Avec ce code tous les tests passent. Avant de pousser la modification je vais construire le fichier de configuration CI/CD.
CI/CD
Comme précédemment, je crée un ticket puis une MR et la branche, que je récupère en local.
Pour pouvoir tester rapidement mes modifications dans .gitlab-ci.yaml, j’utilise gitlab-ci-local que j’ai conteneurisé (via ce boilerplate).
Règles d’exécution
En premier, on va définir les règles qui vont régir quand un pipeline va tourner.
On utilise le mot clé workflow:rules:
pour ça ( cf https://docs.gitlab.com/ci/yaml/#workflowrules )
On veut donc que le pipeline tourne:
- quand main reçoit du code
- quand une MR est mise à jour
- quand une branche est mise à jour
Mais on ne veut pas 2 pipelines quand on push sur une branche dans une MR. Pour trouver les règles on peut s’appuyer sur les variables prédéfinies.
La règle pour main est soit:- if: $CI_COMMIT_BRANCH == 'main'
ou alors
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
La règle pour un push de branche est:
if: $CI_COMMIT_BRANCH
Pour exclure le cas push + MR on va mettre:
- if: $CI_COMMIT_BRANCH && $CI_PIPELINE_SOURCE == "merge_request_event"
when: never
Ce qui revient à dire que jamais on ne fait de pipeline si ces deux conditions sont vraies.
On va donc écrire:
workflow:
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH
- if: $CI_COMMIT_BRANCH && $CI_PIPELINE_SOURCE == "merge_request_event"
when: never
Image par défaut
Ensuite, on va désigner une image docker par défaut pour les pipelines, étant donné que tout notre projet est en Javascript. On travaille avec node v22.15. On va la définir ainsi:
default:
image: node:22.15
Premières étapes
On va définir ensuite les différentes étapes. Il faut pour toutes les étapes installer les dépendances, builder le projet et tester. Prenons une approche itérative de quelqu’un qui ne connaît pas trop le projet ou Javascript.
Je sais que le premier test à lancer est npm run test
J’ajoute donc comme premier ‘stage’ :
test:
script:
- npm run test
Évidemment j’ai l’erreur “jest not found”. Les modules n’ont pas été installés.
Ajoutons npm install
juste avant.
Ça fonctionne !
Améliorations
Je vais ajouter maintenant le test avec cypress. Je transforme le premier test:
en unit-test:
et je crée un fonctional-test:
On a :
unit-test:
stage: test
script:
- npm install
- npm run test
fonctional-test:
stage: test
script:
- npm install
- npm run test:e2e
- les deux tests tournent en parallèle
- les deux tests perdent du temps à installer les dépendances
- L’exécution par Cypress est en échec car il y a un problème de dépendances.
Pour résoudre le fait qu’on soit en parallèle, soit:
- on met chaque test dans un stage différent
- on introduit un lien de dépendance
=> J’introduis un lien de dépendance en ajoutant needs: [unit-test]
dans mon test fonctionnel.
On peut résoudre le fait d’installer les modules à chaque étape grâce au cache
.
Le cache est un chemin partagé entre les différents jobs du pipeline.
=> Je rajoute dans la partie default une déclaration pour le cache, ce qui nous donne donc dans notre cas:
cache: node_modules/
default:
image: node:22.15
cache:
paths:
- node_modules/
workflow:
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH
- if: $CI_COMMIT_BRANCH && $CI_PIPELINE_SOURCE == "merge_request_event"
when: never
unit-test:
stage: test
script:
- npm install
- npm run test
fonctional-test:
stage: test
needs: [unit-test]
script:
- npm install
- npm run test:e2e
Grâce au cache on gagne 10s sur le temps de pipeline, c’est énorme pour un si petit projet !
On peut refactoriser également en transférantnpm install
pour qu’il soit, par défaut,
lancer à chaque fois qu’un job tourne.
Pour cela, on rajoute à default
:before_script:
- npm install
et on le retire des différents jobs.
En utilisant cache
et before_script
on s’assure que les dépendances sont installées
et pour tous les jobs qui vont tourner.
Concernant le problème de dépendances pour cypress, l’éditeur fourni une image docker vérifiée pour pouvoir faire tourner la CI/CD. Je vais donc remplacer l’image default par l’image cypress dans le job.
fonctional-test:
image: cypress/browsers:22.15.0
Malgré l’image j’ai l’erreur
The cypress npm package is installed, but the Cypress binary is missing.
Cette erreur est accompagnée d’une note sur le système de cache de Cypress. En fait par défaut lors de l’installation de Cypress, le cache est dans ~/.cache/Cypress, afin d’être partagé avec tous les projets. Par contre, c’est un chemin qui n’est pas accessible pour le cache de gitlab-ci (le chemin est en dehors du projet).
Pour avoir accès à cypress aussi bien en local que dans la CI :
- réinstaller cypress pour avoir le binaire dans le répertoire de travail
rm -fr ~/.config/Cypress ~/.cache/Cypress
export CYPRESS_CACHE_FOLDER='./cache/Cypress'
npm remove cypress
# cypress 15 bug, apparement un pb de Chromium et Gtk4...
npm install cypress@14.5
- paramétrer le cache cypress dans .gitlab-ci.yml
# ajout de cache
cache:
paths:
- node_modules/
- cache/Cypress
# env var pour le cache cypress
variables:
CYPRESS_CACHE_FOLDER: 'cache/Cypress'
Ainsi tous les emplacements de cache Cypress sont alignés, en local, en gitlab-ci-local et en CI cloud.
L’erreur restante est sur l’absence de serveur à tester (npm start
), qui n’est lui possible que si le projet est buildé.
Du coup, cela nous fait revoir un petit peu l’ordre des exécutions. Nos tests unitaires restent en premier
mais il faut intercaler une étape de build avant nos tests fonctionnels.
On va donc déclarer une nouvelle liste de stages et ajouter l’étape de build :
stages:
- test
- build
- fonctional
build:
stage: build
script:
- npm run build
artifacts:
paths:
- .next/
Ça y est notre pipeline tourne enfin ! Mais il tourne en double sur une branche dans une MR.
La condition précédente n’était pas bonne :
- if: $CI_COMMIT_BRANCH && $CI_PIPELINE_SOURCE == "merge_request_event"
when: never
D’après la documentation la combinaison des variables n’est même pas compatible :
CI_COMMIT_BRANCH | Pre-pipeline | The commit branch name. Available in branch pipelines, including pipelines for the default branch. |
Not available in merge request pipelines or tag pipelines. | ||
CI_PIPELINE_SOURCE | Pre-pipeline | How the pipeline was triggered. The value can be one of the pipeline sources : |
merge_request_event For pipelines created when a merge request is created or updated. | ||
push For pipelines triggered by a Git push event, including for branches and tags. |
Quand je pousse (git push
) ma branche de feature depuis l’ordinateur,
par principe gitlab-ci va essayer de lancer un pipeline sur toutes sortes de choses (tags, branch, commit, MR, etc).
Via workflow:rules:
on vient contrôler tout ça. Dès que Gitlab CI trouve un true alors il va lancer le pipeline.
Dans le cas de la MR, $CI_PIPELINE_SOURCE == "merge_request_event"
revient à true.
Dans le cas de la branche, $CI_COMMIT_BRANCH
revient à true.
On a donc deux pipelines qui tournent. Il faut repréciser les conditions.
- Si je pousse ma branche qui a une MR, seule la condition “existence d’une MR” doit être OK
- sinon, seule la condition “je suis sur une branche” doit être OK
On va donc garder $CI_PIPELINE_SOURCE == "merge_request_event"
pour un déclenchement sur MR.
Par contre côté branche, il nous faut une condition qui revient à true lorsque c’est une branche qui est associé à une MR.
On va alors ajouter when: never
pour que le pipeline ne soit pas lancé.
On pourra ajouter après qu’on veut quand même lancer si c’est une branche ‘simple’.
C’est la variable CI_OPEN_MERGE_REQUESTS
qui va nous servir. Cette variable contient les MR associées à la branche.
On va donc tester qu’on est sur une branche + qu’une MR est ouverte pour exclure ce pipeline.
La condition est
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
On poursuit en ajoutant l’exécution sur une branche.
Résultat
Voici le fichier au final :
# GLOBAL SETUP FOR PIPELINE
default:
image: node:22.15
before_script:
- npm install
cache:
# cache paths for node and cypress installs
paths:
- node_modules/
- cache/Cypress
variables:
# change cypress install cache for next execution
CYPRESS_CACHE_FOLDER: 'cache/Cypress'
workflow:
rules:
# on run toujours sur la branche par défaut
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# on run toujours sur un évenement de MR
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
# on run jamais si on est sur une branche qui a une MR ouverte
- if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS
when: never
# on run sur les branches qui n'ont pas de MR open
- if: $CI_COMMIT_BRANCH
stages:
- test
- build
- fonctional
# JOBS
unit-test:
stage: test
script:
- npm run test
build:
stage: build
script:
- npm run build
artifacts:
paths:
- .next/
fonctional-test:
image: cypress/browsers:22.15.0
stage: fonctional
dependencies: [build]
script:
- npm start &
- npm run test:e2e
Bilan
Voilà pour l’initialisation du CI dans notre projet. Le travail en mode itératif permet ici d’atteindre un fichier de CI qui tourne correctement dès lors que des modifications sont soumises.
Il va nous rester à ajouter les étapes de déploiement en staging, des tests E2E plus poussés et un déploiement en prod. Il faut également ajouter la génération de rapports pour les différents tests afin de visualiser et suivre les erreurs.