Jenkins – PACT

Jenkins est un logiciel open-source qui va nous permettre d’automatiser de nombreuses tâches, dont les tests. Il sert à mettre en place la philosophie de l’intégration continue.

Pour suivre ce tutorial, il faudra que les autres modules aient déjà mis sur le dépot une version du projet. Par exemple, pour Android, il faudrait avoir une version (simple) du projet qui marche.

Créer un environnement propre, minimal et indépendant – Docker

Afin de s’assurer que le code ne dépend pas de la machine sur laquelle il a été écrit, nous allons utiliser Docker, un logiciel permettant de mettre des applications dans des conteneurs. Docker va nous permettre de contrôler précisément notre environnement et d’être sûr de la configuration nécessaire pour exécuter le programme.

Pour commencer, vous avez déjà du apprendre Docker en suivant ce tutoriel: https://www.youtube.com/watch?v=fqMOX6JJhGo . Pour la suite, il va vous falloir savoir écrire un Dockerfile, en particulier:

  • choisir un image de base
  • créer de nouvelles variables d’environnement
  • exécuter des commandes pour créer des dossiers, mettre à jour le système, …
  • savoir lier des dossiers/fichiers extérieurs au Docker avec des dossiers/fichiers intérieurs au Docker

En pratique

Pour notre Docker, nous allons commencer avec une image d’Ubuntu. Vous pouvez trouver son nom exact sur le Hub de Docker.

Il nous faut ensuite installer tous les outils pour faire fonctionner notre programme. On commence pour cela par rechercher les mises à jour avec apt update, puis on les applique avec apt -y upgrade (-y signifie qu’on accepte tout par défaut) et enfin on installe de nouveaux packages avec apt -y install le_nom_des_packages_séparés_par_des_espaces. Vous devriez exécuter une commande comme:

apt update && apt -y upgrade && apt -y install curl

où le seul package installé est curl et && permet de chaîner les commandes. Pour Java, il se peut que vous rencontriez un problème lié à la version. Essayez de prendre Java8 au lieu de la dernière version.

Ensuite, vous pouvez installer d’autres logiciels reliés à votre sujet. Pour python, vous pouvez en profiter pour installer des bibliothèques avec pip3 install package_a_installer . Pour Android, il va vous falloir télécharger la dernière SDK.

Installer Android

Nous commençons par télécharger la dernière version de la SDK. Celle-ci peut être trouvée sur le site d’Android (https://developer.android.com/studio). Nous voulons la version “Command line tools only”. Ce qui nous intéresse plus précisément est le lien de téléchargement. Au moment où j’écris ces lignes, il s’agit de https://dl.google.com/android/repository/sdk-tools-linux-4333796.zip. Nous allons:

  • Définir l’emplacement de la SDK avec une variable d’environement: ANDROID_HOME=”/usr/local/android-sdk”
  • Définir des variables d’environnement contenant les versions d’Android qui nous intéressent: ANDROID_VERSION=28 et ANDROID_BUILD_TOOLS_VERSION=28.0.3
  • Créer les dossiers spécifiques à Android: /usr/local/android-sdk et .android.
  • Aller dans ANDROID_HOME
  • Télécharger la SDK avec curl -o sdk.zip $SDK_URL où SDK_URL est l’url mentionnée plus haut (il faut donc installer curl au début avec apt install)
  • Dézipper le ficher avec unzip sdk.zip (il faut donc installer unzip au début)
  • Supprimer le fichier sdk.zip
  • Accepter les licences avec la commande yes | $ANDROID_HOME/tools/bin/sdkmanager –licenses
  • Mettre à jour la SDK: $ANDROID_HOME/tools/bin/sdkmanager –update
  • Télécharger la version qui nous intéresse: $ANDROID_HOME/tools/bin/sdkmanager “build-tools;${ANDROID_BUILD_TOOLS_VERSION}” “platforms;android-${ANDROID_VERSION}” “platform-tools”.

Je vous laisse écrire tout cela (il vous faut quelques connaissances des commandes Unix, mais pas énormément). Chaînez vos commandes au maximum avec &&, cela réduit la taille de vos images Docker (chaque nouvelle ligne est considérée comme une nouvelle image que l’on écrit en mémoire pour les futurs builds).

Il est possible que vous ayez besoin d’une autre variable environnement: _JAVA_OPTIONS = ‘-Duser.home=.’ . Comme il n’y a pas vraiment d’utilisateur, le home pour Java n’est pas super bien défini. Ce problème permet de corriger des bugs liés à l’apparition d’un “?” dans les chemins d’accès à certains fichiers.

A vous de jouer: écrivez un Dockerfile pour votre projet. Si possible, testez le sur votre machine (installez une machine virtuelle Linux si vous êtes sur Windows et que vous avez des problèmes). Le Dockerfile doit être placé à la racine de votre projet sur Git.

Si jamais depuis votre Docker vous n’arrivez pas à télécharger des fichiers ou à mettre à jour votre système, il se peut qu’il y ait un problème de DNS. Une solution ici.

Si tout va bien, à la fin de cette étape, vous devez avoir un Dockerfile qui va créer une image sur lequel votre code va pouvoir s’exécuter.

Découvrir Jenkins

Créer un nouveau projet

Connectez-vous à Jenkins et retournez à la page d’accueil. Vous devriez voir tous les projets en cours ainsi qu’un menu sur la gauche.

Cliquez sur Open Blue Ocean. Il s’agit d’une interface un peu plus User-friendly de Jenkins. Depuis la nouvelle page, cliquez sur “New Pipeline” en haut à droite.

Puis suivez la procédure. Choisissez Git, puis ajoutez l’URL de votre dépôt git COMMENCANT PAR git@ (par exemple: git@gitlab.enst.fr:julien.romero/test-project.git). Vous trouverez cette URL en allant sur votre dépôt et en cliquant sur clone. Si vous avez bien fait cela, une clef publique devrait apparaître: il vous faut l’ajouter à votre compte GitLab. Finalement, cliquez sur “Create Pipeline“. Si tout ce passe bien, la pipeline sera créée et vous pourrez y accéder.

Créer une pipeline

Une fois créé, vous allez accéder à la page d’édition de la pipeline. Si ce n’est pas le cas, retournez à la page d’accueil de Open Blue Ocean, cliquez sur votre projet et sur “Create Pipeline“.

Vous obtenez une vue sur votre Pipeline qui est actuellement vide. Sur la droite, vous avez les options générale de votre pipeline. Un petit “+” au milieu de l’écran permet de rajouter de nouveaux stages à notre pipeline. Cliquez dessus et nommer le nouveau stage “Build”.

Pour l’instant, il n’y a pas de step. Nous allons en créer une très simple qui va afficher un hello world. Pour cela, cliquez sur “+ Add step“, “Print Message” et écrivez “Hello World!”. Quittez en cliquant sur la flèche en haut à gauche.

Finalement, cliquez sur Save. On va vous proposer d’écrire un message de commit (oui, Jenkins va faire des commits). Vous allez être redirigé vers une nouvelle fenêtre sur laquelle il n’y a qu’une seule ligne qui correspond au travail en cours. Rapidement, le statut va passer au vert: Bravo vous avez écrit votre première pipeline !

Cliquez sur la première ligne (la seule pour l’instant). Vous arrivez sur une page où tout est au vert.

Vous retrouvez votre pipeline. Vous pouvez cliquer sur chaque “stage” et chaque “step” pour voir les sorties qui sont générées. Quand un problème surviendra, des indicateurs passeront à l’orange ou au rouge dans les cas les plus graves.

Tout en haut, vous pouvez accéder aux changements dernièrement apportés, aux résultats des tests (quand il y en aura) et aux artifacts, qui sont les fichiers générés pendant l’exécution de la pipeline (notamment le log qui peut être très utile).

Vous pouvez aussi relancer la pipeline ou la modifier en cliquant sur le crayon.

Exécuter manuellement la pipeline

Normalement, Jenkins vérifira automatiquement si du nouveau code est ajouté au dépot et relancera la pipeline le cas échéant. Si cela n’est pas le cas, vous pouvez toujours exécuter la pipeline manuellement. Pour cela, deux solutions:

Rendez-vous sur la page d’accueil de Jenkins (pas celle d’Open Blue Ocean), choisissez votre projet, la branche qui vous intéresse puis cliquez dans le menu de gauche sur Build Now. La pipeline sera programmée et vous la verrez s’exécuter au centre de la page.

Sinon, dans le menu de Open Blue Ocean, selectionnez votre projet, Branches et run au bout de la ligne de la branche sur laquelle il faut exécuter la pipeline.

Se déplacer dans son dépot

Dans votre projet, vous serez surement amenés à changer le dossier courant. Comme vous le savez probablement, en Unix, cela se fait grâce à la commande cd nom_du_dossier. Il y a cependant une petite subtilité avec Jenkins: l’effet d’un cd n’est que local, c’est à dire qu’il ne s’applique qu’à la ligne en cours. Donc, si vous tapez une nouvelle commande, celle-ci sera exécutée depuis la racine de votre dépôt. Pour palier à ce problème vous pouvez utiliser le même opérateur que dans le Dockerfile: && et chaîner les commandes. Par exemple, vous pouvez faire cd module_android && ./gradlew build.

Comprendre les pipelines

Souvenez-vous, plus haut, nous avons vu que Jenkins allait faire des commits sur votre dépôt. Si vous retournez voir ce qui a changé, vous remarquerez un nouveau fichier dont le nom fait vaguement penser à un fichier rencontré plus haut: Jenkinsfile.

Le Jenkinsfile contient toutes les informations pour décrire notre pipeline. En ouvrant ce fichier, vous verrez que la syntaxe n’est pas très compliquée. La plupart des modifications que nous feront sur ce fichier peuvent se faire à travers Open Blue Ocean mais nous seront amenés à aussi le modifier manuellement. Plus d’information sur ce fichier directement sur le site de Jenkins: https://jenkins.io/doc/book/pipeline/jenkinsfile/.

Dans ce qui suit, il vous sera demandé d’appliquer les concepts proposés à votre projet. J’ai écrit quelques exemples qui peuvent vous aider:

Les Steps

Plus haut, nous avons ajouté une étape (step) à notre stage “Build”. Pour cela, nous avons sélectionné “+ Add step” puis “Print Message”. Il existe plein d’étapes différentes qui peuvent vous être utiles.

  • Print Message: nous l’avons déjà vu, affiche simplement un message.
  • Shell Script: sûrement l’étape la plus utile, elle permet d’exécuter des commandes bash.
  • Enforce Time limit: si vous voulez être sûr qu’une opération se déroule en moins d’un certain temps.
  • Archive JUnit-formatted test results: envoie les résultats des tests écrit au format de JUnit à Jenkins.
  • Record Jacoco coverage report: idem pour les rapports de couverture
  • Mail: envoie des mails
  • Deliver / Wait for interactive input: Attend une interaction avec l’utilisateur (pour confirmer une mise à jour en production en exemple).

Je vous laisse explorer petit à petit la liste et voir si certaines options peuvent voir être utiles.

Revenons à nos pipelines. En général, celle-ci sont constituées de trois grandes étapes. Mais avant d’aborder cela, voyons les paramètres généraux de notre pipeline et ce qu’est l’Agent.

Agent

Retournez à l’édition de votre pipeline. Quand aucun stage n’est sélectionné, le “Pipeline Settings” s’affiche à droite. Dans les paramètres, le premier champs permet de définir l’Agent. L’Agent est la “machine” qui va exécuter votre code. Par défaut, la valeur est “any”, ce qui signifie n’importe quelle machine disponible (donc pas forcément une qui vous convient). On peut aussi sélectionner une image docker (qui est sur le docker hub par exemple). Ce qui nous intéresse d’avantage est la possibilité d’utiliser un Dockerfile. Choisissez cette option et écrivez le nom de votre Dockerfile (ici, trivialement “Dockerfile” si vous n’en avez qu’un). Voilà, votre code sera exécuté sur votre conteneur Docker. Celui-ci ne sera recréé que si vous modifiez le Dockerfile.

Quelque chose devrait vous surprendre: “votre code sera exécuté”. A quel moment avez-vous mis le code dans le conteneur Docker ? En fait, Jenkins se charge de cela pour vous: dès qu’il lance le Docker, il clone le dépôt dedans et vous déplace à la racine de votre dépôt. Vous pouvez voir cela dans l’étape qui s’appelle “Check out from version control” après l’exécution d’une pipeline.

D’autres options sont disponibles pour l’agent. Il est notamment possible de ne choisir aucun agent avec “none”. Chaque stage devra alors lui-même définir son agent.

A vous de jouer: configurez votre agent.

Premier stage: Build

Ici nous compilons le code et créons des packages. A noter que ce n’est pas Jenkins qui compile le code mais des outils présents dans le système (sur notre Docker en l’occurrence).

Pour certains langages comme python, cette étape est inutile. Pour d’autres, comme Java et C, nous devons exécuter la bonne commande. Par exemple, en Java, on utilisera javac.

Cependant, il existe souvent des outils pour nous aider à construire nos projets compliqués (comme un projet Android) et qui nous évitent les complications liées à javac et l’installation des dépendances. Par exemple, pour Android, nous avons Gradle.

Gradle est le passage obligatoire pour tout ceux qui font de l’Android. En général, on n’utilise pas la commande gradle directement, mais un gradle wrapper appelé gradlew qu’il faut mettre sur votre dépôt. Normalement, ce fichier (ainsi que gradlew.bat) est déjà présent quand vous créez un projet Android. Sinon vous pouvez le créer avec la commande “gradle wrapper”. Compiler le projet est alors très simple: ./gradlew :build (si gradlew n’est pas exécutable vous pouvez aussi faire “sh gradlew :build”). Pour mettre à jour le gradle de votre projet, vous pouvez lancer la commande ./gradlew wrapper –gradle-version=6.0 (pour la version 6.0 ici).

A noter que si vous avez beaucoup de commandes à appeler, il serait peut-être bon de les regrouper dans un seul script ou dans un Makefile. Cette remarque vaut aussi pour les prochaines étapes.

A vous de jouer: créez cette première étape. Pour ceux qui font de l’Android, compilez le travail de vos camarades. Pour les autres, écrivez un simple message “Build stage”.

Deuxième stage: Test

C’est ici que nous allons exécuter tous les tests automatiques. Cette étape dépend beaucoup du langage. Avec Gradle, il suffit de faire “./gradlew test”. Avec Pytest, on utilisera “pytest –showlocals -v nom_de_votre_package –junit-xml test-reports/results.xml” (on utilise le format de junit pour faciliter pour plus tard).

Si un test vient à échouer, la pipeline s’arrête. Pour éviter cela vous pouvez ajouter un “|| exit 0” à la fin de la commande de test. Par exemple, “./gradlew test || echo 0”.

En général, quand on exécute des tests, on peut écrire les résultats dans différents formats. On peut ensuite transmettre ces fichiers à Jenkins pour afficher les résultats. Si le format est celui de JUnit (par défault avec gradle), on peut rajouter au Jenkinsfile l’étape “junit ‘app/build/test-results/test*/*xml’” (ceci est le chemin par défaut pour Gradle, adaptez le à votre application).

Attention: si vous avez des espaces dans les noms de vos dossiers et fichiers, cela peut créer des problèmes. Soit vous pouvez changer les noms (mettre un _ à la place des espaces), soit utiliser un “trick” qui consiste à remplacer les espaces par des “*” (c’est à dire n’importe quelle chaîne de caractères).

On peut aussi envoyer les résultats des tests dans le post. post est une section du Jenkinsfile qui est exécutée après toutes les étapes du stage. Il permet notamment de faire tourner certaines commandes même s’il y a eu une erreur. Cela est fait grâce au mot clef always. Par exemple, on peut imaginer une étape de test ressemblant à:

steps {
sh ‘make test-code-xml’
}
post {
always {
junit(allowEmptyResults: true, testResults: ‘test-reports/results.xml’)
}
}

On aimerait aussi dans cette étape de tests générer un rapport sur la couverture du code, c’est à dire à quel point le code est testé et quelles sont les parties qui ne le sont pas.

Pour Python, il existe une extension à pytest: pytest-coverage. La commande pour générer les tests de couverture est alors: “pytest nom_de_votre_package –showlocals -v –cov=nom_de_votre_package –cov-report=xml:reports/coverage.xml”

Cela va générer des rapports ayant le format de Cobertura. Pour envoyer le rapport à Jenkins, il faut modifier le Jenkinsfile pour y inclure:

post {
always {
step([$class: ‘CoberturaPublisher’,
autoUpdateHealth: false,
autoUpdateStability: false,
coberturaReportFile: ‘reports/coverage.xml’,
failNoReports: false,
failUnhealthy: false,
failUnstable: false,
maxNumberOfBuilds: 10,
onlyStable: false,
sourceEncoding: ‘ASCII’,
zoomCoverageChart: false])
}
}

Pour générer un rapport Cobertura avec Gradle, il faut exécuter la commande sh ‘./gradlew :cobertura’. Ceci n’est possible que si cobertura a été inclus au projet dans le build.gradle (c.f. https://github.com/Aunsiels/jenkins-gradle-example/blob/master/build.gradle par exemple).

Il existe une autre bibliothèque pour générer les rapport de couverture: Jacoco. Vous pouvez l’utiliser avec Gradle grâce à la commande “sh ‘./gradlew jacocoTestReport”. Encore une fois, il faut que Jacoco soit dans le build.gradle (par exemple: https://github.com/Aunsiels/jenkins-android-example/blob/master/build.gradle). Vérifiez quelle est la dernière version disponible sur https://github.com/arturdm/jacoco-android-gradle-plugin (ce n’est pas celle sur le dépôt donné en exemple). Il vous faut aussi ajouter le plugin jacoco-android dans le build.gradle du projet (un niveau au dessus, dans https://github.com/Aunsiels/jenkins-android-example/blob/master/app/build.gradle) Jacoco est très simple à inclure avec Jenkins: il suffit de rajouter l’étape “jacoco()”.

A votre tour. Faites en sorte que votre projet comporte au moins un test et généré un rapport de test et un rapport de couverture.

Pour les tests en Android, je vous invite à discuter avec le groupe responsable de cette partie. La page du site d’Android sur le testing explique rapidement quels sont les différents outils à utiliser. La base reste la même mais à cela viennent se greffer d’autres bibliothèques comme Robolectric et Expresso. Celles-ci sont à inclure dans le build.gradle au niveau de votre application (par exemple: https://github.com/Aunsiels/jenkins-android-example/blob/master/app/build.gradle). https://github.com/Aunsiels/jenkins-android-example donne un exemple de test. Vous pouvez trouver un exemple n’utilisant que JUnit ici et un exemple plus poussé . Si vous ne faites pas d’Android, le vocabulaire peut vous sembler ésotérique: discutez-en avec vos camarades.

Nous avons vu plus haut que les rapports de tests peuvent être visualisé dans Open Blue Ocean. Par contre, pour les rapports de couverture, il va vous falloir retourner sur l’accueil principal de Jenkins, cliquer sur votre projet et la branche qui vous intéresse. Sur la droite, un courbe résumant la couverture apparaît. Sur la gauche, en cliquant sur Coverage Report, vous aurez d’avantage d’informations, et notamment les lignes nous testées.

Deuxième stage bis: les tests statiques

On peut décomposer les tests en deux parties: les tests dynamiques, qui demandent que le code soit exécuté pour fonctionner, et les tests statiques qui, au contraire, se basent uniquement sur la représentation textuelle du code. Nous venons de voir la première catégorie, penchons nous sur la deuxième.

Il existe de nombreux type de vérifications statiques. La plus répandue est sûrement la vérification de style. Elle consiste à vérifier que le code est toujours écrit de la même façon: que les noms sont écrits pareils, que les indentations sont toujours les mêmes… Il est très important d’avoir cette cohérence au niveau de l’équipe pour éviter que le code deviennent illisible et incompréhensible.

En Python, vous pouvez utiliser pylint. En fournissant un fichier de configuration, on peut générer un rapport avec: “pylint –rcfile=pylint.cfg pyformlang > pylint.report || true”. Un exemple de fichier de configuration peut être trouvé . On peut aussi générer un rapport concernant pep8, le standard stylistique de Python. Pour cela, vous avez besoin de pycodestyle d’exécuter la commande: “pycodestyle pyformlang > pep8.report || true”. Envoyer les rapports se fait grâce à “recordIssues(enabledForFailure: true, tool: pyLint(pattern: ‘pylint.report’), sourceCodeEncoding: ‘UTF-8’)” et “recordIssues(enabledForFailure: true, tool: pep8(pattern: ‘pep8.report’), sourceCodeEncoding: ‘UTF-8’)” (Documentation pour recordIssues et les différentes options possible: https://jenkins.io/doc/pipeline/steps/warnings-ng/. Utile si vous utilisez un autre langage comme C++ avec son propre linter).

Avec Gradle, vous avez la commande “sh ‘./gradlew checkstyle || exit 0’ ou “sh ‘./gradlew :check’”. Jetez un oeil aux projets en exemple pour voir la difference. En particulier, il faut importer les bonnes bibliothèques dans le build.gradle. Dans jenkins-android-example, les outils sont mis dans le dossier tools et sont importés dans app/build.gradle. On peut ensuite envoyer les résultats avec “recordIssues enabledForFailure: true, tool: checkStyle(pattern: ‘build/reports/checkstyle/*.xml’), sourceCodeEncoding: ‘UTF-8′”.

(la commande ./gradlew checkstyle –stacktrace permet de voir les erreurs éventuelles qui peuvent apparaître au cours de la vérification de style. Le début du dernier paragraphe contiendra les informations utiles)

Il existe d’autres types de tests statiques qui ont pour but d’essayer de réduire le nombre d’erreurs et d’éviter quelques mauvaises pratiques. Pour Gradle, on citera CPD pour trouver du code dupliqué, PMD qui vérifie la qualité du code et SpotBugs qui essaie de trouver certains bugs en avance. Leur utilisation est très similaire à ce qui précède et vous trouverez comment les utiliser dans les dépôts donnés en exemple plus haut.

A vous de jouer: créez quelques vérifications statiques à mettre en place dans vos projets.

Troisième stage: Deploy

La dernière étape consiste à modifier des systèmes extérieurs. Par exemple, on pourra mettre à jour la documentation sur un site ou alors mettre une nouvelle version du projet en production ou sur des dépôts.

Cette étape dépend beaucoup des projets, nous ne rentrerons pas dans les détails.

A vous de jouer: que pourrez faire votre projet à cette étape ? Détaillez-le dans votre rapport et si vous le sentez, créez une nouvelle étape dans votre pipeline.

Déclencher automatiquement la pipeline

Il est possible de déclencher automatiquement la pipeline grâce aux webhooks. Pour cela, il faut dire à Gitlab de nous envoyer une notification à chaque push par exemple. Rendez-vous sur la page Gitlab de votre projet, puis dans Settings (à gauche), Integrations. Il vous sera demandé de renseigner l’URL de votre hook en haut de la page. Vous devez mettre https://jenkins.r2.enst.fr/project/<nom_de_votre_projet>/. Par exemple, https://jenkins.r2.enst.fr/project/pact42/. Vous pouvez ensuite selectionner sur quels événements vous voulez déclencher le hook. Pour commencer, selectionnez Push events.

Faites une modification sur votre dépot et voyez si la pipeline se déclenche automatiquement.

Et après?

Votre projet va être en continuelle évolution: il va donc falloir que vos outils sachent s’adapter. Il y a de nombreuses choses que peut permettre Jenkins: envoyer des mails en cas d’erreur majeure, accepter des merges requests, générer de la documentation, créer des boutons de statut (regardez en haut du Readme de https://github.com/Aunsiels/pyformlang),…

A vous de jouer: surprenez moi.

A vous de jouer: complétez votre rapport avec le travail exécuté et quels ont été les choix pris au cours du PAN2 concernant les tests et l’intégration.