Aller au contenu

Intégration Continue

Dans cette unité, nous discutons des concepts sous-jacents des pratiques d'intégration continue côté serveur à l'exemple de GitLab. Vous apprendrez comment créer un environnement de conteneur fiable pour un projet maven et comment intégrer des vérifications automatiques de qualité à chaque commit poussé.

Résumé de la leçon

Les runners GitLab vous permettent d'exécuter systématiquement l'intégralité du cycle de vie de la construction Maven à chaque modification du code. Un retour direct sur les vérifications de qualité effectuées à chaque commit est un concept essentiel pour garantir une base de code fiable et stable.

Définition et motivation de l'IC

  • Intégration Continue (ou "IC" en abrégé) vise à soutenir le succès du projet en permettant aux développeurs d'envoyer régulièrement leur code vers un dépôt central et d'effectuer des constructions et des tests automatisés.
  • Dans les conférences précédentes, vous avez déjà vu deux éléments de base pour soutenir ces objectifs :
    • Systèmes de construction, par exemple maven : Hautement configurable, utilise une représentation textuelle pour la définition explicite du processus de construction et des exigences de qualité du code.
    • Systèmes de gestion de version, par exemple git : Synchronisation du travail effectué par les développeurs, utilisant des machines séparées.

Besoin de configurations IC

  • Les deux composants fournissent un élément fonctionnel essentiel pour l'IC, mais :
    • Il n'y a aucune garantie que les développeurs utilisent de manière fiable le système de construction avant de pousser leur code.
    • Il n'y a pas de retour visuel direct sur les résultats du système de construction lors de la combinaison des travaux.
  • L'IC est particulièrement pertinente lors de la combinaison des travaux, c'est-à-dire lors de la soumission de commits ou de la fusion de branches. En essence, vous voulez...
    • forcer les développeurs à utiliser des branches pour les fonctionnalités et interdire de travailler directement sur main.
    • vous assurer que le travail sur le point d'être fusionné dans main ne casse rien qui fonctionnait précédemment.
  • Sans une configuration IC, vous n'avez aucune garantie sur l'état de votre branche main, dans le pire des cas :
    • Votre soumission de TP perd des points, car vous n'avez pas soumis quelque chose de bien testé et fonctionnel.
    • Vous présentez les progrès récents à un client, mais la version présentée a en réalité moins de fonctionnalités fonctionnelles car la dernière poussée rapide a tout cassé.
    • Vous envoyez un patch de sécurité, et maintenant des millions de PC sont pris dans une boucle de démarrage infinie.

Les configurations IC sont un mécanisme de sécurité

Une bonne configuration IC garantit une chose : une branche principale protégée qui ne peut être corrompue, ni par une erreur honnête ni par une négligence flagrante. Quoi qu'il arrive, votre projet avance toujours, jamais en arrière.

Dans le reste de cette conférence, nous verrons comment configurer divers mécanismes de protection à l'aide de GitLab.

Exemple de vérifications côté serveur

  • La confiance, c'est bien, le contrôle, c'est mieux : La seule manière de savoir de manière fiable si un commit est " bon", est de l'évaluer côté serveur.
  • Quelles vérifications doivent être effectuées côté serveur pour évaluer si c'est "bon" ?
    • Le logiciel compile-t-il réellement ?
    • Est-il documenté ?
    • Respecte-t-il les règles de checkstyle ?
    • Est-il testé, la couverture est-elle suffisante ?
    • Y a-t-il des éléments superflus dans le dépôt, comme des fichiers class ?
    • ...
  • Comme nous le verrons bientôt, cela est tout à fait possible avec GitLab :

    • Une fois configuré correctement, GitLab vous fournira des informations détaillées sur diverses vérifications de qualité pour chaque commit :
    • En fonction des résultats des tests, nous obtenons directement une idée de la qualité d'un commit :
      • Toutes les vérifications réussies
      • Certaines vérifications avec avertissement
      • Au moins une vérification échouée
    • Illustration :

Remarque : Avec "vérifications", nous ne faisons pas seulement référence aux tests unitaires, mais à toutes les vérifications possibles de qualité du code (y compris les tests unitaires).

Fusionner, ne pas pousser

  • Une protection clé pour tout dépôt est d'interdire les pushes directs sur main.
    • Voir : Paramètres -> Dépôt -> Branches -> Branches protégées
    • Au début du semestre, main était protégé (vous ne pouviez pas pousser)
    • Maintenant, à la fin, nous reviendrons à cette habitude (car vous savez maintenant comment créer des branches et des fusions)
  • Toute fonctionnalité ajoutée doit d'abord être poussée vers une branche.
    • Si quelqu'un tente de pousser directement sur main :
      $ git commit -m "reckless direct push to main"
      ...
      $ git push
      ...
      remote: GitLab: You are not allowed to push code to protected branches on this project.
      error: failed to push some refs
      
  • Au lieu de fusionner le code localement puis de pousser, vous effectuerez la fusion côté serveur, en utilisant des demandes de fusion.

Demandes de fusion

  • Les demandes de fusion se traduisent par :
    "J'ai créé quelque chose d'utile sur une branche, veuillez l'ajouter à main".
    • Souvent, la personne qui effectue la fusion n'est pas le développeur.
    • Pour initier le processus, le développeur crée une nouvelle demande de fusion. (interface web de GitLab, grande bannière)
  • Idéalement, la demande de fusion elle-même offre toutes les informations des vérifications côté serveur d'un seul coup d'œil

Le véritable mérite de l'IC

Peu importe si le code fonctionne sur la machine du développeur, ce qui compte, c'est si le code fonctionne pour le client. Les vérifications côté serveur apportent une tranquillité d'esprit à ceux qui doivent décider de la fusion d'une demande.

Conteneurs en tant que fondement de l'IC

  • L'idée des vérifications côté serveur est séduisante.
  • Mais tester le logiciel signifie également que le serveur doit être capable de compiler et d'exécuter le logiciel.
    • Rappel : Les tests dynamiques nécessitent l'exécution du code.
  • Nous ne pouvons pas exécuter de tests côté serveur, à moins que le serveur ne dispose de :
    • Le code source
    • Un système d'exploitation, nous permettant d'exécuter le code
    • Tous les SDK nécessaires au programme : Maven, compilateur Java, JVM
  • GitLab a naturellement le code source, mais absolument pas l'environnement.

Comment fournir un environnement

  • L'approche classique d'installation d'un environnement de développement logiciel n'est pas viable.
  • Vous ne pouvez pas vous rendre sur le serveur exécutant GitLab et commencer à installer Maven, le compilateur Java, la JVM vous-même.
    • Chronophage
    • Nécessite des droits root
    • Non reproductible de manière fiable
  • Dans l'approche classique et native, vous avez une pile de trois composants :
    • Logiciel
    • Bibliothèques nécessaires au logiciel
    • Système d'exploitation fournissant le noyau pour exécuter les bibliothèques et le logiciel.
  • Il existe deux façons d'interférer avec cette pile pour obtenir le logiciel, sans installer manuellement les prérequis. Les deux modifient la pile native ci-dessus :

Machines virtuelles

  • Les machines virtuelles sont un instantané d'un système d'exploitation entier, c'est-à-dire :
    • Un noyau de système d'exploitation (par exemple Windows)
    • Toutes les bibliothèques
    • Le logiciel proprement dit
  • Livrer une machine virtuelle est fiable, cependant :
    • Diminution des performances : Un Hyperviseur intermédiaire est nécessaire pour simuler un système d'exploitation entier au-dessus d'un noyau d'OS existant.
    • Volumineux : Un système d'exploitation entier est expédié avec les quelques composants logiciels réellement nécessaires.

La JVM n'est pas ce genre de machine virtuelle.

La JVM ne doit pas être confondue avec les machines virtuelles de systèmes d'exploitation. La JVM n'interprète que le bytecode Java. Les machines virtuelles de systèmes d'exploitation peuvent exécuter n'importe quel bytecode, c'est-à-dire exécuter n'importe quel logiciel que la machine simulée pourrait exécuter (comme, par exemple, une JVM).

Conteneurs Docker

  • Les conteneurs Docker sont une réponse aux machines virtuelles de systèmes d'exploitation.
  • Les conteneurs Docker fournissent uniquement les bibliothèques et logiciels nécessaires, et réutilisent le noyau du système d'exploitation hôte existant.
  • Habituellement, ce n'est même pas le conteneur lui-même qui est livré, mais uniquement des instructions sur la façon de le créer étape par étape (appelées aussi Images).
  • Comparé aux VM, les images Docker sont :
    • De petite taille.
  • Comparé aux VM, les conteneurs Docker sont :
    • Presque aussi performants que le système hôte (par exemple, le serveur GitLab sur lequel ils s'exécutent).

Contexte GitLab

  • Les images Docker sont comme des plans, indiquant à une machine ce qui est exactement nécessaire pour travailler avec un projet.
  • Un serveur, par exemple GitLab, peut utiliser une telle image pour construire un environnement fiable, par exemple pour obtenir un compilateur Java, JVM, maven, etc...
    • Pointer vers la bonne image est une seule ligne de code
    • Une fois l'image fournie, il n'est pas nécessaire d'installer manuellement le compilateur Java, la JVM, maven, etc...

Pourquoi est-ce utile pour GitLab ?

  • Nous pouvons dire à GitLab d'utiliser une image menant à un environnement fiable.
  • En utilisant cet environnement, nous pouvons évaluer notre code source côté serveur.
  • Nous pouvons évaluer de manière excessive et rapide.

GitLab CI

Presque toute la configuration GitLab CI est effectuée avec un seul fichier : gitlab-ci.yml

  • Il suffit qu'il existe, dès qu'il est dans votre dépôt de projet et poussé, GitLab l'utilisera.
  • Quoi que nous spécifions dans ce fichier, GitLab essaiera d'évaluer le code source à chaque commit, en fonction des instructions qu'il contient.

La première chose que nous ajoutons au fichier est la référence à l'image Docker à utiliser.

  • Dans le contexte d'INF2050, nous utiliserons toujours la ligne : image: maven:3.9.8-amazoncorretto-21
  • Cette image mène à un environnement de conteneur avec :
    • Java 21 (JVM + Compilateur)
    • Maven
  • Comme le conteneur s'exécute sur un serveur Linux, nous avons également accès à toutes les commandes Linux standard !

Syntaxe YAML

  • Ensuite, nous allons approfondir la manière exacte de définir le processus CI pour GitLab, en utilisant le fichier : gitlab-ci.yml
  • Pour que GitLab comprenne les choses, nous devons nous en tenir aux mots-clés exacts demandés.
    • L'extension du fichier est yml, ce qui signifie YAML, l'acronyme de Yet Another Markup L anguage.
    • Les fichiers YAML, similaires aux fichiers XML ou JSON, doivent respecter le formatage et les mots-clés corrects.
  • Nous avons déjà vu comment spécifier l'image CI à utiliser.
  • Ensuite, nous allons examiner comment spécifier les composants principaux d'une configuration CI, en utilisant la mise en forme et les mots-clés de GitLab.
    En détail, nous verrons :
    • Comment définir des étapes personnalisées (l'ordre des choses à faire)
    • Comment définir les jobs (les choses exactes à faire)
Quelle est la relation entre YAML et GitLab ?

GitLab utilise la notation YAML pour configurer le comportement CI. Il existe de nombreux autres fichiers YAML, utilisant la même syntaxe, mais pas nécessairement les mêmes mots-clés.

Notation YAML générale

Tous les fichiers YAML sont des dictionnaires et utilisent une notation clé/valeur.

  • (Optionnel) marqueur de début de document : ---
  • (Optionnel) marqueur de fin de document : ...
  • Clé de dictionnaire : foo:
  • Liste des valeurs d'éléments : -
    • Forme abrégée, uniquement les valeurs : ['valeur1', 'valeur2', '...']
    • Forme abrégée, dictionnaires : { nom: Max, poste: Professeur, âge: 34 }

Exemple :

---
# After file start marker, enumerate all key/value pairs.
university: UQAM
course: INF2050
students: 164
# Next an entry with mulyiple values for same key
staff:
  - Max
  - Ahmed
  - André-Pierre
  - Felix
  - George
prerequisites: [ 'INF1070', 'INF1120' ]
...

YAML est un super-ensemble de JSON

YAML est un super-ensemble de JSON avec un accent sur la lisibilité humaine. Chaque fichier JSON est également un fichier YAML valide, mais l'inverse n'est pas vrai.

Définir les étapes

Semblable à Maven, la configuration CI de GitLab prévoit un certain ordre des choses, commun à la plupart des projets logiciels :

  1. préparation
  2. construction du logiciel
  3. tests
  4. déploiement
  5. post-achèvement

Ce sont les étapes :

  • Chaque fois que nous définissons un nouveau job, nous devons indiquer explicitement à quelle phase il doit avoir lieu.

    • Pour cela, nous utilisons les mots-clés ci-dessous :
      .pre
      build
      test
      deploy
      .post
      
  • Si nous voulons ajouter des étapes supplémentaires, nous pouvons le faire en les listant dans le fichier .gitlab-ci.yml :

    • Notez cependant que toutes les étapes par défaut sont annulées dès que nous définissons notre propre ensemble.
      # Definition of custom stages, to provide an implicit job order.
      stages:
        - lint
        - build
        - test
        - deploy
      
  • Les jobs individuels (que nous définirons ensuite) appartiendront chacun à une seule phase.

    • Étant donné que les phases définissent un ordre, nous faisons également référence à l'exécution CI en tant que " pipeline CI".

Définir les jobs

  • Une définition de job est en essence une ou plusieurs commandes à exécuter.
    • Nous pouvons utiliser toutes les commandes fournies par le conteneur
    • Étant donné que le conteneur est basé sur une image pour Java / Maven, nous avons les commandes java, javac, et mvn.
    • De plus, nous pouvons utiliser n'importe quelle commande Linux standard.
  • En syntaxe YAML, nous devons décrire :
    1. Le nom du job.
    2. L'étape à laquelle exécuter le job.
    3. Les commandes à exécuter par le job.
  • Exemple :
    sample-job:
       stage: build
       script:
         - echo "Using a linux command to log something to console"
    
    time-consuming-job:
      stage: test
      script:
        - sleep 20
        - echo "Hello, $GITLAB_USER_LOGIN!"
    

Comportement des étapes de Runner

La définition des étapes semble quelque peu artificielle, pourquoi aurions-nous besoin de définir des étapes, et ne pas simplement définir tous les jobs dans une séquence ?

  • Il y a un intérêt naturel à ce que l'exécution CI ne prenne pas trop de temps.
  • Si tous les jobs sont exécutés séquentiellement, nous ne faisons peut-être pas le meilleur usage des ressources serveur.
  • Mais tous les jobs ne doivent pas être exécutés en parallèle.
    Exemples :
    • Tester le Checkstyle et Javadoc en parallèle est acceptable. Il n'y a pas de dépendance.
    • Construire un fichier JAR en parallèle à la compilation des fichiers n'est pas acceptable. Il y a une dépendance.

Les étapes permettent une meilleure consommation des ressources :

  • Tous les jobs au sein de la même étape sont exécutés en parallèle.
  • Tous les jobs des étapes suivantes sont exécutés séquentiellement. Les jobs ultérieurs sont annulés si au moins un job d'une étape précédente échoue.

Visualisation de trois phases, avec des jobs parallèles et séquentiels :

Source : doc.gitlab.com.

Configuration CI avec Maven

L'exemple précédent était assez inutile :

  • Nous ne voulons pas simplement appeler des commandes Linux...
  • Nous voulons appeler des commandes qui évaluent notre code source !

Cas le plus simple

Dans le cas le plus simple, nous utilisons certaines des commandes Linux standard pour vérifier si le dépôt est exempt de code compilé :

check-clutter-job:
  stage: lint
  script:
    - CLUTTER=$(find . -name \*.class)
    - if [[ ! -z $CLUTTER ]]; then exit 1; else exit 0; fi

Explication :

  • La première ligne du script stocke une liste de tous les fichiers class dans une variable.
  • La deuxième ligne du script vérifie si la variable est vide :
    • Si la variable n'est pas vide, elle retourne 1 (échec du job).
    • Si la variable est vide, elle retourne 0 (réussite du job).

Compilation atomique avec Maven

Cependant, nous n'utilisons toujours pas le conteneur !

  • Nous avons sélectionné l'image, spécifiquement parce que le conteneur résultant nous permet d'utiliser Maven.
  • Nous avons déjà un fichier pom.xml de configuration dans notre projet, avec de nombreuses vérifications de qualité, utilisons-le !
image: maven:3.9.8-amazoncorretto-21

maven-job:
  stage: build
  script: "mvn clean package -B"
  • Dans ce cas, le pipeline CI est atomique. Il a juste un seul job de pipeline, effectuant tout le travail lourd.
Peut-on utiliser l'image ci-dessus pour un projet Python ?

Non. L'image doit correspondre aux exigences du projet et une image Java/Maven ne peut pas être utilisée pour créer un conteneur pour le traitement Python. Un Runner ne peut pas fonctionner si tous les besoins ne sont pas satisfaits par le conteneur.

Phases Maven en tant qu'étapes

  • Utiliser un pipeline avec juste une seule commande fonctionne comme vérification de qualité du code.

    • Cependant, si notre compilation échoue, nous ne voyons pas immédiatement quel est le problème.
    • Notamment pour les demandes de fusion, cela est peu pratique.
    • Il serait bien mieux d'avoir des étapes de pipeline individuelles pour les différentes phases de Maven.
  • La première étape consisterait à définir toutes les phases de Maven en tant qu'étapes :

stages:
  - validate
  - compile
  - test
  - package
  - verify
  - javadoc
  • Ensuite, nous pouvons définir des jobs Maven individuels pour chaque étape du pipeline :
validate-job:
  stage: validate
  script: "mvn clean validate -B"

compile-job:
  stage: compile
  script: "mvn clean compile -B"

test-job:
  stage: test
  script: "mvn clean test -B"

package-job:
  stage: package
  script: "mvn clean package -B"

Redondance du cycle de vie

  • Par défaut, Maven exécutera l'ensemble du cycle de vie jusqu'à la phase spécifiée, pour chaque commande mvn individuelle dans un runner.
    • C'est assez gaspilleur, par exemple nous n'avons besoin de faire un lint qu'une seule fois, pour garantir que le code est correctement formaté.
    • Il serait préférable d'exécuter les phases individuellement.
  • Malheureusement, Maven ne permet pas l'exécution d'une seule phase.
    • Mais il y a une astuce : nous pouvons transférer les artefacts de build et exécuter des plugins individuels (pas des phases) sur eux.
    • Nous pratiquerons cette technique pour l'optimisation des ressources lors de la prochaine session de laboratoire.

Les Runners sont isolés

Attention cependant lorsque vous construisez un pipeline séquentiel pour les phases Maven individuelles. Les artefacts ajoutés à target ne sont pas automatiquement transférés d'un runner à l'autre. Pour que les runners suivants fonctionnent, nous devons définir manuellement quels artefacts, provenant de quel runner précédent, réutiliser.

Utilisation des artefacts de build

Chaque runner vit dans son propre environnement, que vous pouvez considérer comme un bac à sable.

  • Les fichiers produits par un runner ne sont pas immédiatement visibles pour un autre runner. C'est comme s'ils fonctionnaient sur deux machines séparées.
  • Mais parfois, vous voulez conserver quelque chose généré par un runner, afin de garder des informations sur un commit.
  • Exemples :
    • Le plugin surefire produit un rapport de test dans target.
    • Le plugin jacoco produit un rapport de couverture dans target.
    • Le plugin javadoc produit un site Web navigable du code source dans target.
  • Tous ces fichiers générés sont perdus dès que le runner qui les a créés retourne en sommeil.

Heureusement, il existe un mot-clé de configuration pour indiquer à GitLab d'extraire les fichiers d'un runner, avant qu'il ne soit dissous :

job-name:
  script:
    # Run some command that produces files, e.g. maven calling javadoc plugin.
    - command-that-produces-files
  # Define which files / folders created by this runner should survive the build process.
  artifacts:
    paths:
      - folder-to-survive-runner

Ensuite, nous allons examiner comment les artefacts issus de différentes étapes de build sont utilisés pour obtenir un retour supplémentaire sur la qualité de votre commit.

Exemple de test

  • Habituellement, il ne suffit pas de savoir qu'un test a échoué, vous voulez savoir exactement quel test a échoué.
  • GitLab fournit une interface dédiée pour afficher cette information, mais vous devez la fournir.
    • Cela se fait par le biais d'un artefact de build !
    • Vous pouvez simplement extraire le rapport de test créé par Maven (dans votre répertoire target), et GitLab ajoutera automatiquement un nouvel onglet à chaque commit avec le rapport de test détaillé :

  • Si vous avez également un plugin de couverture configuré, par exemple jacoco, vous pouvez de même extraire les rapports de couverture pour des informations supplémentaires.
test-job:
  stage: test
  script: "mvn clean test -B"
  artifacts:
    when: always
    reports:
      junit:
        - target/surefire-reports/TEST-*.xml

Exemple JavaDoc

Un deuxième type d'artefact que vous devriez extraire est la documentation générée.

  • Le plugin JavaDoc crée déjà une documentation décente et lisible par l'homme dans le dossier target.
    • De même, GitLab dispose d'une fonctionnalité intégrée pour héberger les fichiers du dépôt sur un serveur web, pour un accès pratique via le navigateur.
    • Cependant, à moins que vous ne soyez un codeur de page web oldschool, votre site est probablement généré, et non écrit à la main :
      • Fichiers HTML.
      • Fichiers CSS.
      • Fichiers JavaScript.
  • Mais attendez ! Nous ne voulons pas de fichiers générés dans notre dépôt !!
    • Utilisons un job de pipeline CI pour générer la documentation côté serveur, sauvegarder l'artefact, et seulement après héberger la documentation sur internet !
    • La première chose que nous devons faire est de configurer le job CI pour déplacer toute documentation générée dans un dossier nommé public.
pages:
  script:
    - mvn javadoc:aggregate
    - mkdir public
    - cp -r target/site/docs/apidocs/* public
  # Define which files / folders created by this runner should survive the build process.
  artifacts:
    paths:
      - public
  • Une fois le runner configuré, vous devez toujours indiquer à GitLab d'héberger réellement le javadoc sur son serveur de fichiers :

  • Accédez à votre projet GitLab sur l'interface web de GitLab

  • Dans la barre latérale gauche, sélectionnez Déployer -> Pages
  • Facultatif : Désélectionnez la case de l'URL personnalisée
  • Accédez à la page web de votre projet.

Seulement sur main

  • Un inconvénient avec la documentation, c'est qu'elle n'est pertinente que pour les logiciels publiés, c'est-à-dire que personne ne se soucie de la documentation pour une fonctionnalité encore en développement sur une branche secrète.
  • Cela se traduit par : nous ne voulons déployer la documentation que pour les commits sur la branche main.
  • Heureusement, il existe un mot-clé supplémentaire pour restreindre les runners à des branches spécifiques :
pages:
  script:
    - ...
  # Only keyword allows restricting for which git branch the job is applied.
  only:
    - main

MISC

Ce contenu de cours vous est fourni, avec l'aide d'un pipeline GitLab CI !

  • J'écris les sources en format MarkDown
  • Je pousse les sources dans un dépôt GitLab
  • GitLab exécute un pipeline CI qui :
    1. Crée un conteneur avec le support Python
    2. Utilise un programme Python pour traduire :
      • Les Markdowns en pages HTML navigables et indexées
      • Les Mermaids en SVG
      • HTML et SVG en PDF
    3. Stocke les artefacts HTML/SVG/PDF produits dans le dossier public
  • GitLab sert les sites web du cours sur son serveur de fichiers, à https://inf2050.uqam.ca/fr/

Chaque fois que je modifie un simple typographique, l'ensemble du pipeline CI est réexécuté, et les pages web se mettent à jour automatiquement à chaque push. :)

Littérature

Inspiration et lectures supplémentaires pour les esprits curieux :