Introduction au CI/CD

ENSG Novembre 2024

Comment utiliser cette présentation ?

  • Pour naviguer, utilisez les flèches en bas à droite (ou celles de votre clavier)
    • Gauche/Droite: changer de chapitre
    • Haut/Bas: naviguer dans un chapitre
  • Pour avoir une vue globale : utilisez la touche “o” (pour “Overview”)
  • Pour voir les notes de l’auteur : utilisez la touche “s” (pour “Speaker notes”)

Bonjour !

Julien Levesy

  • Senior Software Engineer @Voi 🛴
  • Me contacter

Et vous?

A propos du cours

  • Alternance de théorie et de pratique pour être le plus interactif possible
  • Reproductible à la maison, pensé dans le contexte du “Covid à la maison”
  • Contenu entièrement libre et open-source.
    • Le code source est visible ici
  • Le cours va donner lieu a un projet en guise d’evaluation.
    • Les details vous seront donnés en fin de cours.

Avant de Commencer…

  • Comment souhaitez vous gérer les pauses midi?

La Petite histoire du Genie Logiciel

Avant : le cycle en V

Pourquoi questionner le Cycle en V?

  • 🤔 On spécifie et l’on engage un volume conséquent de travail sur de simples hypothèses
    • … et si les hypothèses sont fausses?
    • … et si les besoins changent?
  • ⏲️ Cycle très long (6 mois? 1 an?)
    • Aucune validation à court terme
    • Coût de l’erreur décuplé 💸

Comment éviter ça?

  • Valider les hypothèses au plus tôt, et étendre petit à petit le périmètre fonctionnel.
    • Réduire le périmètre fonctionnel au minimum.
    • Confronter le logiciel au plus tôt aux utilisateurs.
    • Refaire des hypothèses basées sur ce que l’on à appris, et recommencer!
  • Faire du changement la norme
    • Votre logiciel va changer en continu

La clé : gérer le changement!

  • Le changement ne doit pas être un événement, ça doit être la norme.
  • Notre objectif : minimiser le coût du changement.
  • Faire en sorte que:
    • Changer quelque chose soit facile
    • Changer quelque chose soit rapide
    • Changer quelque chose ne casse pas tout

Le but de ce cours est de vous former aux pratiques d’Integration Continue (CI) et de Livraison Continue (CD) visant a garantir la robustesse et la pérénité d’un produit logiciel face au changement.

Préparer votre environnement de travail

Outils Nécessaires 🛠

  • Un navigateur récent (et décent)
  • Un compte sur GitHub
  • On va vous demander de travailler en binôme, commencez à réfléchir avec qui vous souhaitez travailler !
  • Enregistrez vous par ici !

GitPod

GitPod.io : Environnement de développement dans le ☁️ “nuage”

  • But: reproductible
  • Puissance de calcul sur un serveur distant
  • Éditeur de code VSCode dans le navigateur
  • Gratuit pour 10h par mois (⚠️ jusqu’en Avril 2025…)
  • Open-Source : vous pouvez l’héberger chez vous

Démarrer avec GitPod (1/2)🚀

  1. Rendez vous sur https://gitpod.io/login"
  2. Authentifiez vous en utilisant votre compte GitHub
  3. Continuez avec 10h
  4. Selectionnez VS Code Browser comme éditeur
  5. Repondez comme vous le souhaitez aux questions
  6. Vous devriez atterir sur https://gitpod.io/workspaces


⚠️ Ne vous authentifiez pas sur Gitpod Flex (https://app.gitpod.io) ⚠️

Démarrer avec GitPod (2/2)🚀

  • Pour les besoins de ce cours, vous devez autoriser GitPod à pouvoir effectuer certaines modification dans vos dépôts GitHub
  • Rendez-vous sur La page des intégrations avvec GitPod
  • Éditez les permissions de la ligne “GitHub” (les 3 petits points à droits) et sélectionnez uniquement :
    • user:email
    • public_repo
    • workflow

💡Mais qu’est ce qu’un Workspace?

  • C’est un ordinateur distant sur lequel on se connecte via le navigateur
  • ⚠ Faites attention à réutiliser le même workspace tout au long de ce cours⚠
  • ⚠️ 10h c’est pas beaucoup 😭 En fait c’est 50h par défaut
  • ➡️ Suspendez le workspace GitPod des que vous ne l’utilisez pas
    • Bouton bleu en bas a gauche, sélectionnez “Suspend”

Démarrer votre premier Workspace

Cliquez sur le bouton ci-dessous pour démarrer un environnement GitPod personnalisé:

Après quelques secondes (minutes?), vous avez accès à l’environnement:

  • Gauche: navigateur de fichiers (“Workspace”)
  • Haut: éditeur de texte (“Get Started”)
  • Bas: Terminal interactif
  • À droite en bas: plein de popups à ignorer

Checkpoint 🎯

  • Vous devriez pouvoir taper la commande whoami dans le terminal de GitPod:

    • Retour attendu: gitpod
  • Vous devriez pouvoir fermer le fichier “Get Started”…

    • … et ouvrir le fichier .gitpod.yml

On peut commencer !

Guide de survie en ligne de commande sous Linux

Remise à niveau / Rappels

CLI?

  • 🇬🇧 CLI == “Command Line Interface”
  • 🇫🇷 “Interface de Ligne de Commande”

C’est un programme qui accepte une commande en entrée et retourne un resultat.

Nous allons utiliser bash

Anatomie d’une commande

ls --color=always -l /bin
  • Séparateur : l’espace
  • Premier élément (ls) : c’est la commande
  • Les éléments commençant par un tiret - sont des “options” et/ou drapeaux (“flags”)
    • “Option” == “Optionnel”
  • Les autres éléments sont des arguments (/bin)
    • Nécessaire (par opposition)

Manuel des commandes

man <commande> # Commande 'man' avec comme argument le nom de ladite commande
  • Navigation avec les flèches haut et bas
  • Tapez / puis une chaîne de texte pour chercher
  • Touche n pour sauter d’occurrence en occurrence
  • Touche q pour quitter

🎓 Essayez avec ls, cherchez le mot color

  • 💡 La majorité des commandes fournit également une option (--help), un flag (-h) ou un argument (help)
  • … en dernier recours Google c’est pratique aussi !

Raccourcis

Dans un terminal Unix/Linux/WSL :

  • CTRL + C : Annuler le processus ou prompt en cours
  • CTRL + L : Nettoyer le terminal
  • CTRL + A : Positionner le curseur au début de la ligne
  • CTRL + E : Positionner le curseur à la fin de la ligne
  • CTRL + R + mot clé : Rechercher dans l’historique de commandes
  • Tab: Compléter la commande


🎓 Essayez-les ! (Notamment CTRL + R et Tab)

Commandes de base 1/2

  • pwd : Afficher le répertoire courant
    • 🎓 Option -P ?
  • ls : Lister le contenu du répertoire courant
    • 🎓 Options -a et -l ?
  • cd : Changer de répertoire
    • 🎓 Sans argument : que se passe t’il ?
  • cat : Afficher le contenu d’un fichier
    • 🎓 Essayez avec plusieurs arguments
  • mkdir : créer un répertoire
    • 🎓 Option -p ?

Commandes de base 2/2

  • echo : Afficher un (des) message(s)
  • rm : Supprimer un fichier ou dossier
  • touch : Créer un fichier
  • grep : Chercher un motif de texte
  • code : Ouvre le fichier dans l’éditeur de texte

Arborescence de fichiers

  • Le système de fichier a une structure d’arbre
    • La racine du disque dur c’est /
      • 🎓 ls -l /
    • Le séparateur c’est également /
      • 🎓 ls -l /usr/bin

Chemins de l’arborescense

  • Pour référencer un endroit dans l’arbre, on utilise un chemin
  • Deux types de chemins :
    • Absolu (depuis la racine): Commence par / (Ex. /usr/bin)
    • Sinon c’est relatif (e.g. depuis le dossier courant) (Ex ./bin ou local/bin/)
  • Le dossier “courant” c’est .
    • 🎓 ls -l ./bin # Dans le dossier /usr
  • Le dossier “parent” c’est ..
    • 🎓 ls -l ../ # Dans le dossier /usr
  • Tout programme possède un répertoire courant.
    • Le Current / Process Working Directory (CWD/PWD)
  • On peut changer le répertoire courant de l’interpréteur de commande avec cd (change directory)
cd /usr/bin # Change le repertoire courant par /usr/bin : on se déplace dans l'arbre

🎓 Dans quel repertoire nous emmène cette succession de commandes?

cd /usr/bin
cd ../..
cd ./workspaces
  • ~ (tilde) c’est un raccourci vers le dossier “home” de l’utilisateur courant
    • 🎓 ls -l ~
  • - (minus) raccourci pour revenir au dernier répertoire visité (cd -)

Toutes les commandes sont sensible à la casse (majuscules/minuscules) et aux espaces

ls -l /bin
ls -l /Bin
mkdir ~/"Accent tué"
ls -d ~/Accent\ tué
ls -d ~/accent\ tue

Un language (?)

  • Support des variables
# On déclare et initialise une variable
MA_VARIABLE="Salut tout le monde"

# On l'évalue avec avec le caractère `$`
echo "${MA_VARIABLE}"
  • Evaluation d’une sous commande
echo ">> Contenu de /tmp :\n$(ls /tmp)"
  • Des if, des for et plein d’autres trucs (doc)

Codes de sortie

  • Chaque exécution de commande renvoie un code de retour (🇬🇧 “exit code”)
    • Nombre entier entre 0 et 255 (en POSIX )
  • Ce code indique si la commande s’exécutée avec succes ou non
  • Code accessible dans la variable éphémère $? :
ls /tmp
echo $?

ls /do_not_exist
echo $?

# Une seconde fois. Que se passe-t'il ?
echo $?

Entrée, sortie standard et sortie d’erreur

  • Un programme peut consommer des données sur son entrée standard stdin (fd=0)
  • Un programme peut genérer des données sur sa sortie standard stdout (fd=1)
  • Ou en cas d’erreur données sur sa sortie d’erreur stderr (fd=2)

Ces flux de données peuvent être manipulés par l’interpréteur

# Redirige stdout dans un fichier hello.txt
echo "Hello" > /tmp/hello.txt

# Redirige stdout dans le fichier /dev/null
ls -l /tmp >/dev/null
ls -l /tmp 1>/dev/null

ls -l /do_not_exist
# Redirige stdout dans le fichier /dev/null
ls -l /do_not_exist 1>/dev/null
# Redirige stderr dans le fichier /dev/null
ls -l /do_not_exist 2>/dev/null

ls -l /tmp /do_not_exist
# Redirige stdout vers /dev/null et stderr vers stdout /dev/null
ls -l /tmp /do_not_exist 1>/dev/null 2>&1

Pipes

  • Le caractère “pipe” | permet de chaîner des commandes

    • Le “stdout” de la première commande est branchée sur le “stdin” de la seconde
  • Exemple : Afficher les fichiers/dossiers contenant le lettre d dans le dossier /bin :

ls -l /bin

ls -l /bin | grep "d" --color=auto

Exécution de programmes

  • Les commandes sont des fichier binaires exécutables
  • Fichier se trouvant dans l’arborescense de fichier du système
  • Pourtant nous indiquons pas de chemin quand on appelle une commande?

🤔 Comment l’interpréteur retrouve quel fichier exécuter a partir d’un simple nom?

  • La variable d’environnement $PATH liste les dossiers dans lesquels chercher les binaires
  • L’interpréteur cheche le premier binaire qui porte le nom dans les répertoires contenus dans PATH
echo "${PATH}" # Affiche la valeur de PATH

command -v cat # équivalent de "which cat"

ls -l "$(command -v cat)"

💡 Penser a Vérifier cette variable quand une commande fraîchement installée n’est pas trouvée

Checkpoint 🎯

Nous avons vu:

  • Le fonctionnement de l’interpréteur de commande bash
  • Les raccourcis utiles (CTRL+R et TAB)
  • Comment sont organisés les fichiers sous Linux
  • Comment manipuler les flux de données entre commandes

Comment fonctionnent les Internets?

🧐 Que se passe-t-il quand je tape google.com dans mon navigateur et que j’appuie sur entrée?

  1. 📖 Resolution DNS
  2. 🔌 Connection TCP
  3. 🔒 Handshake TLS
  4. ➡️ Envoi d’une requête HTTP au Serveur
  5. ⬅️ Réception d’une reponse HTTP et décodage du contenu HTML
  6. 🎨 Rendu de la page par le navigateur

Zoom sur HTTP

  • Hyper Text Transfer Protocol
  • Défini un format de requête/réponse dans le modèle client / serveur
    • ➡️ Le client demande une ressource à un serveur via une requête HTTP,
    • ⬅️ Le serveur lui réponds une réponse avec le contenu de la ressource.

Anatomie d’une requête HTTP

Une requête est composée des champs suivant:

  • Méthode: Indique une action désirée (GET, POST, PUT, DELETE, HEAD, OPTIONS…)
  • Hote: indique un domaine dans lequel récupérer les resources (github.com)
  • Chemin (path): indique une ressource à obtenir au serveur (/assets/file.js)
  • Paramètres de requête (query parameters): paramètres additionnels de requête apposés au path (/pages/node?utm_source=facebook)
  • Entêtes (headers): Couple clé -> multiples valeurs indiquant des méta information sur la requête (Accepted-Content, User-Agent,Accept, Referrer, Authorization, Cookies)
  • Corps (body): Optionnel, contenu encodé à envoyer au serveur, par exemple une soumission de formulaires.

Une réponse est composée des champs suivant:

  • D’un status code 🐱
    • 200 OK, 404 Not Found, 301 Moved Permanently etc..
  • Entêtes (headers): Couple clé -> multiples valeurs indiquant des méta information sur la réponse (Content-Length, Content-Encoding,Content-Type …)
  • Un corps de réponse à lire et à décoder
    • HTML, JSON ou autre…

Comment parler HTTP depuis le terminal?

  • On propose d’utiliser cURL
  • Outil pour transférer des données dans différents protocoles
    • Le couteau suisse des internets!

🎓 Exercice: Première Requête en utilisant cURL

  • Que signifie cette ligne de commande?
    • Indice: man curl
  • Pouvez expliquer le résultat affiché?
curl --verbose --location --output /dev/null voi.com

✅ Solution: Première Requête en utilisant cURL

  • C’est verbeux 🙃, mais on l’a demandé avec --verbose. cURL va afficher sur la sortie standard tous les échanges effectués avec le serveur
  • --location indique à cURL de suivre les redirections
  • --output indique à cURL d’écrire le contenu dans répondu /dev/null au lieu de l’afficher sur la sortie standard

Regardons d’un peu plus près les logs:

# On se connecte a une IPv6... probablement celle de voi.com?
* Trying [2606:4700:20::681a:3d6]:80...
* Connected to voi.com (2606:4700:20::681a:3d6) port 80

# cURL formule la requête demandée sur HTTP.
> GET / HTTP/1.1
> Host: voi.com
> User-Agent: curl/8.4.0
> Accept: */*
>
# Le serveur nous réponds une 301 !? voi.com à bougé?
< HTTP/1.1 301 Moved Permanently
# [...]
# Aha! Le serveur nous redirige vers le même site, mais en HTTPS sur le port 443.
< Location: https://voi.com:443/
# Comme indiqué: on se reconnecte a voi.com sur le port 443!
* Clear auth, redirects to port from 80 to 443
* Issue another request to this URL: 'https://voi.com:443/'
*   Trying [2606:4700:20::681a:3d6]:443...
* Connected to voi.com (2606:4700:20::681a:3d6) port 443

# On se connecte en HTTPS, du coup il va falloir établir une session TLS
# Ensuite cURL et le serveur se mettent d'accord et établissent la connexion sécurisée.
* (304) (OUT), TLS handshake, Client hello (1):
# [...]
# On est connectés de façon sécurisée au serveur!
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256
* Server certificate:
# [...] Le certificat du serveur est valide!
*  SSL certificate verify ok.
# [...] On refait notre requête une fois connectés!
> GET / HTTP/2
> Host: voi.com
> User-Agent: curl/8.4.0
> Accept: */*
>
# Victoire le serveur nous réponds!
< HTTP/2 200
# Du HTML!
< content-type: text/html; charset=utf-8
# et 22kb de données!
{ [21877 bytes data]
  • Ce qu’il viens de se passer est ce que l’on appelle une HTTPS upgrade
  • Le serveur “force” le client a se connecter en utilisant HTTPS de façon sécurisée!
  • Pourquoi?
    • TLS prouve que le client parle bien au bon serveur!
    • TLS chiffre les communications sur le réseau, on peut faire transiter des données sans(trop) se soucier d’être espionnés 🕵️
  • Maintenant essayez d’enlever l’option --location, que se passe-t-il?
  • Maintenant essayez d’enlever l’option --output /dev/null, que se passe-t-il?

Autres Options Utiles de cURL

  • Contrôle de la méthode de la requête: --request POST, --request DELETE
  • Ajouter un header a la requête: --header "Content-Type: application/json"
  • Envoyer un body dans la requête:
    • Directement depuis la ligne de commande --data '{"some":"json"}'
    • En lisant un ficher --data '@some/local/file'

🎓 Exercice: Afficher du JSON de Façon Lisible

  • Qu’affiche le résultat de la commande suivante?
  • Comment le rendre plus lisible?
    • Indice: il faut utilser un | (pipe) et la commande jq
curl https://swapi.dev/api/planets/1

✅ Solution: Afficher du JSON de Façon Lisible

curl https://swapi.dev/api/planets/1 | jq .

Bonus: jq permets de sélectionner un attribut JSON.

curl https://swapi.dev/api/planets/1 | jq .residents

Checkpoint 🎯

  • Internet repose sur une collection de protocole (DNS, TCP, TLS, HTTP)
  • HTTP permets de formuler une requête à un serveur et une réponse
  • cURL est un outil très complet pour parler HTTP depuis un terminal!

Les fondamentaux de git

Tracer le changement dans le code

avec un VCS : 🇬🇧 Version Control System

également connu sous le nom de SCM (🇬🇧 Source Code Management)

Pourquoi un VCS ?

  • Le code c’est une collection de fichiers textes dans un repertoire
  • … il suffit de sauvegarder ce répertoire pour etre tranquille


Pourquoi encore un outil ?

  • Pour conserver une trace de tous les changements dans un historique
    • Avoir un historique des changements effectués
    • Possibilité de revenir en arrière dans les changements!
  • Pour collaborer efficacement sur une même base de code
    • Gere l’integration de changements dans l’historique
    • Aide à la résolution de conflits
    • Simplifie le partage d’une base de code

Usage d’un VCS

Local

Distribué

Quel VCS utiliser ?

Nous allons utiliser Git

https://git-scm.com/

  • Git nous permets de transformer notre répertoire en dépôt
  • Nous allons pouvoir ensuite ajouter des fichier au dépôt
  • …et ensuite les changer!

🎓 Exercice : Intitialiser un dépôt Git

  • Dans le terminal de votre environnement GitPod:
  • Créez un dossier vide nommé projet-vcs-1 dans le répertoire /workspace, puis positionnez-vous dans ce dossier
mkdir -p /workspace/projet-vcs-1/
cd /workspace/projet-vcs-1/
  • Est-ce qu’il y a un dossier .git/ ?

  • Essayez la commande git status ?

  • Initialisez le dépôt git avec git init

    • Est-ce qu’il y a un dossier .git/ ?
    • Essayez la commande git status ?

✅ Solution : Intitialiser un dépôt Git

mkdir -p /workspace/projet-vcs-1/
cd /workspace/projet-vcs-1/
ls -la # Pas de dossier .git
git status # Erreur "fatal: not a git repository"
git init ./
ls -la # On a un dossier .git
git status # Succès avec un message "On branch master No commits yet"

Les 3 états d’un fichier dans un dépôt Git

  • L’historique (“Version Database”) : dossier .git
  • Dossier de votre projet (“Working Directory”) - Commande
  • La zone d’index (“Staging Area”)

🎓 Exercice : Ajouter un fichier

  • Créez un fichier README.md dedans avec un titre et vos nom et prénoms
    • Essayez la commande git status ?
  • Ajoutez le fichier à la zone d’indexation à l’aide de la commande git add (...)
    • Essayez la commande git status ?
  • Créez un commit qui ajoute le fichier README.md avec un message, à l’aide de la commande git commit -m <message>
    • Essayez la commande git status ?

🎓 Solution : Ajouter un fichier

echo "# Read Me\n\nObi Wan" > ./README.md
git status # Message "Untracked file"

git add ./README.md
git status # Message "Changes to be committed"
git commit -m "Ajout du README au projet"
git status  # Message "nothing to commit, working tree clean"

Terminologie de Git - Diff et changeset

diff: un ensemble de lignes “changées” sur un fichier donné

changeset: un ensemble de “diff” (donc peut couvrir plusieurs fichiers)

Terminologie de Git - Commit

commit: un changeset qui possède un (commit) parent, associé à un message

“HEAD”: C’est le dernier commit dans l’historique


🎓 Exercice :avec Git - 2

  • Afficher la liste des commits
  • Afficher le changeset associé à un commit
  • Modifier du contenu dans README.md et afficher le diff
  • Annulez ce changement sur README.md

✅ Solution : avec Git - 2

git log

git show # Show the "HEAD" commit
echo "# Read Me\n\nObi Wan Kenobi" > ./README.md

git diff
git status

git restore README.md
git status

Terminologie de Git - Branche

  • Abstraction d’une version “isolée” du code
  • Concrètement, une branche est un alias pointant vers un “commit”

🎓 Exercice :avec Git - 3

  • Créer une branche nommée feature/html
  • Ajouter un nouveau commit contenant un nouveau fichier index.html sur cette branche
  • Afficher le graphe correspondant à cette branche avec git log --graph

✅ Solution : avec Git - 3

git branch feature/html && git switch feature/html
# Ou git switch --create feature/html
echo '<h1>Hello</h1>' > ./index.html
git add ./index.html && git commit --message="Ajout d'une page HTML par défaut" # -m / --message

git log
git log --graph
git lg # cat ~/.gitconfig => regardez la section section [alias], cette commande est déjà définie!

Terminologie de Git - Merge

  • On intègre une branche dans une autre en effectuant un merge
  • Plusieurs strategies sont possibles pour merger:
    • Quand l’historique de commit n’a pas diverge: git fait avancer la branche directement, c’est un fast-forward
    • Dans le cas contraire, un nouveau commit est créé, fruit de la combinaison de 2 autres commits

🎓 Exercice :avec Git - 4

  • Merger la branche feature/html dans la branche principale
    • ⚠️ Pensez à utiliser l’option --no-ff (no fast forward) pour forcer git a créer un commit de merge.
  • Afficher le graphe correspondant à cette branche avec git log --graph

✅ Solution : avec Git - 4

git switch main
git merge --no-ff feature/html # Enregistrer puis fermer le fichier 'MERGE_MSG' qui a été ouvert
git log --graph

# git lg

Exemple d’usages de VCS

🎯 Checkpoint

On a vu :

  • A quoi sert git et sa nomenclature de base (diff, changest, commit, branch)
  • A quoi reconnaître un dépôt initialisé local et l’espace de travail associé
  • Comment utiliser git localement (ajouter au staging, commiter)
  • l’historique et un merge avec git (localement)

Javascript et NodeJS

Javascript

  • Langage interprété, syntaxe proche du C
  • Typage dynamique (types des variables assigné // deviné á l’exécution)
  • Gestion de la mémoire automatisée (avec un Garbage Collector)
  • Fonctions pouvant être manipulées comme des variables
  • Supporte différents styles de programation (impératif, OO ou fonctionnel)
  • Pensé pour l’évenementiel, la concurence
  • Standardisé par l’European Computer Manufacturers Association (ECMA)
    • Avec la spécification ECMA-262 réévaluée tous les ans depuis 2015.
  • Langage de script initialement conçu pour rendre interactives des pages web (AJAX)
  • Maintenant aussi utilisé coté serveur!

Ou s’exécute le code Javascript?

Nous avons besoin d’un interpréteur pour exécuter notre code Javascript

  • 🖥️ Coté navigateur:
    • V8 (Chrome et dérivés…)
    • SpiderMonkey (Firefox)
  • 💽 Coté serveur:
    • NodeJS: Un environnement d’exécution Javascript
    • D’autres alternatives existent (deno , bun …)

NodeJS? Kézako?

  • Environement d’exécution Javascript libre et multi platforme
  • Basé sur V8
  • Possède une vaste librairie standard
  • Optimisé pour les opérations asynchrones
  • Actuellement en version 23

🎓 Exercice : Un premier programme NodeJS

  • Dans le répertoire workspace créez un répertoire helloworld
  • Dans ce répertoire, créez un fichier index.js avec le contenu suivant
console.info("Hello World");
  • Exécutez votre programme a l’aide de la commade node ./index.js
  • Que se passe t’il?

✅ Solution : Un premier programme NodeJS

# Crée un répertoire helloworld
mkdir -p /workspace/helloworld
# Saute dans le répertoire
cd /workspace/helloworld
# Crée un fichier index.js avec notre programme
echo 'console.info("Hello world");' > index.js
# Exécute notre programme
node ./index.js

Ce programme affiche sur sa sortie standard le message “Hello World”

  • NodeJS peut exécuter un programme: node ./monprograme.js
  • Ou alors s’exécuter en mode REPL: node
    • 💡 Très utile pour expérimenter… ou s’en servir de calculatrice :D


🎓 Quel est le résultat de l’opération ((38 + 44) / 12) - 1

Un programme un peu plus complexe

Voici maintenant un programme qui résouds le nom de domaine voi.com vers une adresse IP en utilisant DNS

import dns from 'node:dns';

dns.resolve('voi.com', (err, records) => {
    console.log(records, err);
});
  • 🎓 Quelle sont les adresses IP du site voi.com?
  • 🎓 (bonus) et les addresses IPv6? doc

Analysons ligne par ligne

  • import 'dns': Importe le module dns de la librairie standard node
  • dns.resolve: Appelle la fonction resolve du module DNS en passant deux arguments.
    • Une chaine de caractères ‘voi.com’
    • Une fonction anonyme qui accepte deux arguments (err, records) et qui affiche la liste d’adresses

Types en Javacript

Javascript est un langage faiblement typé

let foo = 42; // foo is now a number
foo = "bar"; // foo is now a string
foo = true; // foo is now a boolean

Types primitifs

  • Boolean: true ou false
  • Number: Valeur numérique stockée sur 64 bits (regroupe integer ET float)
  • String: Chaine de caractères (UTF-16)
  • Null: absence d’un object, une seule valeur possible null
  • Undefined: absence de valeur, une seule valeur possible undefined

D’autres types existent … Bigint, Symbol

null vs undefined

  • undefined signifie qu’une variable a été déclarée mais n’a pas reçu de valeur.
  • null signifie l’absence d’objet
let foo;
console.log("FOO", foo) // <- undefined

let bar = null;
console.log("BAR", null) // <- null

Variables en Javascript

  • Une variable est une zone mémoire dans laquelle on peut écrire ou lire une valeur
  • Il existe trois mots clés pour déclarer des variables
    • let: Déclare une variable réassignable
    • const: Déclare une variable non réassignable (en lecture seule)
    • var: Déclare une variable réassignable
      • Maintenue dans la spec pour rétrocompatibilité, mais il est recommandé de ne plus l’utiliser

Différence entre const et let

let foo = 4;
const bar = 12;


foo = 56;
bar = 67;

🎓 Le script suivant s’exécute t’il?

Différences entre var et let/const

Une variable déclarée avec let / const n’est utilisable que si elle est déclarée préalablement.

function good() {
    count = 12; // ReferenceError: Cannot access 'count' before initialization

    let count = 0;
}


function bad() {
    badCount = 42; // Valid, because of some reordering happening when node interprets the code.

    var badCount = 12;
}

bad();
good();

La portée d’une variable déclarée avec let ou const est facilement prédictible

var bad = 12;
let good = 42;

if (true) {
  var bad = 56;
  let good = 12;
}

console.log(bad, good)

🎓 Qu’affiche le script suivant?

var est une source incroyable de problèmes

Variables en Javascript: bonnes pratiques

  • 💀 On n’utilise pas var.
  • ✅ Si notre variable n’est pas réécrite, on utilise const
  • ✅ Sinon, on utilise let.

Déclaration de Block

  • Tout ce qui est entre accolade est un block.
  • La portée des variables déclarées avec const et let est le “block”
{ // block anonyme
  const maVariable = 12;

};

console.log(maVariable);

🎓 Le script suivant s’exécute t’il?

Controle de flot

// if / else
if (condition1) {
  statement1;
} else {
  statement2;
}

// switch case
const name = "julien"
switch (name) {
  case "michel":
     //...
  case "julien":
    console.log("bonjour");
  default:
    // ...
}

Conditions

Toute expression qui évalue vers une valeur booleenne

a == b // égalité
a === b // égalité stricte
a >= b // comparaisons

 a = b // Pas une condition, un assignement!

Egalité vs Egalité stricte

  • a == b egalité, tente de faire des conversion de types implicites
  • a === b égalité stricte, ne fait pas de conversions implicites
let a = 2;
let b = '2';

a == b; // true
a === b; // false, a est du type number, b est du type string

Boucles

for (let step = 0; step < 5; step++) {
  // Runs 5 times, with values of step 0 through 4.
  console.log("Walking east one step");
}

Il y à plein d’autres formes de boucles documentation .

Fonctions

  • Une fonction est un groupement logique d’instructions
  • Accepte des arguments et peut retourner un résultat
function sayHello(name) {
  console.log("Hello", name);
}

Passage des arguments par valeur (on fait une copie)

function passageParValeur(count) {
  count = 56;
}

let value = 45;

passageParValeur(value);

console.log(value); // <- Affiche 45

…sauf dans le cas des objets!

func makeItAHonda(car) {
  car.brand = "honda";
}

const car = {
  brand: "renault",
  seats: 4
};

console.log("brand is", car.brand); // Affiche renault

makeItAHonda(car);

console.log("brand is", car.brand); // Affiche honda!

Les fonctions peuvent etre manipulées comme des valeurs

function otherFunction(callback) {
  // do something...
  const result = getResult();
  callback(result);
}

const myCallback = function(result) {
  console.log("Result is", result);
}

otherFunction(myCallback);

Une fonction à accès aux variables déclarées dans le scope parent.

function sum(a) {
  const word = "coucou";

  return function(b) {
    console.log(word);
    return a + b;
  };
}

console.log(sum(3)(4));

Cela s’appelle une closure

Arrow Functions

Syntaxe allégée pour déclarer et implémenter une fonction.

function otherFunction(callback) {
  // do something...
  const result = getResult();
  callback(result);
}


const myCallback = (result) => {
  console.log(result);
};

otherFunction(myCallback);
otherFunction((result) => console.log(result));

Collections

On distigue deux types:

  • Les collections indexées: tableaux
  • Les collections clés valeur: les maps
const arr = [1, 2, 3, 4];
const arr1 = new Array(1, 2, 3, 4); // équivalent
const arr2 = Array(1, 2, 3, 4);     // équivalent

// Accès par index
console.log(arr[2]);

// Itération
for (v of arr) {
  console.log(v);
}

arr.forEach((v) => console.log(v));

🎓 Exercice: Trouvez la valeur la plus grande dans un tableau;

Soit le tableau suivant:

const arr = [18, 4, 99, 1203, 5, 3, 5566, 22, 12];
  • Écrivez une fonction qui permet de trouver la valeur la plus grande d’un tableau
  • Bonus, faites le en une seule ligne (👀 Indice et Indice )

✅ Solution: Trouvez la valeur la plus grande dans un tableau

function findMax(arr) {
  let max = 0;

  for value of arr {
    if value > max {
      max = value;
    }
  }

  return max;
}

// Ou encore...
function findMaxH4ckZ0r(arr) {
  return arr.reduce((max, v) => v > max ? v : max)
}

💭 Quelle version préférez vous?

Objets

Un objet est une collection d’attributs.

On peut créer un objet avec une expression litterale

const myCar = {
  brand: "Renault",
  wheels: 3,
  year: 1997
}

Ou encore avec un constructeur

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}

const myCar = new Car("renault", 3, 1997);

Accéder aux propriétés d’un objet

const myCar = {
  brand: "Renault",
  wheels: 3,
  year: 1997
}


// Dot notation
console.log("La marque est", myCar.brand);

// Bracket notation
console.log("La marque est", myCar["brand"]);

// Les attributs non définis ont la valeur `undefined`
console.log(myCar.wings) // undefined

Classes

  • Depuis ES6 (2015), Javascript supporte l’idée de classe, comme d’autres langages orientés objet
  • Une classe c’est:
    • Une définition de type
    • Qui comporte un état (une collection d’attributs)
    • Et des méthodes (des fonctions attachées a ce type)
class Car {
  // Attributs privés
  #brand = "";
  #wheels = 0;
  #year = 0;

  // Constructeur
  constructor(brand, wheels, year) {
    this.#brand = brand
    this.#wheels = wheels
    this.#year = year
  }

  // Accesseur
  get wheels() {
    return this.#wheels
  }

  // Methode
  isOperational() {
    return this.#wheels === 4;
  }
}

On crée une nouvelle instance d’une classe, un objet, avec new

const myCar = new Car("Renault", 3, 1998);

if (myCar.isOperational()) {
  console.log("I can drive my car");
} else {
  console.log("I can't drive my car");
}

myCar.#wheels // Jette une erreur!
myCar.wheels // Utilise l'accesseur!

Un petit mot au sujet de this

  • this est une référence vers l’objet courant dans une méthode
  • En dehors d’un objet, this pointe vers “l’objet global”
  • Pour une fonction la valeur de this est déterminée au moment de l’appel de la fonction
function Car() {
  this.honk = function() {
    console.log("HONK HONK");
  };

  setTimeout(function() {
    this.honk(); // TypeError: this.honk is not a function WAAAAAAAT?
  }, 2000);
}

Ce comportement (peu intuitif) change avec les arrow functions, this est hérité du scope de l’appelant!

function Car() {
  this.honk = function() {
    console.log("HONK HONK");
  };

  setTimeout(() => {
    this.honk();
  }, 2000);
}

Gestion d’erreur

Javascript représente une erreur a l’aide d’exceptions:

  • On “jette” une exception a l’aide de l’instruction throw
  • On “attrape” une exception avec des instructions try..catch..finally
function doSomethingButFails() {
  throw "oops something went wrong";
}


try {
  doSomethingButFails();
} catch (e) {
  console.error("Could not do something", e);
} finally {
  console.info("Show must go on! Let's proceed anyway!");
}

Interpolation de chaines de caractères

On peut faire de l’interpolation de chaines de caractères en utilisant les backticks

const age = 12;
const message = `Your age is ${age}`;

console.log(message); // Your age is 12

🎓 Exercice: Votre Premier Serveur HTTP avec Node

  • Ecrivez un script js qui lance un serveur HTTP de façon a ce que la commande curl suivante reçoive en réponse
curl "localhost:3000?name=julien"

Hello julien
  • Il faut se servir de la fonction createServer module HTTP de node doc
  • Ne pas oublier de démarer le serveur en appelant listen doc
  • Pour extraire le paramètre de requète name il vous faut
    • Parser l’URL de la requète aver const reqUrl = new URL("http://localhost"+req.url)
    • Et ensuite accéder au paramètre avec reqUrl.searchParams.get("name")

✅ Solution: Votre Premier Serveur HTTP avec Node

import http from "node:http";
import { URL } from "node:url";

const srv = http.createServer((req, res) => {
  const reqUrl = new URL("http://localhost"+req.url);
  res.statusCode = 200;
  res.end("Hello " + reqUrl.searchParams.get("name")+"\n");
});

srv.listen(3000, "localhost", () => {
  console.log("Serving requests...");
});

Modules Javascript

  • En JS, un fichier est égal a un module
  • Deux standards existent
    • CommonJS: Venant de l’ecosystème NodeJS
    • MJS: Standardisé par ECMA

Modules Javascript MJS

// moduleA.js
class Car {
  // etc...
}
const message = "Hello, from Module A!";

export default message;
export Car;
// moduleB.js
import messageA, { Car } from "./moduleA";

console.log(messageA);
const myCar = new Car()

🎓 Exercice: Déplacez votre serveur HTTP dans un module

  1. Groupez la logique de votre serveur dans une fonction dédiée
  2. Déplacez cette fonction dans un nouveau module JS (nouveau fichier) qui exporte cette fonction.
  3. Importez votre module dans votre script index.js

✅ Solution: Déplacez votre serveur HTTP dans un module

// server.js
import http from "node:http";
import { URL } from "node:url";

export function runServer() {
  const srv = http.createServer((req, res) => {
    const reqUrl = new URL("http://localhost"+req.url);
    res.statusCode = 200;
    res.end("Hello " + reqUrl.searchParams.get("name")+"\n");
  });

  srv.listen(3000, "localhost", () => {
    console.log("Serving requests...");
  });
}
// index.js
import { runServer } from "./server.js";

runServer();

Javascript Asynchrone

  • Ajouez une ligne de log immédiatement après l’appel a votre fonction qui execute le serveur
  • Est-ce que cette ligne sera affichée après Serving requests... ou avant?
// index.js
import { runServer } from "./server.js";

runServer();
console.log("Done with index.js");
  • Done with index.js est affichée avant!
  • Pourquoi?
    • Server.listen est une opération asynchrone
    • Cette opération crée un socket et écoute dessus, cela utilise un (ou plusieurs) appels système bloquants
      • Problème : nodejs n’utilise qu’un seul thread, si l’on effectue une opération bloquante de façon synchrone, le reste de notre application ne pourra plus s’exécuter pendant la durée de cette opération!
      • Solution: nodejs ne bloque pas sur les appels systèmes, mais enregistre le fait qu’il faut appeler une fonction dite callback passée en argument quand l’opération bloquante est terminée!

Différents styles de programation asynchrone:

  • 🔴 Callbacks: on passe une ou plusieurs fonction appelées quand l’opération bloquante est terminée
    • Simple, mais cela mène rapidement a du code 🍝
  • 🟡 Promesses: une fonction bloquante retourne une promesse.
  • 🟢 Async/Await: On cache une promesse derrère des mots clefs du langage.

Promesses

Objet qui représente la complétion our l’echec d’une opération asynchrone.

// promesse qui résouds après 500ms!
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve("hello ENSG");
  , 500)
});

promise
  // On chaine un premier "handler" qui traite le resultat de la promesse et retourne une autre valeur
  .then((result) => {
    console.log("Result is", result);
    return result.length
  })
  // On chaine un second "handler" qui traite le resultat de la promesse précédente
  .then((length) => {
    console.log("length is", length)
  })
  // On chaine un "handler" d'erreur qui traite le cas ou notre promesse échoue
  .catch((err) => {
    console.error(err);
  });

🎓 Exercice: Utilisation de l’API fetch

Fetch est une API native de Javascript qui permets de récupérer une resource en effectuant une requète HTTP doc


  • Ecrivez un script JS qui affiche la population de Tatooine en récupérant avec fetch le contenu à l’URL suivante
https://swapi.dev/api/planets/1
  • 💡 Pour parser en JSON le contenu de la réponse HTTP, il faut utiliser la méthode json de la réponse … qui retourne une promesse!
  • 💡 Il est possible de voir un aperçu de la réponse JSON ici

✅ Solution: Utilisation de l’API fetch

//index.js

fetch("https://swapi.dev/api/planets/1")
  .then((resp) => resp.json())
  .then((planet) => {
    console.log("planet population is ", planet.population);
  });

Bon c’est toujours un peu 🍝🍝🍝🍝🍝🍝

Async / Await

  • ECMASript 2017 introduit les fonctions asynchones: fonction déclarée avec le mot clé async
  • Une fonction asyncrhone peut porter une ou plusieurs instructions await qui attendent la résolution d’une promesse.
async function doStuff() {
  const promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("Hello ENSG"), 500);
  })

  const result = await promise;

  console.log("Result is", result)
  console.log("Length is", result.length)
}

doStuff();

🎓 Exercice: fetch avec Async / Await

Réécrivez votre programme qui donne la population de tatooine en utilisant Async / Await

✅ Exercice: fetch avec Async / Await

async function fetchTatooinePopulation() {
  const response = await fetch("https://swapi.dev/api/planets/1");
  const planet = await response.json();

  console.log("planet population is", planet.population);
}

fetchTatooinePopulation();

Checkpoint 🎯

Références

Typescript

Le typage dynamique, cette fausse bonne idée!

function printLength(item) {
  console.log("Length is", item.length);
}


printLength([1,2,3]);         // Ok!
printLength("coucou");        // Ok!
printLength({"name":"joel"}); // Ok... oh wait?

Toute une classe d’erreurs (stupides) de programation ne sont détectées qu’a l’exécution, alors qu’on pourrait les trouver bien plus tot!

Erreurs d’autant plus faciles à commettre…

🤯🤯 🤯 Et si on rajoutait un système de typage statique sur Javascript 🤯 🤯 🤯

Hello Typescript

  • Langage Open Source dévelopé par Microsoft
  • 1.0 en 2014, Actuellement en v5
  • Rajoute des annotations optionelles de typage a Javascript
  • Superset de Javascript (tout programme Javascript est valide en Typescript)

Un premier programme Typescript

type User = {
  name: string;
  age: number;
};

function isAdult(user: User): boolean {
  return user.age;
}

const justine = {
  name: 'Justine',
  age: 27,
} satisfies User;

const isJustineAnAdult = isAdult(justine);

🎓 Exécutez ce programme avec node

type User = {
     ^^^^

SyntaxError: Unexpected identifier 'User'
    at wrapSafe (node:internal/modules/cjs/loader:1497:18)
    at Module._compile (node:internal/modules/cjs/loader:1519:20)
    at Object..js (node:internal/modules/cjs/loader:1709:10)
    at Module.load (node:internal/modules/cjs/loader:1315:32)
    at Function._load (node:internal/modules/cjs/loader:1125:12)
    at TracingChannel.traceSync (node:diagnostics_channel:322:14)

La “““transpilation”””

  • node exécute du Javascript, pas du Typescript
  • Il faut convertir notre code Typescript en Javascript
  • Pour cela on utilise la commande tsc (Typescript Compiler)
tsc ./index.ts

Visiblement le compilateur Typescript a trouvé quelque chose!

index.ts(7,3): error TS2322: Type 'number' is not assignable to type 'boolean'.
  • 💀 C’est un souci de typage qui signifie une erreur de logique!
  • Notre fonction isAdult devrait comparer l’age de la personne à un age seuil pour dire si elle est adulte ou non!

🎓 Notre programme à un souci, corrigez le!

Types primitifs

string, number, void (rien) and boolean

let age :number = 10;
let age = 10; // Equivalent, tsc devine le type de la variable age en fonction de la valeur assignée!

const isFalse :boolean = false;

const name = "hello word";

age = name; // Type 'string' is not assignable to type 'number'.

Collections

const ages :number[] = [2, 3, 4];
const ages = [2, 3, 4];                 // Equivalent
const ages :Array<number> = [2, 3, 4]; // Equivalent, utilise un type parametrique

Any

any est un type spécial qui signifie tous les types. A éviter car on perds l’intérèt d’utiliser typescript

let foo :any = 4;
foo = "string";
foo = {};

Fonctions

On rajoute des annotations aux arguments et à la valeur de retour!

function sayHello(name: string, age: number): void {
  console.log(`Hello ${name}, you have ${age} years old`);
}

const sayArrow = (name :string, age :number): void => {
  console.log(`Arrow ${name}, you have `${age} years old`);
};

Pour les fonctions anonymes, le type est deviné par le compilateur.

const numbers = [1, 2, 3, 4];

numbers.forEach((v) => console.log(v.length)); // Property 'length' does not exist on type 'number'

Objets

function sayHello(user: { name: string, age: number}): void {
    console.log(`Hello ${user.name}, your age is ${user.age}`);
}

type User = {
  name: string;
  age: number;
};

function sayHelloClean(user :User): void {
    console.log(`Hello ${user.name}, your age is ${user.age}`);
}

Définir ses Propres Types

Deux façons plus ou moins équivalentes: alias de types ou interfaces

// Déclare le type user qui est un objet deux attributs (name et age).
type User = {
  name: string;
  age: number;
};

// Déclare l'interface User2 qui représente l'ensemble des objets ayants au moins l'attribut name et age.
interface User2 {
  name: string;
  age: number;
}

function sayHello(user: User) {
  //...
}

function sayHello2(user: User2) {
  // ...
}

const myUser = {name: "John", age: 40};
const myCar = {name: "Bernadette2", age: 4, brand: "McLaren" };

sayHello(myUser); // OK!
sayHello2(myUser); // OK!
sayHello(myCar); // OK!
sayHello2(myCar); // OK!

Attributs Optionels

type User = {
  name: string;
  age: number;
  haircut?: string; // Haircut peut etre soit une string, soit undefined
};

const user: User = {name: "foo", age: 12};

console.log(user.haircut);

user.haircut = "banana"

console.log(user.haircut);

Types Paramétriques

Permets de définir un type en fonciton d’un (ou plusieurs) autre(s).

// Définit une interface Container de quelque chose (type T).
interface Container<T> {
  content: T;
}

let intContainer :Container<number> = {content: 4};
let stringContainer :Container<string> = {content: "foooo"};

//Exemple concret: Arrays!

const myArray :Array<string> = ["foo", "bar", "biz"];

Unions

type NumberOrString = number | string;
type StringOrNullOrUndefined = string | null | undefined;

type User = {
  name: string;
  age: number;
  haircut: StringOrNullOrUndefined; // Haircut peut etre soit une string, soit undefined
};

🎓 Exercice: Corrigez le code suivant

Corrigez le code suivant de façon à ce que tsc passe… (sans toucher au corps de la fonction startCar)

type Car = unknown;

function startCar(car: any) {
  console.log(`Cheking the wheels ${car.wheels.length}`);
  console.log(`Starting the car ${car.brand}`);
}

✅ Solution: Corrigez le code suivant

type Wheel = {};

type Car = {
  brand: string;
  wheels: Array<Wheel>;
};

function startCar(car: Car) {
  console.log(`Cheking the wheels ${car.wheels.length}`);
  console.log(`Starting the car ${car.brand}`);
}

Checkpoint 🎯

  • Nous avons vu comment utiliser Typescript

  • Nous avons vu rapidement quelques primitives de bases du langage

    • Types primitifs et annotations
    • Objets
    • Définition de types, unions et types generiques
  • Pour aller plus loin c’est par ici !

Présentation de votre projet

Contexte(1/4)

  • Voi est une société qui fournit un service de “véhicules de transport doux” à la demande
    • 🔓 Vous déverrouillez un véhicule avec votre Smartphone
    • 🛴 Vous faites votre trajet avec le véhicule
    • 🔒 Vous verrouillez et laissez le véhicule sur votre lieu d’arrivée
    • 💸 Vous payez le temps passé sur le véhicule

Contexte(2/4)

Un cas d’utilisation majeur est de permettre aux utilisateurs de trouver un véhicule proche d’eux facilement.

Contexte(3/4)

  • Ce service est assuré par une API HTTP appelée vehicle-server qui doit supporter les fonctionnalités suivantes:
    • Enregistrer un véhicule
    • Trouver les N véhicules les plus proches de soi
    • Supprimer un véhicule

Contexte (4/4)

  • Voi est entrain de reconstruire cette fonctionnalité et à décidé de sous-traiter le développement de ce projet a l’ENSG…
  • Une équipe technique de Voi avait commencé l’implémentation du serveur, et vous à mis à disposition une archive ici , contenant le code source du projet.

Prise en Main du Projet

cd /workspace

# Téléchargez le projet sur votre environnement de développement
curl -sSLO https://cicd-lectures.github.io/assets/vehicle-server.tar.gz

# Décompresser et extraire l'archive téléchargée
tar xvzf ./vehicle-server.tar.gz

cd ./vehicle-server

A partir de la vous pouvez ouvrir le fichier README.md et commencer à suivre ses instructions.

Qu’est-ce qui va / ne va pas dans ce projet d’après vous?

Triste Rencontre avec la Réalité

  • Pas de gestion de version
  • node_modules nous est fourni tel quel, aucun moyen de le reconstruire.
  • Le projet ne fonctionne pas complètement,
    • delete réponds un erreur 😭
    • create accepte un shortcode de 6 caractères, et en demande 4!
  • On lance le js directement depuis dist…mais on ne sait pas le générer!

🎓 Exercice : Initialisez un dépôt git

  • Supprimez les répertoires dist, node_modules et l’archive
  • Mettez en place un fichier .gitignore qui vous évitera de comitter dist/ et node_modules!
  • Initialisez un dépôt git dans le répertoire
  • Créez un premier commit, avec uniquement le code source Typescript

✅ Solution : Initialisez un dépôt git

# Suppression des fichiers générés
rm -f /workspace/vehicle-server.tar.gz
rm -rf ./dist ./node_modules

# Création et édition du fichier .gitignore
code .gitignore

# On initialise un nouveau dépôt git
git init

# On ajoute tous les fichiers contenus a la zone de staging.
git add .

# On crée un nouveau commit
git commit -m "Add initial vehicle-server project files"

Contenu du fichier .gitignore

node_modules/
dist/

Checkpoint 🎯

  • Vous avez récupéré un projet typescript qui semble fonctionner…

    • ..mais pas vraiment à l’état de l’art.
  • Application des chapitres précédents : vous avez initialisé un projet git local

Produire un Livrable

🤔 Quel est le problème ?

On a du code. C’est un bon début. MAIS:

  • Le code en lui meme ne délivre pas de valeur,
    • C’est l’exécution de ce programme qui en produit!
  • Qu’est ce qu’on “fabrique” à partir du code ?
    • Un livrable (un binaire, une image Docker, une application iOS // Android …)

Caractéristiques de notre livrable 📦

Nous souhaitons que notre livrable soit:

  • Versionné: Chaque livrable est associé a une version de notre base de code
  • Reproductible:
    • Il est possible et facile de générer notre livrable
    • Deux générations de livrables partant d’une meme version génèrent le “même” livrable!

Que signifie “reproductible” ?

  • Il faut que notre processus de génération de livrable, (le build) soit entièrement déterministe.
  • Il faut qu’en fonction d’un jeu de paramètres, le résultat du build soit même le “même”.
  • Il en va de même pour l’environnement ou notre programme est exécuté.
    • Notre environnement de production

Quels sont les paramètres de notre livraison ?

  • Le code: Dans quelle version est-il? Est-il fonctionnel? Est-ce qu’il est sauvegardé?
  • Les dépendances de notre code: Toutes les libraires utilisés dans notre application.
  • Les outils de génération de livrables: Quel compilateur et dans quelle version?
  • L’environnement d’exécution cible: Node 22 ou Node 23? Quelle version de PostgreSQL? Quel OS/Architecture CPU? Quel Navigateur?
  • Le processus de livraison lui même: Dans quelle mesure la procédure de génération est elle répétable et respectée?

Risques encourus?

  • Ne pas etre capable de livrer!
  • 😡 Dans le meilleur des cas, votre livrable ne marche pas du tout.
  • 🤡 Dans certains cas votre livrable va casser sans explication facile et seulement sur la production du client les jours impairs d’une année bisextile.
    • Allez reproduire et débugger!
  • 😱 Livrer votre application va devenir une angoisse permanente
  • 😱😭🔥☠️ Vous livrez une CVE ou un malware, avec un accès direct a votre base de données.

On en est où la dedans? (1/2)

  • Le code
    • ✅ On vient de mettre en place git. On sait identifier une version par un hash de commit.
    • ❌ On ne sait pas vraiment dire si l’application “fonctionne” ou pas.
  • Les dépendances de notre code
    • ❌ On ne sait ni les récupérer, ni les contrôler.
  • Les outils permettant de générer notre livrable
    • ❌ Typescript 5 est indiqué dans la documentation fournie mais c’est tout

On en est où la dedans? (2/2)

  • L’environnement cible:
    • ❌ Aucune version de node indiquée.
    • ❌ On sait que l’on à besoin de Postgres et Postgis
  • Le processus de livraison lui même:
    • ❌ Nous n’avons encore rien défini

Quelles solutions ? (1/2)

  • Le code
    • ➡️ Solution (pour garantir une bonne utilisation): L’analyse statique (le lint)
    • ➡️ Solution (pour savoir si il fonctionne): les tests automatisés
    • ➡️ Solution (pour garantir qu’il fonctionne à chaque changement): l’intégration continue (CI)
  • Les dépendances du code
    • ➡️ Solution: Mise en place d’un outil de gestion et d’audit des dépendances
  • Les outils de génération du code:
    • ➡️ Solution: Mise en place d’un processus automatisé de génération de livrable s’expécutant dans un environment controllé

Quelles solutions ? (2/2)

  • L’environnement cible:
    • ➡️ Solution: Utilisation d’outils de packaging (Docker) pour notre application et son environment cible
  • Le processus de livraison lui même:
    • ➡️ Solution: définir un cycle de vie et en déduire un processus de livraison

Les grandes étapes de la génération de notre livrable

  1. build: Compilation de l’application
  2. lint: Analyse statique de code pour détecter des problèmes ou risques
  3. test: Exécution de la suite de tests automatisées
  4. package: Création du livrable
  5. release: Livraison du livrable

Checkpoint 🎯

Notre première étape va etre de faire en sorte de pouvoir lancer le serveur dans notre environment de développement.


Cela sigifie:

  1. Installer toutes les dépendances nécesaires pour la génération et l´exécution de notre code
  2. Générer du code exécutable (appeler tsc)

La Gestion de Dépendances

Pourquoi réutiliser du code et des outils?

  • 🧱 L’informatique moderne est un assemblage de briques logicielles
  • ⚙️ … chacune des briques étant infiniment complexe
    • Ex: TLS, PostgresSQL, Linux, Firefox…
  • 🤔 Il est difficilement envisageable de démarrer un projet sans réutiliser des briques logicielles.
  • 🧘 Cela permet de concentrer son effort de développement sur ce qui apporte de la valeur.
    • ➡️ Dans notre cas, notre métier est la gestion de véhicules, pas l’implémentation d’une pile réseau et d’un serveur HTTP.

⚠️ Ajouter une dépendance n’est pas un acte anodin ⚠️

  • Si votre dépendance ne fonctionne plus ou est compromise, votre livrable sera impactée
  • Attention à ne pas rajouter une dépendance trop grosse pour n’utiliser qu’une petite fonctionnalité!
  • Attention aux dépendances de vos dépendances 😱
  • Quelques règles d’usage:
    • Vérifier que votre dépendance est activement maintenue? (date du dernier commit, existence d’une communauté autour)
    • 👀 le code. Est-ce que vous le comprenez? Est-ce que vous pourriez le debugger ou le faire vous même?

Dépendre de librairies externes pose une quantité de problèmes!

  • Comment récupérer l’intégralité du code dont on à besoin?
  • Comment maintenir à jour ce code?
  • Comment s’assurer qu’il n’a pas été modifié?
  • Comment garantir la reproductibilité de ce processus?

Mais le pire, c’est que c’est un problème récursif! Nos dépendances ont aussi des dépendances!

Un peu de terminologie

  • Une dépendance est une librairie de code externe ou un outil qui fournit une fonctionnalité.
  • On distingue deux types de dépendances:
    • dépendance directe: référencée directement par notre application
    • dépendance transitive: référencée par une des librairies dont l’application dépends

Comment gérer ses dépendances?

  • On introduit un outil de gestion de dépendances
    • Permets au développeur de définir une liste de dépendances en fixant ou en plaçant une contrainte de version (ex <= 4.3.0)
    • Cela permet de construitruire un arbre de dépendances
    • Télécharge toutes les dépendances dans l’environment d’exécution et les mets à disposition de l’application.

Comment cela fonctionne avec Javascript?

NPM kesako?

  • Node Package Manager
  • Package Manager et infrastructure de distribution de paquets (registry)
  • Gère aussi bien les dépendances de code que les outils
  • Des alternatives existent (yarn et pnpm)

Du coup c’est quoi un paquet?

  • Un fichier ou un répertoire décrit par un fichier package.json
  • Un paquet peut mettre à disposition:
    • Un module Javascript / Typescript
    • Des scripts exécutables
      • Pour lancer un script il vous faut utiliser la commande npx <script>

🎓 Exercice: Initialisez un nouveau paquet NPM

  • Dans le répertoire /workspace/vehicle-server
    • Lancez la comande npm init
    • Répondez aux questions posées
  • Observez ensuite le fichier généré

Le fichier package.json

  • Fichier décrivant un paquet npm
  • Contient des métadonées a propos du paquet
  • Et surtout, la liste des paquets dont dépends notre projet!
    • ℹ️ Ses dépendances directes

Zoom sur les dépendances

npm distingue quatre types de dépendances:

  • Dépendances de prodution: nécessaires a l’exécution du projet
  • Dépendances de développement: nécessaires au développement du projet (option --save-dev)
  • Dépendances de peers: contraint des versions entres packages (option --save-peers)
  • Dépendances optionelles: dépendances non nécessaires mais pouvant ajouter des foncitonalités (option --save-optional)

⚠️ Selon où vous utilisez npm, vous n’avez pas besoin de toutes les dépendances ⚠️

Comment NPM gère les dépendances transitives?

  • Certains paquets (la majorité) ont aussi des dépendances, qui deviennent indirectement des dépendances de notre projet
  • npm télécharge chacune des dépendances de chacun des parquets et les écrit dans node_modules
    • Oui la taille du répertoire node_modules est un problème 😭
  • ☢️ Cela signifie que plusieurs versions d’une meme paquet peut etre présent dans l’arbre de dépendance d’un projet!

Visualiser l’arbre de dépendances

  • npm ls permets de lister les dépendances d’un projet (--all affiche tout l’arbre de dépendance!)
  • Ce site permets d’afficher l’arbre de dépendance d’un paquet public NPM

🎓 Exercice: Ajoutez Typescript comme dépendance

  • Il nous faut typescript d’installé pour pouvoir générer du Javascript
  • Le package se trouve ici
  • Quels sont les fichiers / répertoires changés ou créés par npm? Quels sont leur roles?

✅ Exercice: Ajoutez Typescript comme dépendance

  • npm install --save-dev typescript
    • ⚠️ -D ou --save-dev: Typescript n’est pas utile a l´exécution!
  • Une section devDependencies est ajoutée au package.json
    • typescript est listé avec la version ^5.6.3
  • Un mystérieux fichier package-lock.json est aussi généré!
  • Enfin un répertoire node_modules est créé content le code du package typescript!

Configurer le Compilateur Typescript

tsc nécessite un fichier de configuration tsconfig.json pour fonctionner.

Créez ce fichier et ajoutez le contenu suivant:

{
  "compilerOptions": {
    "target": "es2020",                                  /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
    "module": "commonjs",                                /* Specify what module code is generated. */
    "outDir": "dist",                                   /* Specify an output folder for all emitted files. */
    "esModuleInterop": true,                             /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
    "forceConsistentCasingInFileNames": true,            /* Ensure that casing is correct in imports. */
    "strict": true,                                      /* Enable all strict type-checking options. */
    "skipLibCheck": true                                 /* Skip type checking all .d.ts files. */
  }
}

Versions

NPM suit un standard pour exprimer des versions appelé semantic versioning

Un changement ne changeant pas le périmètre fonctionnel incrémente le numéro de version patch.

Par exemple: une correction de bug

Un changement changeant le périmètre fonctionnel de façon rétrocompatible incrémente le numéro de version mineure.

Par exemple: L’ajout d’une nouvelle fonction dans une librairie

Un changement changeant le périmètre fonctionnel de façon rétrocompatible incrémente le numéro de version mineure.

Par exemple: L’ajout d’une nouvelle fonction dans une librairie

Un changement changeant le périmètre fonctionnel de façon non rétrocompatible incrémente le numéro de version majeure.

Par exemple: Le retrait d’une fonction dans une librairie

On résume

  • Changer de version mineure ne devrait avoir aucun d’impact sur votre code.
  • Changer de version majeure peut nécessiter des adaptations.

Mais tout ça dépends du bon vouloir et de la rigeur des mainteneurs 😅

Contraindre des versions avec NPM

typescript est listé avec la version ^5.6.3 (notez le ^), qu’est ce que cela signifie?

NPM liste en dépendance des ranges de versions, cela signifie:

  • ^ : Toutes les versions ayant la meme version majeure
    • exemple: 5.6.4, 5.9.10, 5.48.4
  • ~ : Toutes les versions ayant la meme version mineure
    • exemple: 5.6.4, 5.6.5, 5.6.6
  • >, >= : Toutes les version superieures a la version indiquée
    • exemple:5.6.4, 6.4.3 etc…
  • 5.6.4: Un range d’une valeur unique… on fixe la version

npm install permets de forcer l’installation d’un paquet dans une certaine version

npm install is-true@4.3.0 # <- installera is true avec la contrainte ^4.3.0

Comment NPM fonctionne?

  1. Prends les dépendances listées dans un fichier package.json
  2. Résouds toutes les dépendances (transitives et directes) vers la plus grande version autorisée et disponible
  3. Télécharge l’arbre de dépendance et l’écrit dans le répertoire node_modules

Vous voyez un problème?

Que se passe t’il si, entre deux installations, une nouvelle version autorisée par la liste des dépendances est publiée?

⚠️ L’installation des paquets NPM n’est pas reproductible!

  • Les paquets installés peuvent changer d’une installation l’autre!
  • Les contraintes de versions exprimées ne suffisent pas “figer” un arbre de dépendance!
    • On peut figer ses dépendances directes, mais aucune garantie que les dites dépendances feront de même avec les leurs!
  • 😭 Cela peut introduire des problèmes!
  • 💀 … ou pire être un vecteur d’attaque !

Solution: le fichier package-lock.json

  • C’est une photo qui capture l’arbre de dépendances complet et permets á NPM d’installer exactement le même arbre!
  • Il capture pour chaque dépendance:
    • La version exacte utilisée
    • L’URL de téléchargement du paquet (resolved)
    • Une somme de contrôle de l’archive téléchargée (integrity), qui permet de vérifier que l’archive téléchargée n’est pas altérée


🎓 Que continent votre fichier package-lock.json? Quelle est la particularité du paquet typescript?

🎓 Exercice: Installez les dépendances nécessaires pour transpiler et exécuter le projet!

  • Pour compiler:
    • Il nous faut les définitions de types des libraries JS utilisées (paquets @types/xxxx)
    • Les définitons de types de NodeJS
  • Pour exécuter:
    • Il nous faut le framework web express en version 5.0.1!
    • Il nous faut aussi la lib pg , pour se connecter à la base de donnée
  • Faites attention a bien différencier les dépendances d’exécution, des dépendances de développement

Voici les commandes pour compiler et lancer le serveur

# Lancer le serveur de base de données (si il n'existe pas déja!)
docker run -d -name vehicle-database -e POSTGRES_USER=vehicle -e POSTGRES_PASSWORD=vehicle -e POSTGRES_DB=vehicle -p 5432:5432 postgis/postgis:16-3.4-alpine
# Compile le serveur de Typescript vers Javascript
npx tsc
# Lance le serveur javascript
node dist/index.js
# Vous pouvez ensuite rejouer les requètes du README!

✅ Solution: Installez les dépendances nécessaires pour transpiler et exécuter le projet!

npm install --save-dev @types/express @types/node @types/pg
npm install express@5.0.1 pg

rm -rf dist/
npx tsc
node dist/index.js

(Bonus) Combien de dépendances ont été installées au total? 😱

Audit de dépendances

  • Du coup cette petite aventure nous aura fait installer 117 paquets!
  • Il est virtuellement impossible d’aller auditer soi même toutes ces dépendances!
  • npm fournit une commande audit qui permets de détecter (voir même de corriger automatiquement) des problèmes de sécurité sur l’arbre de dépendances!
    • ⚠️ Toutes les failles de sécurité ne se valent pas (exemple )
    • ⚠️ Cela n’empêche pas d’aller mettre son nez dans le code de certaines de vos dépendances

Scripts NPM

NPM permets de définir dans le fichier package.json des scripts exécutables via la commande npm run <script-name> Cela permets de “normaliser” les commandes utilisées pour travailler avec un projet

// package.json
{
  // ...
  "scripts": {
    "build": "rm -rf ./dist && tsc",
    "start": "npm run build && node dist/index.js",
    "start-db": "docker container run -d -name vehicle-database -e POSTGRES_USER=vehicle -e POSTGRES_PASSWORD=vehicle -e POSTGRES_DB=vehicle -p 5432:5432 postgis/postgis:16-3.4-alpine",
    "stop-db": "docker container rm -f vehicle-database"
  },
  // ...
}

🎓 Ajoutez ces scripts a votre fichier package.json, vous pourrez utiliser ensuite les commandes suivantes

npm run start-db
npm run start
npm run stop-db

Checkpoint 🎯

  • Nous avons vu les défis de la gestion de dépendance et le fonctionnement de NPM!
  • Un paquet npm sans package-lock.json n’est pas reproductible!
  • Nous sommes maintenant capable de compiler et d’exécuter notre projet!
    • C’est une étape importante, créez donc un commit pour sauvegarder ça!

Mettre son code en sécurité

Une autre petite histoire

Votre dépôt est actuellement sur votre ordinateur.

  • Que se passe t’il si :
    • Votre disque dur tombe en panne ?
    • On vous vole votre ordinateur ?
    • Vous échappez votre tasse de thé / café sur votre ordinateur ?
    • Une météorite tombe sur votre bureau et fracasse votre ordinateur ?

C’est arrivé à la personne qui a écrit ces slides…

Comment éviter ça?

  • Répliquer votre dépôt sur une ou plusieurs machines !
  • Git est pensé pour gérer ce de problème

Gestion de version décentralisée

  • Chaque utilisateur maintient une version du dépôt local qu’il peut changer à souhait
  • Ils peuvent “pousser” une version sur un dépôt distant
  • Un dépôt local peut avoir plusieurs dépôts distants.

Centralisé vs Décentralisé

Source: Geek for Geeks

Cela rends la manipulation un peu plus complexe, allons-y pas à pas :-)

🎓 Exercice: Créer un dépôt distant

  • Rendez vous sur GitHub
    • Créez un nouveau dépôt distant en cliquant sur “New” en haut à gauche
    • Appelez le vehicle-server
    • Une fois créé, mémorisez l’URL (https://github.com/...) de votre dépôt :-)
    • Inscrivez l’URL de votre depot ici

Consulter l’historique de commits

Dans votre workspace

# Liste tous les commits présent sur la branche main.
git log

Associer un dépôt distant (1/2)

Git permet de manipuler des “remotes”

  • Image “distante” (sur un autre ordinateur) de votre dépôt local.
  • Permet de publier et de rapatrier des branches.
  • Le serveur maintient sa propre arborescence de commits, tout comme votre dépôt local.
  • Un dépôt peut posséder N remotes.

Associer un dépôt distant (2/2)

# Liste les remotes associés a votre dépôt
git remote -v

# Ajoute votre dépôt comme remote appelé `origin`
git remote add origin https://<URL de votre dépôt>

# Vérifiez que votre nouveau remote `origin` est bien listé a la bonne adresse
git remote -v

Publier une branche dans sur dépôt distant

# git push <remote> <votre_branche_courante>
git push origin main

Que s’est il passé ?

  • git a envoyé la branche main sur le remote origin
  • … qui à accepté le changement et mis à jour sa propre branche main.
  • git a créé localement une branche distante origin/main qui suis l’état de main sur le remote.
  • Vous pouvez constater que la page github de votre dépôt affiche le code source

Refaisons un commit !

git commit --allow-empty -m "Yet another commit"
git push origin main

Branche distante

  • Dans votre dépôt local, une branche “distante” est automatiquement maintenue par git
  • Elle suit le dernier état connu de la branche sur le remote.
  • Pour voir toutes les branches distantes
    • git branch -a
  • Pour mettre a jour les branches distantes depuis le remote:
    • git fetch <nom_du_remote>
# Lister toutes les branches y compris les branches distances
git branch -a


# Notez que est listé remotes/origin/main


# Mets a jour les branches distantes du remote origin
git fetch origin


# Rien ne se passe, votre dépôt est tout neuf, changeons ça!

Créez un commit depuis GitHub directement

  • Cliquez sur le bouton éditer en haut à droite du “README”
  • Changez le contenu de votre README
  • Dans la section “Commit changes”
    • Ajoutez un titre de commit et une description
    • Cochez “Commit directly to the main branch”
    • Validez



GitHub crée directement un commit sur la branche main sur le dépôt distant

Rapatrier les changements distants

# Mets à jour les branches distantes du dépôt origin
git fetch origin

# La branche distante main a avancé sur le remote origin
# => La branche remotes/origin/main est donc mise a jour

# Ouvrez votre README
code ./README.md

# Mystère, le fichier README ne contient pas vos derniers changements?
git log

# Votre nouveau commit n'est pas présent, AHA !

Branche Distante VS Branche Locale

Le nouveau commit à été rapatrié

Cependant il n’est pas encore présent sur votre branche main locale

# Merge la branch distante dans la branche locale.
git merge origin/main

Vu que votre branche main n’a pas divergé (== partage le même historique) de la branche distante, git merge effectue automatiquement un “fast forward”.

Updating 1919673..b712a8e
Fast-forward
 README.md | 1 +

 1 file changed, 1 insertion(+)

Cela signifie qu’il fait “avancer” la branche main sur le même commit que la branche origin/main

# Liste l'historique de commit
git log

# Votre nouveau commit est présent sur la branche main !
# Juste au dessus de votre commit initial !

Et vous devriez voir votre changement dans le ficher README.md

Git(Hub|Lab|tea|…)

Un dépôt distant peut être hébergé par n’importe quel serveur sans besoin autre qu’un accès SSH ou HTTPS.

Une multitudes de services facilitent et enrichissent encore git: (GitHub, Gitlab, Gitea, Bitbucket…)

=> Dans le cadre du cours, nous allons utiliser GitHub.

git + GitHub = superpowers!

  • GUI de navigation dans le code
  • Plateforme de gestion et suivi d’issues
  • Plateforme de revue de code
  • Intégration aux moteurs de CI/CD
  • And so much more…

Intégration Continue (CI)

Continuous Integration doesn’t get rid of bugs

But it does make them dramatically easier to find and remove.

Martin Fowler

Pourquoi faire de L’Intégration Continue ?

But : Détecter les fautes au plus tôt pour en limiter le coût

Source : http://cartoontester.blogspot.be/2010/01/big-bugs.html

Qu’est ce que l’Intégration Continue ?

Objectif : que l’intégration de code soit un non-évènement

  • Construire et intégrer le code en continu
  • Le code est intégré souvent (au moins quotidiennement)
  • Chaque intégration est validée de façon systématique et automatisée
  • On joue une collection de vérifications qui atteste que le changement ne casse pas l’existant
    • compilation, tests, lint etc…

Et concrètement ? 1/2

  • Un•e dévelopeu•se•r ajoute du code/branche/PR :
    • une requête HTTP est envoyée au système de “CI”
  • Le système de CI compile et teste le code
  • On ferme la boucle : Le résultat est renvoyé au dévelopeu•se•r•s

Et concrètement ? 2/2

Quelques moteurs de CI connus

GitHub Actions

GitHub Actions est un moteur de CI/CD intégré à GitHub

  • ✅ : Très facile à mettre en place, gratuit et intégré complètement
  • ❌ : Utilisable uniquement avec GitHub, et DANS la plateforme GitHub

Concepts de GitHub Actions

  • Sur un évènement déclencheur
  • GitHub crée un ou plusieurs environment d’exécution (Runner)
  • Et exécute une ou plusieurs suites d’étapes
  • Enfin la (les) runner(s) sont détruits une fois toutes les étapes exécutées

Concepts de GitHub Actions - Step 1/3

Concepts de GitHub Actions - Step 2/3

Une Step (étape) est une tâche individuelle à faire effectuer par le CI :

  • Par défaut c’est une commande à exécuter - mot clef run
  • Ou une “action” (quel est le nom du produit déjà ?) - mot clef uses
steps: # Liste de steps
  # Exemple de step 1 (commande)
  - name: Say Hello
    run: echo "Hello ENSG"
  # Exemple de step 2 (une action)
  - name: 'Login to DockerHub'
    uses: docker/login-action@v1 # https://github.com/marketplace/actions/docker-login
    with:
      username: ${{ secrets.DOCKERHUB_USERNAME }}
      password: ${{ secrets.DOCKERHUB_TOKEN }}

Concepts de GitHub Actions - Step 3/3

Une Step peut avoir des outputs

steps:
  - name: "Install Node"
    uses: actions/setup-node@v4
    id: setup_node
    with:
      node-version: '22.04'

  - name: "Echo installed version"
    run: |
      echo "${{steps.setup_node.outputs.node-version}}"

Concepts de GitHub Actions - Job 1/2

Concepts de GitHub Actions - Job 2/2

Un Job est un groupe logique de steps :

  • Enchaînement séquentiel de steps
  • 1 Job == 1 Runner créé
  • Regroupement logique :
    • Exemple : “compiler puis tester le résultat de la compilation”
jobs: # Map de jobs
  build: # 1er job, identifié comme 'build'
    name: 'Build Slides'
    runs-on: ubuntu-22.04 # cf. prochaine slide "Concepts de GitHub Actions - Runner"
    steps: # Collection de steps du job
      - name: 'Build the JAR'
        run: mvn package
      - name: 'Run Tests on the JAR file'
        run: mvn verify
  deploy: # 2nd job, identifié comme 'deploy'
    # ...

Concepts de GitHub Actions - Runner

Un Runner est un serveur distant sur lequel s’exécute un job.

Concepts de GitHub Actions - Workflow 1/2

Concepts de GitHub Actions - Workflow 2/2

Un Workflow est une procédure automatisée composée de plusieurs jobs, décrite par un fichier YAML.

  • On parle de “Workflow/Pipeline as Code”
  • Chemin : .github/workflows/<nom du workflow>.yml
  • On peut avoir plusieurs fichiers donc plusieurs workflows
.github/workflows
├── ci-cd.yaml
├── bump-dependency.yml
└── nightly-tests.yaml

Concepts de GitHub Actions - Évènement 1/2

Concepts de GitHub Actions - Évènement 2/2

Un évènement du projet GitHub (push, merge, nouvelle issue, etc. ) déclenche l’exécution du workflow

  • Plein de type d’évènements : push, issue, alarme régulière, favori, fork, etc.

    • Exemple : “Nouveau commit poussé”, “chaque dimanche à 07:00”, “une issue a été ouverte” …
  • Un workflow spécifie le(s) évènement(s) qui déclenche(nt) son exécution

    • Exemple : “exécuter le workflow lorsque un nouveau commit est poussé ou chaque jour à 05:00 par défaut”

Concepts de GitHub Actions : Exemple Complet

name: Node.js CI
on: # Évènements déclencheurs
  - push:
      branch: main # Lorsqu'un nouveau commit est poussé sur la branche "main"
  - schedule:
    - cron: "*/15 * * * *" # Toutes les 15 minutes
jobs:
  test-linux:
    runs-on: ubuntu-24.04
    steps:
    - uses: actions/checkout@v5
    - uses: actions/setup-node@v6
    - run: npm run test
  test-mac:
    runs-on: macos-12
    steps:
    - uses: actions/checkout@v5
    - uses: actions/setup-node@v6
    - run: npm run test

Essayons GitHub Actions

  • But : nous allons créer notre premier workflow dans GitHub Actions

  • N’hésitez pas à utiliser la documentation de GitHub Actions:

Exemple simple avec GitHub Actions

  • Retournez dans le dépôt créé précédemment dans votre environnement GitPod
  • Dans le projet “vehicle-server”, sur la branch main,
    • Créez le fichier .github/workflows/bonjour.yml avec le contenu suivant :
name: Bonjour
on:
  - push
jobs:
  dire_bonjour:
    runs-on: ubuntu-24.04
    steps:
      - run: echo "Bonjour 👋"
  • Commitez puis poussez
  • Revenez sur la page GitHub de votre projet et naviguez dans l’onglet “Actions” :
    • Voyez-vous un workflow ? Et un Job ? Et le message affiché par la commande echo ?

Exemple simple avec GitHub Actions : Récapépète

Exemple GitHub Actions : Checkout

  • Supposons que l’on souhaite utiliser le code du dépôt…
    • Essayez: modifiez le fichier bonjour.yml pour afficher le contenu de README.md :
name: Bonjour
on:
  - push
jobs:
  dire_bonjour:
    runs-on: ubuntu-24.04
    steps:
      - run: ls -l # Liste les fichier du répertoire courant
      - run: cat README.md # Affiche le contenu du fichier `README.md` à la base du dépôt

Est-ce que l’étape “cat README.md” se passe bien ? (SPOILER: non ❌ )

🎓 Exercice GitHub Actions : Checkout

  • But : On souhaite récupérer (“checkout”) le code du dépôt dans le job

  • 👷🏽‍♀️ C’est à vous d’essayer de réparer 🛠 le job :

    • L’étape “cat README.md” doit être conservée et doit fonctionner
    • Utilisez l’action “checkout” Documentation du marketplace GitHub Action
    • Vous pouvez vous inspirer du Quickstart de GitHub Actions

✅ Solution GitHub Actions : Checkout

name: Bonjour
on:
  - push
jobs:
  dire_bonjour:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v5 # Récupère le contenu du dépôt correspondant au commit du workflow en cours
      - run: ls -l # Liste les fichier du répertoire courant
      - run: cat README.md # Affiche le contenu du fichier `README.md` à la base du dépôt

Exemple : Environnement d’exécution

  • Notre workflow doit s’assurer que “la vache” 🐮 doit nous lire 💬 le contenu du fichier README.md
  • Essayez la commande cat README.md | cowsay dans GitPod
    • Modifiez l’étape “cat README.md” du workflow pour faire la même chose dans GitHub Actions
    • SPOILER: ❌ (la commande cowsay n’est pas disponible dans le runner GitHub Actions)

Problème : Environnement d’exécution

  • Problème : On souhaite utiliser les mêmes outils dans notre workflow ainsi que dans nos environnement de développement

  • Plusieurs solutions existent pour personnaliser l’outillage, chacune avec ses avantages / inconvénients :

    • Personnaliser l’environnement dans votre workflow: (⚠️ sensible aux mises à jour, ✅ facile à mettre en place)
    • Spécifier un environnement préfabriqué pour le workflow (⚠️ complexe, ✅ portable)
    • Utiliser les fonctionnalités de votre outil de CI (⚠️ spécifique au moteur de CI, ✅ efficacité)

🎓 Exercice : Personnalisation dans le workflow

  • But : exécuter la commande cat README.md | cowsay dans le workflow comme dans GitPod
  • 👷🏽‍♀️ C’est à vous de mettre à jour le workflow pour personnaliser l’environnement :

✅ Solution : Personnalisation dans le workflow

name: Bonjour
on:
  - push
jobs:
  dire_bonjour:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v5 # Récupère le contenu du dépôt correspondant au commit du workflow en cours
      - run: |
          sudo apt-get update
          sudo apt-get install -y cowsay
      - run: cat README.md | cowsay

🎓 Exercice : Environnement préfabriqué

✅ Solution : Environnement préfabriqué

name: Bonjour
on:
  - push
jobs:
  dire_bonjour:
    runs-on: ubuntu-24.04
    container:
      image: ghcr.io/cicd-lectures/gitpod:latest
      options: --user=root
    steps:
      - uses: actions/checkout@v5 # Récupère le contenu du dépôt correspondant au commit du workflow en cours
      - run: cat README.md | cowsay

Checkpoint 🎯

  • Quel est l’impact en terme de temps d’exécution du changement précédent ?
  • Problème : Le temps entre une modification et le retour est crucial

🎓 Exercice : Optimiser avec les fonctionnalités du moteur de CI

✅ Solution : Optimiser avec les fonctionnalités du moteur de CI

name: Bonjour
on:
  - push
jobs:
  dire_bonjour:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v5 # Récupère le contenu du dépôt correspondant au commit du workflow en cours
      - uses: juliangruber/read-file-action@v1
        id: readfile
        with:
          path: ./README.md
      - uses: Code-Hex/neo-cowsay-action@v1
        with:
          message: ${{ steps.readfile.outputs.content }}

🎓 Exercice : Intégration Continue du projet “vehicle-server”

👷🏽‍♀️ C’est à vous de modifier le projet “vehicle-server” pour faire l’intégration continue!

  • Nous souhaitons mettre en place un workflow qui, pour chaque commit poussé sur votre dépôt, va:
    • Récupérer le code de l’application depuis GitHub
    • Installer node dans la même version majeur que la version de GitPod
    • L’application est compilée et le code Javascript est généré dans le répertoire dist

Pensez à supprimer/renommer le workflow bonjour.yaml

🎓 Solution : Intégration Continue du projet “vehicle-server”

name: Vehicle Server CI
on:
  - push
jobs:
  ci:
    runs-on: ubuntu-24.04
    steps:
      - name: Checkout Code
        uses: actions/checkout@v5
      - name: Setup Node
      - uses: actions/setup-node@v6
        with:
          node-version: '22.x'
      - name: Check Node Version
        run: node --version
      - name: Build application
        run: npm run build
      - name: List dist output
        run: ls dist/

🎯 Checkpoint

  • Pour chaque commit poussé dans la branche main du Vehicle Server,
  • GitHub action vérifie que l’application est compilable et fabriquée,
  • Avec un feedback (notification GitHub).

=> On peut modifier notre code avec plus de confiance !

Git à plusieurs

Limites de travailler seul

  • Capacité finie de travail
  • Victime de propres biais
  • On ne sait pas tout

Travailler en équipe ? Une si bonne idée ?

  • … Mais il faut communiquer ?
  • … Mais tout le monde n’a pas les mêmes compétences ?
  • … Mais tout le monde y code pas pareil ?

Collaborer c’est pas évident, mais il existe des outils et des méthodes pour vous aider.

Git Multijoueur

  • Git permet de collaborer assez aisément
  • Chaque développeur crée et publie des commits…
  • … et rapatrie ceux de de ses camarades !
  • C’est un outil très flexible… chacun peut faire ce qu’il lui semble bon !

… et (souvent) ça finit comme ça !

Comment eviter cette situation?

  • La clé est de vous mettre d’accord sur des règles de fonctionnement avec votre équipe!
  • En ce qui concerne l’utilisation de Git on parle de définir un Git Flow


En voici un exemple

Gestion des Branches

  • Les “versions” du logiciel sont maintenues sur plusieurs branches principales (main, staging)
  • Ces branches reflètent l’état du logiciel
    • main: version actuelle en production
    • staging: prochaine version
  • Chaque groupe de travail (développeur, binôme…)
    • Crée une branche de travail à partir de la branche staging
    • Une branche de travail correspond à une chose à la fois
    • Pousse des commits dessus qui implémentent le changement

Quand le travail est fini, la branche de travail est “mergée” dans staging

Quand on souhaite faire une nouvelle version du logiciel. On merge staging dans main, et l’on crée un tag sur le commit de merge

Qu’est ce qu’un tag?

  • Merger staging dans main est un évenement important qui doit figurer dans l’historique de notre code
  • Un identifiant de commit est de granularité trop faible pour être utilisable.
  • Utilisation de tags git pour définir des versions.
  • Un tag git est une référence sur un commit, attachant un label à un commit spécifique
git tag 1.0.0 -a -m "Première release 1.0.0"

Gestion des remotes

Où sont stockées ces branches ?

Plusieurs modèles possibles

  • Un remote pour les gouverner tous !
  • Chacun son propre remote (et les commits seront bien gardés)
  • … toute autre solution est bonne,
    • …du moment que toute votre équipe l’utilise!

Un remote pour les gouverner tous

Tous les développeurs envoient leur commits et branches sur le même remote

  • Simple a gérer …
  • … mais nécessite que tous les contributeurs aient accès au dépôt
    • Adapté a l’entreprise, peu adapté au monde de l’open source

Chacun son propre remote

  • La motivation: le contrôle d’accès

    • Tout le monde peut lire le dépôt principal. Personne ne peut écrire dessus.
    • Tout le monde peut dupliquer le dépôt public et écrire sur sa copie.
    • Toute modification du dépôt principal passe par une procédure de revue.
    • Si la revue est validée, alors la branche est “mergée” dans la branche cible
  • C’est le modèle poussé par GitHub !

Forks ! Forks everywhere !

Dans la terminologie GitHub:

  • Un fork est un remote copié d’un dépôt principal
    • C’est la où les contributeurs poussent leur branche de travail.
    • Les branches de version (main, staging…) vivent sur le dépôt principal
    • La procédure de ramener un changement d’un fork à un dépôt principal s’appelle la Pull Request (PR)

🎓 Exercice : Créez un fork

  • Nous allons vous faire forker vos dépôts respectifs
  • Trouvez vous un binôme dans le groupe.
  • Rendez vous sur cette page pour inscrire votre binôme.
  • Depuis la page du dépôt de votre binôme, cliquez en haut à droite sur le bouton Fork.

A vous de jouer: Corrigez la fonctionnalité “suppression d’un véhicule” dans projet de votre binôme

🎓 Exercice : Contribuez au projet de votre binôme (1/4)

Première étape:

  1. On clone le fork dans son environnement de développement
  2. On crée une branche de développement
cd /workspace/

# Clonez votre fork
git clone <url_de_votre_fork>

# Créez votre feature branch
git switch --create fix-delete
# Équivalent de git checkout -b <...>

🎓 Exercice : Contribuez au projet de votre binôme (2/4)

  • Extraire l’identifiant (la valeur id) du path en utilisant req.params.
  • Parser la valeur obtenue en number en utilisant parseInt
  • Appeler la méthode deleteVehicle du VehicleStore en passant l’identifiant.
    • ⚠️ C’est une méthode asynchrone!
  • Enfin il faut faire une réponse:
    • Si la suppression est réussie, répondre un status code 204 en appelant res.status(xxx).send(),

N’oubliez pas de tester votre changements en utilisant les exemples du fichier README!

🎓 Exercice : Contribuez au projet de votre binôme (3/4)

Une fois que vous êtes satisfaits de votre changement il vous faut maintenant créer un commit et pousser votre nouvelle branche sur votre fork.

🎓 Exercice : Contribuez au projet de votre binôme (4/4)

Dernière étape: ouvrir une pull request!

  • Rendez vous sur la page de votre projet
  • Sélectionnez votre branche dans le menu déroulant “branches” en haut a gauche.
  • Cliquez ensuite sur le bouton ouvrir une pull request
  • Remplissez le contenu de votre PR (titre, description, labels) et validez.

La procédure de Pull Request

Objectif : Valider les changements d’un contributeur

  • Technique : est-ce que ça marche ? Est-ce maintenable ?
  • Fonctionnel : est-ce que le code fait ce que l’on veux ?
  • Humain : Propager la connaissance par la revue de code.
  • Méthode : Tracer les changements.

Revue de code ?

  • Validation par un ou plusieurs pairs (technique et non technique) des changements
  • Relecture depuis github (ou depuis le poste du développeur)
  • Chaque relecteur émet des commentaires // suggestions de changement
  • Quand un relecteur est satisfait d’un changement, il l’approuve
  • La revue de code est un exercice difficile et potentiellement frustrant pour les deux parties.
    • Comme sur Twitter, on est bien à l’abri derrière son écran
  • En tant que contributeur, soyez respectueux de vos relecteurs : votre changement peut être refusé et c’est quelque chose de normal.
  • En tant que relecteur, soyez respectueux du travail effectué, même si celui ci comporte des erreurs ou ne correspond pas à vos attentes.

💡 Astuce:Proposez des solutions plutôt que simplement pointer les problèmes.

🎓 Exercice : Relisez votre PR reçue !

  • Vous devriez avoir reçu une PR de votre binôme
  • Relisez le changement de la PR
  • Effectuez quelques commentaires (bonus: utilisez la suggestion de changements), si c’est nécessaire
  • Si elle vous convient, approuvez la!
  • En revanche ne la “mergez” pas, car il manque quelque chose…

Validation automatisée

Objectif: Valider que le changement n’introduit pas de régressions dans le projet

  • A chaque fois qu’un nouveau commit est créé dans une PR, une succession de validations (“checks”) sont déclenchés par GitHub
  • Effectue des vérifications automatisées sur un commit de merge entre votre branche cible et la branche de PR

Quelques exemples

  • Analyse syntaxique du code (lint), pour détecter les erreurs potentielles ou les violations du guide de style
  • Compilation du projet
  • Exécution des tests automatisés du projet
  • Déploiement du projet dans un environnement de test…

Ces “checks” peuvent êtres exécutés par votre moteur de CI ou des outils externes.

🎓 Exercice : Déclencher un Workflow de CI sur une PR

  • Votre PR n’a pas déclenché le workflow de CI de votre binôme 🤔
  • Il faut changer la configuration de votre workflow pour qu’il se déclenche aussi sur une PR
  • Vous pouvez changer la configuration du workflow directement dans votre PR
  • La documentation se trouve par ici
  • Quand vous poussez votre changement, vous devriez voir votre workflow CI s’exécuter!

✅ Exercice : Déclencher un Workflow de CI sur une PR

name: Vehicle Server CI
on:
  - push
  - pull_request
jobs:
  ci:
    runs-on: ubuntu-24.04
    steps:
      - name: Checkout Code
        uses: actions/checkout@v5
      - name: Setup Node
      - uses: actions/setup-node@v6
        with:
          node-version: '22.x'
      - name: Check Node Version
        run: node --version
      - name: Build application
        run: npm run build
      - name: List dist output
        run: ls dist/

🎯 Checkpoint

Nous avons vu:

  • Un exemple de modèle de gestion de branches git
  • Plusieurs modèles de gestions de remotes
  • Ce qu’est un “fork” sur GitHub
  • Le processus pour effectuer une contribution en utilisant un projet forké!

  • ➡️ Nous allons maintenant améliorer notre CI en ajoutant de l’analyse statique et des tests!
  • ➡️ Vous pouvez merger la PR reçue et supprimer le répertoire du fork dans votre workspace Gitpod

Analyse Statique de Code

(lint)

Pourquoi faire de l’analyse statique?

  • Objectif: améliorer les capacités de notre CI à détecter des problèmes
    • Plus on détecte des problèmes tôt, moins ils seront couteux!
  • Un linter est un programme qui parcours une base de code à la recherche d’erreurs sans exécuter le programme
  • Ces outils permettent de détecter de nombreuses erreurs de types variés
  • Ils permettent de garantir aussi une utilisation uniforme du langage dans une base de code

Par exemple

  • Appel d’une fonction asynchrone sans utiliser await
  • Une assignation ou une lecture de variable non typée
  • Problèmes d’indentation
  • Risque d’injection SQL ou d’exécution arbitraire de commandes
  • Utilisation de “nombres magiques”…

Comparaison avec Typescript

function sayName(value: any) {
  console.log(value.name);
}
  • Cet exemple de code est valide pour le compilateur Typescript
  • Il est en revanche dangereux, aucune vérification n’est faite pour prouver que value à un attribut name
  • Ce n’est pas parce que le code est correct d’un point de vue typage qu’il est forcément juste!
    • Il n’y à pas de ça compile alors ça marche

Typescript + Lint = ❤️

  • Typescript nous permets d’annoter notre code avec des informations supplémentaires sur le type de variables
  • Cet ajout d’information permets aux analyseurs de code d’affiner leurs vérifications
  • Ce sont deux outils parfaitement complémentaires!

ESLint

https://eslint.org

  • Le linter prédominant de la communauté Javascript
  • Construit autour de l’idée de règles
  • Une règle vérifie qu’un bout de code valide une certaine attente, et indique comment le corriger
  • ESLint embarque des centaines de règles par défaut
  • Mais est aussi facilement extensible à l’aide de plugins

typescript-eslint

  • ESLint n’est conçu que pour travailler avec Javascript
  • Fort heureusement le projet typescript-eslint étends les capacités d’eslint pour qu’il puisse supporter Typescript!
  • Il arrive avec un parseur typescript et aussi un lot de règles groupés par presets pour valider du code TS

🎓 Exercice : Mettez en place typescript-eslint dans votre projet

Dans une nouvelle branche, à jour de main, de votre dépot vehicle-server

  • Mettez en place typescript-eslint en suivant le guide de mise en place
  • On souhaite activer les presets strict et stylistic
  • On souhaite aussi que npm run lint excécute l’analyse statique sur la base de code typescript!
  • Corrigez éventuellement les erreurs et warnings rapportées
    • 💡eslint peut corriger certaines erreurs automatiquement!

✅ Solution : Mettez en place typescript-eslint dans votre projet

  1. On ajoute les dépendances de développement au package npm du projet
npm install --save-dev eslint @eslint/js @types/eslint__js typescript-eslint
  1. On ajoute la configuration typescript-eslint
// ./eslint.config.mjs
// @ts-check

import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';

export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.recommended,
);
  1. On ajoute un script lint au fichier package.json
{
...
  "scripts":{
    // ...
    "lint": "eslint ./src"
  }
}
  1. On lance le lint et on applique la correction automatique
npx eslint --fix ./src
npm run lint

🎓 Exercice : Ouvrez une PR qui rajoute l’excécution du lint à votre workflow de CI

Nous voulons maintenant que le workflow de CI excécute le lint après la compilation

✅ Solution : Ouvrez une PR qui rajoute l’excécution du lint à votre workflow de CI

  1. On crée un commit rajoutant les outils de lint et leur configuration
  2. On rajoute un autre commit qui rajoute un step executant npm run lint dans le workflow de CI
  3. On ouvre une PR avec ces changements
name: Vehicle Server CI
on:
  - push
  - pull_request
jobs:
  ci:
    runs-on: ubuntu-24.04
    steps:
      - name: Checkout Code
        uses: actions/checkout@v5
      - name: Setup Node
      - uses: actions/setup-node@v6
        with:
          node-version: '22.x'
      - name: Check Node Version
        run: node --version
      - name: Build application
        run: npm run build
      - name: Run Lint
        run: npm run lint
      - name: List dist output
        run: ls dist/

🎯 Checkpoint

Nous avons vu:

  • L’analyse statique est un outil complémentaire au typage statique
  • Cela permet de détecter des problèmes et d’assurer une bonne utilisation du langage
  • Nous avons mis en place ESLint + typescript-eslint
  • Et notre workflow vérifie que notre base de code respecte toutes les règles de lint!
  • ➡️ Si le CI est vert, vous pouvez merger votre PR!

Tests Automatisés

Qu’est ce qu’un test ?

C’est du code qui:

  • Crée les conditions d’un cas de test (given)
  • Appelle le système testé (when)
  • Valide les résultats retourné et les effets de bords du système (then)

Pourquoi faire des tests?

  • Objectif: rendre notre CI capable de détecter des problèmes de logique
  • Prouve que le logiciel se comporte comme attendu à tout moment
  • Détecte les impacts non anticipés des changements introduits, les régressions

Qu’est ce que l’on teste ?

  • Une fonction
  • Une combinaison de classes
  • Un serveur applicatif et une base de données

On parle de SUT, System Under Test.

Différents systèmes, Différentes Techniques de Tests

  • Test unitaire
  • Test d’intégration
  • Test de bout en bout
  • Smoke tests
  • Test de performance

(La terminologie varie d’un développeur / langage / entreprise / écosystème à l’autre)

Test unitaire

  • Test validant le bon comportement une unité de code
  • Prouve que l’unité de code interagit correctement avec les autres unités.
  • Test s’excécutant rapidement, ne nécessite aucune infrastructure.
  • Les autres composants dont l’unité de code dépends sont “bouchonnés”, cela pour garantir leur simplicité et leur facilité.
    • Par Exemple: la couche d’accès a la base de données est réimplémentée en mémoire.

Tests et Jabbascript

  • Javascript / Typescript ne fourni pas de framework de tests, Il nous faut en installer un nous même
  • On se propose d’utiliser Jest
  • Permets de décrire des tests, faire des assertions et définir des mocks simplement, tout en un!
  • La encore, hautement configurable et extensible!

Jest & Typescript

  • Jest est conçu pour décrire des tests en Javascript…
  • Pour utiliser Typescript, il nous faut utiliser un transformer Jest qui transforme les tests décrits en Typescript vers Javascript
  • Plusieurs options existent. On se propose d’utiliser ts-jest

🎓 Exercice: Installez Jest et ts-jest🐛

✅ Solution: Installez Jest et ts-jest🐛

  1. Installation du framework de test
# Installation des dépendances
npm install --save-dev jest ts-jest @types/jest

# Initialisation de la configuration de jest
npx ts-jest config:init
  1. Ajout du script test dans le fichier package.json
{
...
  "scripts":{
    // ...
    "test": "jest"
  }
}
  1. On excécute la suite de tests
npm run test

Vous deviez obtenir ce message d’erreur!

No tests found, exiting with code 1
Run with `--passWithNoTests` to exit with code 0
In /workspace/vehicle-server
  23 files checked.
  testMatch: **/__tests__/**/*.[jt]s?(x), **/?(*.)+(spec|test).[tj]s?

Bon c’est bien sympa, mais il faudrait ajouter notre premier test.

Nous avons un bug…

Apparament lorsque l’on essaye de créér un véhicule

curl \ 
    -H "Content-Type: application/json" \ 
    --data '{"shortcode":"abbc", "battery": 12, "latitude": 53.43, "longitude": 43.43}' \ 
    localhost:8080/vehicles | jq .

Le serveur nous réponds

{
  "error": {
    "code": 2,
    "message": "Invalid create vehicle request",
    "details": {
      "violations": [
        "Shortcode must be only 4 characters long"
      ]
    }
  }
}

Pourtant le shortcode donné est abbc, du coup le serveur devrait accepter cette requète 🤬

Vie d’une requète dans notre serveur

  • Notre serveur réponds a certaines combinaisons de verbes HTTP + path: les routes
  • Les routes sont configurées dans le fichier app.ts
  • A chaque route est associé un Controller qui valide puis traduit une requète HTTP vers de la logique métier
    • Les controlleurs se trouvent dans ./src/controller
  • Les controlleurs, pour persister les données peuvent utiliser aussi le VehicleStore, qui permets de manipuler les véhicles en base
    • Le store se trouve dans ./stc/store/vehicle.ts

🎓 Exercice: Avec ces informations, pouvez vous isoler la ligne problèmatique dans notre code?

  • ./src/controller/create.ts L46 est problèmatique
  • Ce qui fait que validateRequestPayload retourne une violation L16
  • Et du coup le controller jette une instance de AppError
  • La correction est facile à faire…
  • …mais essayons d’abord d’écrire un test qui prouve que la logique de validation fonctionne!
  • …comme ça le problème ne se reproduira plus!

Contenu de notre test

  • Ici note SUT est la seule méthode publique du CreateVehicleController, la méthode handle
  • Notre test joue le scénario suivant
    • Sachant que j’ai une requète valide!
    • Lorsque j’appelle la méthode handle du CreateVehicleController
    • Alors la réponse doit avoir le contenu attendu. (resultat)
    • (et optionellement) on valide que CreateVehicleStore à été appellé (effet de bord)

Problème: nous ne voulons pas intéragir avec la base de données!

  • Le constructeur de VehicleStore necessite un pg.Pool, un pool de connections a la base de données.
  • Dans le cas d’un test unitaire, nous n’avons pas d’infrastructure disponible.
  • Solution: Utilisation d’un bouchon (mock) pour notre instance de la classe VehicleStore
    • Une fausse implémentation, pilotable par les tests, qui permets de nous affranchir de notre base de donnée

Mise en place du test

Créez un fichier ./src/controller/create.test.ts et ajoutez le contenu suivant.

import {expect, jest, test} from '@jest/globals';
import { Pool } from 'pg';
import { Request, Response } from 'express';

import { FakeResponse } from "../fake/response";
import { CreateVehicleController } from "./create";
import { Vehicle } from "../model/vehicle";
import { VehicleStore } from "../store/vehicle";
import { AppError, ErrorCode } from "../errors";

// On définit ici un module `Mock` ie: tout chargement du module `import { VehicleStore } from "../store/vehicle'`
// retournera une """fausse""" implémentation qui  n'intéragit pas avec la base de données.
jest.mock('../store/vehicle', (() => ({
  VehicleStore: jest.fn().mockImplementation(() => {
    return {
      createVehicle: jest.fn().mockImplementation(async (req: any): Promise<Vehicle> => {
        return new Vehicle(
          12,
          req.shortcode,
          req.battery,
          req.position,
        );
      }),
    }
  })
})));

Cycle de Vie d’un test

  • Un test nécessite une phase de mise en place, et une phase de nettoyage
  • Ces phases sont jouées soit avant / après chaque test beforeEach, afterEach
  • ou en début / fin d’un bloc describe, via beforeAll, afterAll
  • Motivation: L’isolation des tests, pour éviter que un test est un effet sur un autre test de la même suite

Ajoutez le bloc suivant a la fin de votre fichier de test

// Describe décrit un groupe logique de tests, ayant la même logique de mise en place et de nettoyage.
describe('create vehicle controller', () => {
  let controller: CreateVehicleController;
  let store: VehicleStore;

  // Avant chaque test on réinitialise le store et le controller.
  beforeEach(() => {
    store =  new VehicleStore({} as Pool); // <- instance mockée!
    controller = new CreateVehicleController(store);
  });

  test('creates a valid vehicle', async () => {
    // Given (mise en place du test).

    // When (exécution de la méthode testée).

    // Then (validation des résultats.
  });
});

Vous pourez ensuite lancer npm run test

 PASS  src/controller/create.test.ts
  create vehicle controller
    ✓ creates valid vehicle (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.036 s
Ran all test suites.

Implémentation du Test Unitaire

Il nous faut maintenant implémenter le corps du test

// Describe décrit un groupe logique de tests, ayant la même logique de mise en place et de nettoyage.
describe('create vehicle controller', () => {
  let controller: CreateVehicleController;
  let store: VehicleStore;

  // Avant chaque test on réinitialise le store et le controller.
  beforeEach(() => {
    store =  new VehicleStore({} as Pool); // <- instance mockée!
    controller = new CreateVehicleController(store);
  });

  test('creates a valid vehicle', async () => {
    // Given.
    const req = {
      body: {
        shortcode: 'abac',
        battery: 17,
        longitude: 45,
        latitude: 45
      },
    };

    const resp = new FakeResponse();

    // When.
    await controller.handle(req as Request, resp as unknown as Response);

    // Then.
    expect(resp.gotStatus).toEqual(200);
  });
});

Vous pouvez maintenant relancer npm run test

 FAIL  src/controller/create.test.ts
  create vehicle controller
    ✕ creates valid vehicle (2 ms)

  ● create vehicle controller › creates valid vehicle

    Invalid create vehicle request

      16 |     const violations = validateRequestPayload(req.body);
      17 |     if (violations.length > 0) {
    > 18 |       throw new AppError(
         |             ^
      19 |         ErrorCode.BadRequest,
      20 |         "Invalid create vehicle request",
      21 |         { violations: violations },

      at CreateVehicleController.handle (src/controller/create.ts:18:13)
      at Object.<anonymous> (src/controller/create.test.ts:70:26)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        2.748 s, estimated 3 s

La méthode handle à jeté une AppError! Notre test mets en évidence notre bug!

🎓 Exercice: Faites les changements nécessaires (dans le code métier) pour faire passer notre test au vert!

🎓 Exercice: Valider la réponse

  • Si l’on fait une requète correcte, le controlleur écrit le status 200 dans l’objet réponse
  • Le controlleur écrit aussi le véhicule créé dans le champ json dans la réponse, il faudrait le valider.
  • 🎓 Complétez le test de façon à ce qu’il vérifie que le contenu répondu corresponde a la requète!
    • 💡Vous pouvez accéder au contenu via resp.gotJson
    • 💡Le mock donnera toujours l’ID 12 au nouveau véhicule
    • 💡Vous pouvez vous aider des assertions jest doc
    • Pour vérifier: penser a faire échouer votre test (en changeant l’ID répondu par le mock par exemple);

✅ Solution: Valider la réponse

expect(resp.gotJson).toEqual({
    vehicle: new Vehicle(
        12,
        'abac',
        17,
        {longitude: 45, latitude: 45},
    )
});

🎓 Exercice: Ecrire le cas négatif!

  • Si l’on commente l’appel à la fonction validation, votre test continuera de passer!
  • Du coup on aimerait avoir un autre test qui prouve que lorsqu’un shortcode trop court / long, une AppError est jetée avec le bon contenu!
  • Quelques indications:
    • 💡Il faut appeler la methode dans un bloc try / catch pour capturer l’exception
    • 💡Il vous faut aussi valider que le type de l’exception est AppError, jest à une assertion pour ça
    • 💡Pour accéder au attributs d’une AppError, il vous faut convertir l’exception avec le mot clé as
      • const myErr = err as AppError
    • 💡La définition de AppError se trouve dans ./src/errors.ts
    • 💡Comment se comporte votre test si aucune erreur n’est jetée?

✅ Solution: Ecrire le cas négatif!

// Describe décrit un groupe logique de tests, ayant la même logique de mise en place et de nettoyage.
describe('create vehicle controller', () => {
  let controller: CreateVehicleController;
  let store: VehicleStore;

  // Avant chaque test on réinitialise le store et le controller.
  beforeEach(() => {
    store =  new VehicleStore({} as Pool); // <- instance mockée!
    controller = new CreateVehicleController(store);
  });

  test('creates a valid vehicle', async () => {
    // Given (mise en place du test).

    // When (exécution de la méthode testée).

    // Then (validation des résultats.
  });

  test('rejects invalid shortcode', async () => {
    // Given.
    expect.assertions(4); // Sets the amount of expected assertions, allows to catch if the function doesn't throw.

    const req = {
      body: {
        shortcode: 'abacab',
        battery: 50,
        longitude: 45,
        latitude: 45
      },
    } as Request;

    // When.
    try {
      await controller.handle(req, {} as Response);
    } catch (err) {
      // Then.
      expect(err).toBeInstanceOf(AppError);

      const typedError = err as AppError;

      expect(typedError.code).toBe(ErrorCode.BadRequest);
      expect(typedError.message).toBe("Invalid create vehicle request");
      expect(typedError.details).toEqual({
        violations: [ "Shortcode must be only 4 characters long" ],
      });
    }
  });
});

Test Unitaire : Pro / Cons

  • ✅ Super rapides (<1s) et légers a exécuter
  • ✅ Pousse à avoir un bon design de code
  • ✅ Efficaces pour tester des cas limites
  • ❌ Environnement “aseptisé” et “bouchonné”, défini par le développeur
  • ❌ “Ossifie” le code

Pensez a exécuter npm lint et a comitter une fois que le lint passe.

Le périmètre testé est-il satisfaisant?

  • La suite de tests qui vient de casser teste la logique de validation de la requête reçue.
  • Est-ce que cela est suffisant pour prouver que la fonctionnalité “créer un véhicule” fonctionne ?
  • Pas exactement, d’autres composants entrent en jeu dans l’environnement réel
    • La couche de communication avec la base de données, le routage HTTP…

Tester des composants indépendamment ne prouve pas que le système fonctionne une fois intégré!

✅ Solution: Tests d’intégration

  • Test validant que l’assemblage de composants se comportent comme prévu.
  • Teste votre application au travers de tous ses composants
  • Par exemple avec vehicle-server:
    • Prouve que GET /vehicles retourne la liste des véhicules les plus proche d’un point donné
    • Prouve que POST /vehicles enregistre un nouveau véhicule en base.

Définition du SUT

Une suite de tests d’intégration doit:

  • Démarrer et provisionner un environnement d’exécution (une DB, Elasticsearch, un autre service…)
  • Démarrer votre application
  • Jouer un scénario de test
  • Éteindre et nettoyer son environnement d’exécution pour garantir l’isolation des tests

➡️ On se place ici d’un point de vue du client de l’application

  • ❌ Ce sont des tests plus lents et plus complexes que des tests unitaires.
  • ⏳Tout tester avec des tests d’intégration n’est pas efficace
  • ➡️ Il faut équilibrer les deux stratégies

On parle de “pyramide des tests”

Source octo.com

Anatomie de notre test d’intégration

Dans notre cas, pour réaliser un test d’intégration il va nous falloir

  1. Démarrer notre serveur de base de données avant d’exécuter notre suite de tests
  2. Provisionner notre schema avant chaque test
  3. Démarer notre application
  4. Envoyer une requête HTTP a notre serveur
  5. Faire des vérifications sur la réponse obtenue (résultat)
  6. Faire des vérificatiosns sur ce qu’on socke en base (effet de bord)
  7. Détruire le schena a la fin du test
  8. Eteindre le serveur de base de données a la fin de l’exécution de la suite de testts

Pour nous simplifier la tấche, on se propose d’utiliser les librairies suivantes:

  • ladjs/supertest abstrait le démarage, la configuration et l’envoi de requêtes HTTP a l’application charge
  • @testcontainers/postgresql qui permet en quelque lignes de démarer et d’arreter un serveur postgresql dans un container Docker.

🎓 Exercice: En suivant la documentation, installez ces dépendances!

Mise en place de notre test d’intégration

/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-misused-promises, @typescript-eslint/no-explicit-any * -- supertest forces us to use any */

import { describe, beforeAll, afterAll, beforeEach, afterEach, test } from '@jest/globals';
import { Express } from 'express';
import { Pool } from 'pg';
import { PostgreSqlContainer, StartedPostgreSqlContainer } from "@testcontainers/postgresql";
import request from "supertest";

import { connectDb, createSchema, dropSchema } from "./database";
import { setupApp } from "./app";
import { Vehicle } from "./model/vehicle";

const postgisImage = 'postgis/postgis:16-3.4-alpine';

jest.setTimeout(30 * 1000);

describe('vehicle server', () => {
  let app: Express;
  let dbConn: Pool;
  let container: StartedPostgreSqlContainer;

  // Avant d'exécuter la suite de tests.
  beforeAll(async () => {
    // On démare le container de base de données.
    container = await new PostgreSqlContainer(postgisImage)
      .start();

    // On établit la connection avec le dit serveur.
    dbConn = await connectDb({
      host: container.getHost(),
      port: container.getPort(),
      database: container.getDatabase(),
      user: container.getUsername(),
      password: container.getPassword(),
    });

    // On crée une instance de notre application!
    app = setupApp(dbConn);
  });

  // Après tous les tests.
  afterAll(async () => {
    // On se déconnecte du serveur de base de données.
    await dbConn.end();
    // On arrête le serveur de base de données.
    await container.stop();
  });

  // Avant chaque test...
  beforeEach(async () => {
    // On crée le schema en base de données.
    await createSchema(dbConn);
  });

  // Après chaque test...
  afterEach(async () => {
    // On détruit le schema en base de données.
    await dropSchema(dbConn);
  });

  test('GET /vehicles', async () => {
    // Given (mise en place du test).

    // When (appel HTTP).

    // Then (validation des résultats).
  });
})

Essayez ensuite de lancer npm run test

Nous avons un AUTRE BUG 😭😱

Lorsque l’on crée un véhicule…

curl \
    -H "Content-Type: application/json" \
    --data '{"shortcode":"abbccc", "battery": 12, "latitude": 53.43, "longitude": 43.43}' \
    localhost:8080/vehicles | jq

Le serveur intervertit la longitude et la latitude 🤦

{
  "vehicle": {
    "id": 1,
    "shortcode": "abbccc",
    "battery": 12,
    "position": {
      "longitude": 53.43, // <- devrait etre 43.43
      "latitude": 43.43   // <- devrait etre 53.43
    }
}

Cela est aussi visible lorque qu’on liste les véhicules

curl localhost:8080/vehicles | jq .
{
  "vehicles": [
    {
      "id": 1,
      "shortcode": "abbccc",
      "battery": 12,
      "position": {
        "longitude": 53.43, // <- devrait etre 43.43
        "latitude": 43.43   // <- devrait etre 53.43
      }
    }
  ]
}
  • Cela proviens certainement d’un bout de code utilisé en commun entre le ListVehiclesController et le CreateVehicleController
  • Nous n’avons rien vu lors de l’écriture des tests unitaires du ListVehiclesController
  • 🎓 Exercice: Avec ces informations, pouvez vous trouver la / les lignes problèmatiques?
  • La fonction newVehicleFromRow mélange la longitude avec la latitude (L90-91)
  • Faites la correction, mais essayons d’écrire un test d’intégration pour que cela ne se reproduise plus!
  • Nous allons écrire un test sur la fonctionnalité “trouver les vehicules les plus proches”
    • GET /vehicles?lat=xxx&long=xxx&limit=10

🎓 Exercice: Complétez le test d’intégration

  • Avec le jeu de données suivant
await dbConn.query(
  `INSERT INTO vehicle_server.vehicles (shortcode, battery, position) VALUES
    ('abcd', 94, ST_GeomFromText('POINT(-71.060316 48.432044)')),
    ('cdef', 20, ST_GeomFromText('POINT(-70.060316 49.432044)')),
    ('ghij', 59, ST_GeomFromText('POINT(-74.060316 49.432044)'));
  `
);

✅ Solution: Complétez le test d’intégration

/* eslint-disable @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-misused-promises, @typescript-eslint/no-explicit-any * -- supertest forces us to use any */

import { describe, beforeAll, afterAll, beforeEach, afterEach, test } from '@jest/globals';
import { Express } from 'express';
import { Pool } from 'pg';
import { PostgreSqlContainer, StartedPostgreSqlContainer } from "@testcontainers/postgresql";
import request from "supertest";

import { connectDb, createSchema, dropSchema } from "./database";
import { setupApp } from "./app";
import { Vehicle } from "./model/vehicle";

const postgisImage = 'postgis/postgis:16-3.4-alpine';

jest.setTimeout(30 * 1000);

describe('vehicle server', () => {
  let app: Express;
  let dbConn: Pool;
  let container: StartedPostgreSqlContainer;

  // Avant d'exécuter la suite de tests.
  beforeAll(async () => {
    // On démare le container de base de données.
    container = await new PostgreSqlContainer(postgisImage)
      .start();

    // On établit la connection avec le dit serveur.
    dbConn = await connectDb({
      host: container.getHost(),
      port: container.getPort(),
      database: container.getDatabase(),
      user: container.getUsername(),
      password: container.getPassword(),
    });

    // On crée une instance de notre application!
    app = setupApp(dbConn);
  });

  // Après tous les tests.
  afterAll(async () => {
    // On se déconnecte du serveur de base de données.
    await dbConn.end();
    // On arrête le serveur de base de données.
    await container.stop();
  });

  // Avant chaque test...
  beforeEach(async () => {
    // On crée le schema en base de données.
    await createSchema(dbConn);
  });

  // Après chaque test...
  afterEach(async () => {
    // On détruit le schema en base de données.
    await dropSchema(dbConn);
  });

  test('GET /vehicles', async () => {
    // Given.
    await dbConn.query(
      `INSERT INTO vehicle_server.vehicles (shortcode, battery, position) VALUES
        ('abcd', 94, ST_GeomFromText('POINT(-71.060316 48.432044)')),
        ('cdef', 20, ST_GeomFromText('POINT(-70.060316 49.432044)')),
        ('ghij', 59, ST_GeomFromText('POINT(-74.060316 49.432044)'));
      `
    );

    // When.
    const response = await request(app).get('/vehicles');

    // Then.
    expect(response.statusCode).toBe(200);
    expect(response.body.vehicles.map((v: Vehicle) => v.shortcode)).toEqual(['cdef', 'abcd', 'ghij']);
    expect(response.body.vehicles.map((v: Vehicle) => v.position)).toEqual([
      {longitude: -70.060316, latitude: 49.432044},
      {longitude: -71.060316, latitude: 48.432044},
      {longitude: -74.060316, latitude: 49.432044},
    ]);
  });
})
  • N’oubliez pas de vérifier ce que dit npm run lint et de corriger les problèmes reportés!
  • Une fois que le lint est au vert, n’oubliez pas de créer un commit!

🎓 Exercice: Activez les tests dans votre CI et créez vous une PR

  • Rajoutez un commit sur votre branch qui change votre workflow de ci pour qu’à chaque build npm run test soit exécuté après le lint!
  • Ensuite créez vous une PR avec votre branche!
  • Vous devriez voir votre job de CI vert et vos tests exécutés

✅ Solution: Activez les tests dans votre CI

name: Vehicle Server CI
on:
  - push
  - pull_request
jobs:
  ci:
    runs-on: ubuntu-24.04
    steps:
      - name: Checkout Code
        uses: actions/checkout@v5
      - name: Setup Node
      - uses: actions/setup-node@v6
        with:
          node-version: '22.x'
      - name: Check Node Version
        run: node --version
      - name: Build application
        run: npm run build
      - name: Run Lint
        run: npm run lint
      - name: Run Tests
        run: npm run test
      - name: List dist output
        run: ls dist/

Checkpoint 🎯

Nous avons maintenant un moyen systèmatique de vérifier que la logique de notre application est correcte!

  • ❌ Ce n’est pas gratuit, il existe différentes stratégies de tests avec chacunes leurs avantages et inconvenients…
    • Test unitaires, faciles a écrire, rapides et précis
    • Tests d’intégration, plus lourds et complexes, mais capable de tester l’intégralié de l’application
  • ⚖️ … et la nécessité d’avoir une stratégie équilibrée: la pyramide des tests!

🎉 Vous pouvez merger votre PR!

Rappels et remise à niveau

Quel est le problème résolu par docker?

Problème de temps exponentiel

Déjà vu ?

L’IT n’est pas la seule industrie à résoudre des problèmes…

✅ Solution: Le conteneur intermodal

“Separation of Concerns”

Comment ça marche ?

“Virtualisation Légère

Conteneur != VM

“Separation of concerns”: 1 “tâche” par conteneur

VMs && Conteneurs

Non exclusifs mutuellement

🎓 Exercice : où est mon conteneur ?

  • Retournez dans Gitpod
  • Dans un terminal, exécutez les commandes suivantes :
# Affichez la liste de tous les conteneurs en fonctionnement (aucun)
docker container ls

# Exécutez un conteneur
docker container run hello-world # Equivalent de l'ancienne commande 'docker run'


docker container ls
docker container ls --all
# Quelles différences ?

🩻 Anatomie

  • Un service “Docker Engine” tourne en tâche de fond et publie une API REST
  • La commande docker run ... a envoyé une requête POST au service
  • Le service a télécharge une Image Docker depuis le registre DockerHub,
  • Puis a exécuté un conteneur basé sur cette image

✅ Solution : Où est mon conteneur ?

Le conteneur est toujours présent dans le “Docker Engine” même en étant arrêté

CONTAINER ID   IMAGE         COMMAND    CREATED          STATUS                      PORTS     NAMES
109a9cdd3ec8   hello-world   "/hello"   33 seconds ago   Exited (0) 17 seconds ago             festive_faraday
  • Un conteneur == une commande “conteneurisée”

    • cf. colonne “COMMAND
  • Quand la commande s’arrête : le conteneur s’arrête

    • cf. code de sortie dans la colonne “STATUS

🎓 Exercice : Cycle de vie d’un conteneur en tâche de fond

  • Lancez un nouveau conteneur en tâche de fond, nommé webserver-1 et basé sur l’image nginx

  • Affichez les “logs” du conteneur (==traces d’exécution écrites sur le stdout + stderr de la commande conteneurisée)

  • Comparez les versions de Linux de Gitpod et du conteneur

    • Regardez le contenu du fichier /etc/os-release
    • 💡 docker container exec

✅ Solution : Cycle de vie d’un conteneur en tâche de fond

docker container run --detach --name=webserver-1 nginx
# <ID du conteneur>

docker container ls

docker container logs webserver-1

cat /etc/os-release
# ... Ubuntu ...
docker container exec webserver-1 cat /etc/os-release
# ... Debian ...

🤔 Comment accéder au serveur web en tâche de fond ?

$ docker container ls
CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS         PORTS     NAMES
ee5b70fa72c3   nginx     "/docker-entrypoint.…"   3 seconds ago   Up 2 seconds   80/tcp    webserver-1
  • ✅ Super, le port 80 (TCP) est annoncé (on parle d’“exposé”)…
  • ❌ … mais c’est sur une adresse IP privée
docker container inspect \
  --format='{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \
  webserver-1

🎓 Exercice : Accéder au serveur web via un port publié

  • But : Créez un nouveau conteneur webserver-public accessible publiquement

  • Utilisez le port 8080 publique

  • 💡 Flag --publish pour docker container run

  • 💡 GitPod va vous proposer un popup : choisissez “Open Browser”

✅ Solution : Accéder au serveur web via un port publié

docker container run --detach --name=webserver-public --publish 8080:80 nginx
# ... container ID ...

docker container ls
# Le port 8080 de 0.0.0.0 est mappé sur le 80 du conteneur

curl http://localhost:8080
# ...

🤔 D’où vient “hello-world” ?

  • Docker Hub : C’est le registre d’images “par défaut”

    • Exemple : Image officielle de nginx
  • 🎓 Cherchez l’image hello-world pour en voir la page de documentation

    • 💡 pas besoin de créer de compte pour ça
  • Il existe d’autre “registres” en fonction des besoins (GitHub GHCR, Google Artifact Registry, etc.)

🤔 Que contient “hello-world” ?

  • C’est une “image” de conteneur, c’est à dire un modèle (template) représentant une application auto-suffisante.

    • On peut voir ça comme un “paquetage” autonome
  • C’est un système de fichier complet:

    • Il y a au moins une racine /
    • Ne contient que ce qui est censé être nécessaire (dépendances, librairies, binaires, etc.)

🤔 Pourquoi des images ?

  • Un conteneur est toujours exécuté depuis une image.
  • Une image de conteneur (ou “Image Docker”) est un modèle (“template”) d’application auto-suffisant.

=> Permet de fournir un livrable portable (ou presque).

🤔 Application Auto-Suffisante ?

C’est quoi le principe ?

🤔 Pourquoi fabriquer sa propre image ?

Essayez ces commandes dans Gitpod :

cat /etc/os-release
# ...
git --version
# ...

# Même version de Linux que dans GitPod
docker container run --rm ubuntu:20.04 git --version
# docker: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "git": executable file not found in $PATH: unknown.

# En interactif ?
docker container run --rm --tty --interactive ubuntu:20.04 git --version

=> Problème : git n’est même pas présent !

🎓 Fabriquer sa première image

  • But : fabriquer une image Docker qui contient git
  • Dans votre workspace Gitpod, créez un nouveau dossier /workspace/docker-git/
  • Dans ce dossier, créer un fichier Dockerfile avec le contenu ci-dessous :
FROM ubuntu:20.04
RUN apt-get update && apt-get install --yes --no-install-recommends git
  • Fabriquez votre image avec la commande docker image build --tag=docker-git <chemin/vers/docker-git/
  • Testez l’image fraîchement fabriquée
    • 💡 docker image ls

✅ Fabriquer sa première image

mkdir -p /workspace/docker-git/ && cd /workspace/docker-git/

cat <<EOF >Dockerfile
FROM ubuntu:20.04
RUN apt-get update && apt-get install --yes --no-install-recommends git
EOF

docker image build --tag=docker-git ./

docker image ls | grep docker-git

# Doit fonctionner
docker container run --rm docker-git:latest git --version

Conventions de nommage des images

[REGISTRY/][NAMESPACE/]NAME[:TAG|@DIGEST]
  • Pas de Registre ? Défaut: registry.docker.com
  • Pas de Namespace ? Défaut: library
  • Pas de tag ? Valeur par défaut: latest
    • ⚠️ Friends don’t let friends use latest
  • Digest: signature unique basée sur le contenu

Conventions de nommage : Exemples

  • ubuntu:20.04 => registry.docker.com/library/ubuntu:20.04
  • dduportal/docker-asciidoctor => registry.docker.com/dduportal/docker-asciidoctor:latest
  • ghcr.io/dduportal/docker-asciidoctor:1.3.2@sha256:xxxx

🎓 Utilisons les tags

  • Il est temps de “taguer” votre première image !
docker image tag docker-git:latest docker-git:1.0.0
  • Testez le fonctionnement avec le nouveau tag
  • Comparez les 2 images dans la sortie de docker image ls

✅ Utilisons les tags

docker image tag docker-git:latest docker-git:1.0.0

# 2 lignes
docker image ls | grep docker-git
# 1 ligne
docker image ls | grep docker-git | grep latest
# 1 ligne
docker image ls | grep docker-git | grep '1.0.0'

# Doit fonctionner
docker container run --rm docker-git:1.0.0 git --version

🎓 Mettre à jour votre image (1.1.0)

  • Mettez à jour votre image en version 1.1.0 avec les changements suivants :

    • Ajoutez un LABEL dont la clef est description (et la valeur de votre choix)
    • Configurez git pour utiliser une branche main par défaut au lieu de master (commande git config --global init.defaultBranch main)
  • Indices :

✅ Mettre à jour votre image (1.1.0)

cat ./Dockerfile
FROM ubuntu:20.04
RUN apt-get update && apt-get install --yes --no-install-recommends git
LABEL description="Une image contenant git préconfiguré"
RUN git config --global init.defaultBranch main

docker image build -t docker-git:1.1.0 ./docker-git/
# Sending build context to Docker daemon  2.048kB
# Step 1/4 : FROM ubuntu:20.04
#  ---> e40cf56b4be3
# Step 2/4 : RUN apt-get update && apt-get install --yes --no-install-recommends git
#  ---> Using cache
#  ---> 926b8d87f128
# Step 3/4 : LABEL description="Une image contenant git préconfiguré"
#  ---> Running in 0695fc62ecc8
# Removing intermediate container 0695fc62ecc8
#  ---> 68c7d4fb8c88
# Step 4/4 : RUN git config --global init.defaultBranch main
#  ---> Running in 7fb54ecf4070
# Removing intermediate container 7fb54ecf4070
#  ---> 2858ff394edb
Successfully built 2858ff394edb
Successfully tagged docker-git:1.1.0

docker container run --rm docker-git:1.0.0 git config --get init.defaultBranch
docker container run --rm docker-git:1.1.0 git config --get init.defaultBranch
# main

Bon… pourquoi je vous embête avec tout ça?

Docker est un moyen idéal de livrer notre application!

  • Notre application à besoin:
    • De NodeJS en version 20.x
    • De nos dépendances de production
    • Du code généré pour s’exécuter
  • Docker nous permet de packager tout ça en une seule image!
  • C’est le “““standard””” du moment

🎓 Construire une Image du Vehicle Server

  • A partir de l’image de base NodeJS une image du vehicle-server
  • Il vous faut copier les sources avec l’instruction COPY
  • Compiler le serveur
  • Faire en sorte que le point d’entrée de l’image soit le serveur (en utilisant ENTRYPOINT )
  • L’image doit être utilisable avec la commande suivante:
  • Vous pouvez obtenir l’IP de votre container de database (une fois lancé) en utilisant docker container inspect.
docker run \
    --tty
    --interactive
    --rm \
    --env PORT=8080 \
    --env DB_PORT=5432 \
    --env DB_HOST="${DATABASE_SERVER_IP}" \
    --publish 8080:8080 \
    image:tag

✅ Construire une Image du Vehicle Server

FROM node:20-alpine3.19
COPY . /app
WORKDIR /app
RUN npm install && \
  npm run build

ENTRYPOINT ["node", "/app/dist/index.js"]

Qu’avons nous construit?

  • On part d’une image de base avec NodeJS
  • On copie l’intégralité de nos sources dedans
  • On Installe nos dépendances
  • On compile le code Typescript

Est’ce que c’est efficace?

  • L’image finale fait ~220Mb
  • Mais on embarque toutes nos dépendances et nos fichiers de configurations!
  • On peut faire plus léger en utilisant
    • Le multi stage build de Docker qui nous permets de faire plusieurs étapes dans la création de notre image!
    • Les fonctionalité de npm pour installer seulement les dépendances de production!

Multi-Stage build kesako?

  • Cela permet d’avoir plusieurs instructions FROM dans un Dockerfile, qui vont créer plusieurs images intermédiaires
  • On peut nommer nos étapes en utilisat FROM xxx AS <stage name>
  • On peut récupérer des fichiers d’une étape précédente en utilisant COPY --from <stage name> /path1 /path
  • Seule la dernière étape du Dockerfile est taggée!

🎓 Optimiser l’Image du Vehicle Server

En partant de votre Dockerfile, creez un build en deux étapes:

  • 1.: Une étape qui transpile l’application Typescript vers Javascript
  • 2.: Une étape qui installe les dépendances de production uniquement et qui copie le code JS généré depuis l’étape de build.

✅ Optimiser l’Image du Vehicle Server

FROM node:20-alpine3.19 AS build
COPY . /app
WORKDIR /app
RUN npm install && \
  npm run build

FROM node:20-alpine3.19 AS runtime
RUN mkdir -p /app
WORKDIR /app
COPY ./package.json ./package-lock.json .
RUN npm install --production
COPY --from=build /app/dist /app/dist

ENTRYPOINT ["node", "/app/dist/index.js"]

Quelle est la taille de l’image?

Checkpoint 🎯

  • Une image Docker fournit un environnement de système de fichier auto-suffisant (application, dépendances, binaires, etc.) comme modèle de base d’un conteneur
  • On peut spécifier une recette de fabrication d’image à l’aide d’un Dockerfile et de la commande docker image build
  • Les images Docker ont une convention de nommage permettant d’identifier les images très précisément
  • Nous avons maintenant une image Docker pour distribuer notre serveur!

Une fois que votre image fonctionne, vous pouvez ouvrir une PR avec ce Dockerifle et la merger

Livraison

Nous sommes prêts, il est grand temps de livrer notre v1.0.

…mais c’est quoi l’objectif de notre livraison déjà?

🏝️ Notre livraison sera…

  • Une image Docker de l’application…
  • … visible sur le Docker Hub
  • … avec un (Docker) tag pour chaque version

🎓 🐳 Docker Hub

  • Si vous n’avez pas déjà un compte sur le Docker Hub , créez-en un maintenant (nécessite une validation)
  • Une fois authentifiés, naviguez dans votre compte (en haut à droite, “My Account”)
  • Allez dans la section “Security” et créez un nouvel “Access Token”
    • Permissions: “Read & Write” (pas besoin de “Delete”)
    • ⚠️ Conservez ce token dans un endroit sûr (ne PAS partagez à d’autres)

💡 Activer le 2FA est une bonne idée également

🎓 “Taguez” et déployez la version 1.0.0

  • Depuis GitPod, créez un tag git local 1.0.0

    • 💡 git tag 1.0.0 -a -m "Première release 1.0.0, mode manuel"
  • Fabriquez l’image Docker avec le tag (Docker) 1.0.0

    • 💡 npm run package ?
      • npm accepte les variables d’environment!
  • Publiez l’image sur le DockerHub

    • 💡 Vous devez vous authentifier avec docker login
    • 💡 docker image push
      • 💡 Peutêtre en faire un npm run publish ?
  • Publier le tag sur votre “remote” origin.

    • 💡 git push origin 1.0.0

🎓 “Taguez” et déployez la version 1.0.0

On ajoute les nouveau scripts npm

{
    "scripts": {
        "package": "docker build -t jlevesy/vehicle-server:${TAG}",
        "publish": "docker image push jlevesy/vehicle-server:${TAG}",
        "release": "npm run package && npm run publish"
    },
}

Ensuite pour publier notre version

docker login --username=<VOTRE USERNAME>

git tag 1.0.0 -a -m "Première release 1.0.0, mode manuel"
git push origin 1.0.0
TAG=1.0.0 npm run release

Nous avons déployé manuellement notre première image Docker, avec synchronisation historique git <-> image Docker

=> 🤔 C’était très manuel. Et si on regardait à automatiser tout ça ?

“Continuous Everything”

Livraison Continue

Continuous Delivery (CD)

🤔 Pourquoi la Livraison Continue ?

  • Diminuer les risque liés au déploiement
  • Permettre de récolter des retours utilisateurs plus souvent
  • Rendre l’avancement visible par tous

Qu’est ce que la Livraison Continue ?

  • Suite logique de l’intégration continue:
    • Chaque changement est potentiellement déployable en production
    • Le déploiement peut donc être effectué à tout moment

Your team prioritizes keeping the software deployable over working on new features

-Martin Fowler-

La livraison continue est l’exercice de mettre à disposition automatiquement le produit logiciel pour qu’il soit prêt à être déployé à tout moment.

Livraison Continue avec GitHub Actions

Prérequis: exécution conditionnelle des jobs

Il est possible d’exécuter conditionnellement un job ou un step à l’aide du mot clé if (documentation )

jobs:
  release:
    steps:
      # Lance le step dire coucou uniquement si la branche est main.
      - name: "Dire Coucou"
        run: echo "coucou"
        if: contains('refs/heads/main', github.ref)

🎓 Secret GitHub / DockerHub Token

🎓 Livraison Continue sur le DockerHub

  • But : Automatiser le déploiement de l’image dans le DockerHub lorsqu’un tag est poussé

  • Changez votre workflow de CI de façon à ce que, sur un push de tag, les tâches suivantes soient effectuées :

    • Comme avant: Build, Tests, Package
    • Si c’est un tag, alors il faut créer et pousser l’image sur le DockerHub avec npm run release
  • 💡 Utilisez les GitHub Action suivantes :

  • 💡 Il vous faut aussi trouver la condition a appliquer pour exécuter une étape uniquement sur un push de tag

  • 💡 Ainsi que trouver le tag courant depuis le workflows

✅ Livraison Continue sur le DockerHub

# ...
    steps:
      # ... npm run lint
      # ... npm run test
      # ... npm run build
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        if: startsWith(github.ref, 'refs/tags/')
        with:
          username: xxxxx
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Push if triggered by a tag
        if: startsWith(github.ref, 'refs/tags/')
        run: |
          TAG="${{github.ref_name}}" npm run release

Déploiement Continu

🇬🇧 Continuous Deployment / “CD”

🤔 Qu’est ce que le Déploiement Continu ?

  • Version “avancée” de la livraison continue:
    • Chaque changement est déployé en production, de manière automatique

Continuous Delivery VS Deployment

Source : http://blog.crisp.se/2013/02/05/yassalsundman/continuous-delivery-vs-continuous-deployment

Bénéfices du Déploiement Continu

  • Rends triviale les procédures de mise en production et de rollback
    • Encourage à mettre en production le plus souvent possible
    • Encourage à faire des mises en production incrémentales
  • Limite les risques d’erreur lors de la mise en production
  • Fonctionne de 1 à 1000 serveurs et plus encore…

🎓 Déploiement Continu sur le DockerHub

  • But : Déployer votre image vehicle-server continuellement sur le DockerHub

  • Changez votre workflow de CI de façon à ce que, sur un push sur la branch main, les tâches suivantes soient effectuées :

    • Comme avant: on joue le cycle de vie via make.
    • SI c’est la branche main, alors il faut pousser l’image avec le tag main sur le DockerHub
    • Conservez les autre cas avec les tags

🎓 Déploiement Continu sur le DockerHub

# ...
    steps:
      # ... npm run build
      # ... npm run lint
      # ... npm run test
      # ... Tag release!
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        if: contains('refs/heads/main', github.ref)
        with:
          username: xxxxx
          password: ${{ secrets.DOCKERHUB_TOKEN }}
      - name: Push if on `main` branch
        if: contains('refs/heads/main', github.ref)
        run: |
          TAG="${{github.ref_name}}" npm run release

Checkpoint 🎯

  • La livraison continue et le déploiement continu étendent les concepts du CI
  • Les 2 sont automatisées, mais un être humain est nécessaire comme déclencheur pour la 1ère
  • Le choix dépends des risques et de la “production”
  • On a vu comment automatiser le déploiement dans GitHub Actions
    • Conditions dans le workflow
    • Gestion de secrets

Évaluation du Module

  • L’évaluation pour le cours de CI/CD sera un projet dédié
  • But: Ecrire une CLI qui communique avec le vehicle-server pour
    • 1.: Créér un véhicule
    • 2.: Lister les véhicules créés
  • L’addresse du serveur doit être configurable

Exemple d’utilisation

# Créé un véhicle
$ vehicle-cli --address=localhost:8080 create-vehicle --shortcode=abcd --battery=12 --longitude=20.0 --latitude=30.0`
Created vehicule `abcd`, with ID `34`

# Affiche les erreurs rapportées par le serveur.
$ vehicle-cli --address=localhost:8080 create-vehicle --shortcode=abcdef --battery=12 --longitude=20.0 --latitude=30.0`
Could not create the vehicle
- Shortcode must be only 4 charactes long

# Liste les véhicules
vehicle-cli --address=localhost:8080  list-vehicles
# Affiche la liste des véhicules répondu par le serveur... format libre!
  • Vous avez le choix du langage et du format du livrable
    • Une image Docker c’est bien!
    • Un binaire aussi, ca sera testé sous Linux / x86_64.
  • L’utilisation d’un framework pour faire des outils en CLI est recommandée
  • Attention avec C++, je ne suis pas sur qu’il y aie des outils de gestion de dépendance.

Barème (indicatif):

  • Livrable (/3):
    • Est-ce que vous avez un livrable utilisable? (Par exemple une image Docker ou un binaire)
    • Est-ce que ce livrable est associé clairement a un tag et donc un commit dans Git?
      • Dans le cas d’une image Docker, le tag de l’image doit correspondre au tag git.
    • Est-ce que le livrable est généré automatiquement quand un tag git est poussé?
  • Fonctionalités (/2)
    • Est-ce que je peux lister les véhicules créés?
    • Est-ce que je peux créér un véhicule?
    • Est-ce que les messages d’erreur du serveur sont bien affichés?
  • Utilisation de Git et GitHub (/5)
    • Est-ce que le Git Flow est clair?
    • Est-ce qu’il y a des PRs? Bien Documentées? De la revue de code?
    • Est-ce qu’il y à des tags dans l’historique ?
  • Gestion des Dépendances (/3)
    • Utilisez vous un outil de gestion de dépendances?
    • Est-ce que vos dépendances sont récupérées de façon reproductible?
  • Intégration Continue (/5)
    • Utilisez vous un outil de CI?
    • Le workflow de CI est il exécuté sur les PR?
    • Le workflow joue le lint, les tests, et le build
  • Tests (/2)
    • Avez vous une suite de tests automatisée? Que prouve t’elle?
      • Si trop compliquée a mettre en place / pas le temps, écrivez un paragraphe dans le README décrivant comment vous vous y prendriez :)

BONUS TRACK

Git + + + + +

  • git rebase (avec -i ❤️)
  • git commit –amend
  • git add -p
  • git revert
  • git reflog
  • OH SHIT GIT

Dependabot!

  • Ajoutez la mise à jour Automatique des Dépendances avec Dépendabot Docs

Docker Compose

  • Mettez en place un fichier docker compose qui lance votre serveur et sa base de données!

AMA (Ask Me About)

  • Infrastructure & Cloud?
  • Monitoring et Observabilité?
  • Open Source?
  • Career ? Remote Work?
  • …?