


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.
GitPod.io : Environnement de développement dans le ☁️ “nuage”
VS Code Browser comme éditeur⚠️ Ne vous authentifiez pas sur Gitpod Flex (https://app.gitpod.io) ⚠️
user:emailpublic_repoworkflowCliquez sur le bouton ci-dessous pour démarrer un environnement GitPod personnalisé:
Après quelques secondes (minutes?), vous avez accès à l’environnement:
Vous devriez pouvoir taper la commande whoami dans le terminal de GitPod:
gitpodVous devriez pouvoir fermer le fichier “Get Started”…
.gitpod.ymlOn peut commencer !
Remise à niveau / Rappels

C’est un programme qui accepte une commande en entrée et retourne un resultat.
Nous allons utiliser bash
ls --color=always -l /bin
ls) : c’est la commande- sont des “options” et/ou drapeaux (“flags”)/bin)man <commande> # Commande 'man' avec comme argument le nom de ladite commande
/ puis une chaîne de texte pour cherchern pour sauter d’occurrence en occurrenceq pour quitter🎓 Essayez avec ls, cherchez le mot color
--help), un flag (-h) ou un argument (help)Dans un terminal Unix/Linux/WSL :
CTRL + C : Annuler le processus ou prompt en coursCTRL + L : Nettoyer le terminalCTRL + A : Positionner le curseur au début de la ligneCTRL + E : Positionner le curseur à la fin de la ligneCTRL + R + mot clé : Rechercher dans l’historique de commandesTab: Compléter la commande🎓 Essayez-les ! (Notamment CTRL + R et Tab)
pwd : Afficher le répertoire courant-P ?ls : Lister le contenu du répertoire courant-a et -l ?cd : Changer de répertoirecat : Afficher le contenu d’un fichiermkdir : créer un répertoire-p ?echo : Afficher un (des) message(s)rm : Supprimer un fichier ou dossiertouch : Créer un fichiergrep : Chercher un motif de textecode : Ouvre le fichier dans l’éditeur de texte
/ls -l //ls -l /usr/bin/ (Ex. /usr/bin)./bin ou local/bin/).ls -l ./bin # Dans le dossier /usr..ls -l ../ # Dans le dossier /usrcd (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 courantls -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
# On déclare et initialise une variable
MA_VARIABLE="Salut tout le monde"
# On l'évalue avec avec le caractère `$`
echo "${MA_VARIABLE}"
echo ">> Contenu de /tmp :\n$(ls /tmp)"
if, des for et plein d’autres trucs (doc)$? :ls /tmp
echo $?
ls /do_not_exist
echo $?
# Une seconde fois. Que se passe-t'il ?
echo $?

stdin (fd=0)stdout (fd=1)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
Le caractère “pipe” | permet de chaîner des commandes
Exemple : Afficher les fichiers/dossiers contenant le lettre d dans le dossier /bin :
ls -l /bin
ls -l /bin | grep "d" --color=auto
🤔 Comment l’interpréteur retrouve quel fichier exécuter a partir d’un simple nom?
$PATH liste les dossiers dans lesquels chercher les binairesecho "${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
Nous avons vu:
bashCTRL+R et TAB)
🧐 Que se passe-t-il quand je tape google.com dans mon navigateur et que j’appuie sur entrée?

Une requête est composée des champs suivant:
GET, POST, PUT, DELETE, HEAD, OPTIONS…)github.com)/assets/file.js)/pages/node?utm_source=facebook)Accepted-Content, User-Agent,Accept, Referrer, Authorization, Cookies)Une réponse est composée des champs suivant:
Content-Length, Content-Encoding,Content-Type …)man curlcurl --verbose --location --output /dev/null voi.com
--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 standardRegardons 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]
HTTPS upgradeHTTPS de façon sécurisée!--location, que se passe-t-il?--output /dev/null, que se passe-t-il?--request POST, --request DELETE--header "Content-Type: application/json"--data '{"some":"json"}'--data '@some/local/file'| (pipe) et la commande jqcurl https://swapi.dev/api/planets/1
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
cURL est un outil très complet pour parler HTTP depuis un terminal!avec un VCS : 🇬🇧 Version Control System
également connu sous le nom de SCM (🇬🇧 Source Code Management)
Pourquoi encore un outil ?




projet-vcs-1 dans le répertoire /workspace, puis positionnez-vous dans ce dossiermkdir -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
.git/ ?git status ?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"
.git
README.md dedans avec un titre et vos nom et prénomsgit status ?git add (...)git status ?README.md avec un message, à l’aide de la commande git commit -m <message>git status ?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"
diff: un ensemble de lignes “changées” sur un fichier donné

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

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

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


README.md et afficher le diffREADME.mdgit 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

feature/htmlindex.html sur cette branchegit log --graphgit 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!

feature/html dans la branche principale--no-ff (no fast forward) pour forcer git a créer un commit de merge.git log --graphgit switch main
git merge --no-ff feature/html # Enregistrer puis fermer le fichier 'MERGE_MSG' qui a été ouvert
git log --graph
# git lg
“Infrastructure as Code” :
Code Civil:
On a vu :
git et sa nomenclature de base (diff, changest, commit, branch)

Nous avons besoin d’un interpréteur pour exécuter notre code Javascript
workspace créez un répertoire helloworldindex.js avec le contenu suivantconsole.info("Hello World");
node ./index.js# 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”
node ./monprograme.jsnode🎓 Quel est le résultat de l’opération ((38 + 44) / 12) - 1
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);
});
import 'dns': Importe le module dns de la librairie standard nodedns.resolve: Appelle la fonction resolve du module DNS en passant deux arguments.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
Boolean: true ou falseNumber: 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 nullUndefined: absence de valeur, une seule valeur possible undefinedD’autres types existent … Bigint, Symbol…
null vs undefinedundefined signifie qu’une variable a été déclarée mais n’a pas reçu de valeur.null signifie l’absence d’objetlet foo;
console.log("FOO", foo) // <- undefined
let bar = null;
console.log("BAR", null) // <- null
let foo = 4;
const bar = 12;
foo = 56;
bar = 67;
🎓 Le script suivant s’exécute t’il?
var et let/constUne 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.constlet.block.const et let est le “block”{ // block anonyme
const maVariable = 12;
};
console.log(maVariable);
🎓 Le script suivant s’exécute t’il?
// if / else
if (condition1) {
statement1;
} else {
statement2;
}
// switch case
const name = "julien"
switch (name) {
case "michel":
//...
case "julien":
console.log("bonjour");
default:
// ...
}
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!
a == b egalité, tente de faire des conversion de types implicitesa === b égalité stricte, ne fait pas de conversions impliciteslet a = 2;
let b = '2';
a == b; // true
a === b; // false, a est du type number, b est du type string
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 .
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
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));
On distigue deux types:
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));
Soit le tableau suivant:
const arr = [18, 4, 99, 1203, 5, 3, 5566, 22, 12];
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?
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
classe, comme d’autres langages orientés objetclass 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!
thisthis est une référence vers l’objet courant dans une méthodethis pointe vers “l’objet global”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);
}
Javascript représente une erreur a l’aide d’exceptions:
throwtry..catch..finallyfunction 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!");
}
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
curl suivante reçoive en réponsecurl "localhost:3000?name=julien"
Hello julien
createServer module HTTP de node doclisten docconst reqUrl = new URL("http://localhost"+req.url)reqUrl.searchParams.get("name")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...");
});
CommonJS: Venant de l’ecosystème NodeJSMJS: Standardisé par ECMA// 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()
// 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();
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!Server.listen est une opération asynchronenodejs 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!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:
promesse.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);
});
fetchFetch est une API native de Javascript qui permets de récupérer une resource en effectuant une requète HTTP doc
fetch le contenu à l’URL suivantehttps://swapi.dev/api/planets/1
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 🍝🍝🍝🍝🍝🍝
asyncawait 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();
fetch avec Async / AwaitRéécrivez votre programme qui donne la population de tatooine en utilisant Async / Await
fetch avec Async / Awaitasync 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();

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 🤯 🤯 🤯
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)
node exécute du Javascript, pas du Typescripttsc (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'.
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!
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'.
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 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 = {};
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'
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}`);
}
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!
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);
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"];
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
};
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}`);
}
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}`);
}
Nous avons vu comment utiliser Typescript
Nous avons vu rapidement quelques primitives de bases du langage
Pour aller plus loin c’est par ici !
Un cas d’utilisation majeur est de permettre aux utilisateurs de trouver un véhicule proche d’eux facilement.

vehicle-server qui doit supporter les fonctionnalités suivantes: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.
node_modules nous est fourni tel quel, aucun moyen de le reconstruire.dist…mais on ne sait pas le générer!dist, node_modules et l’archive.gitignore qui vous évitera de comitter dist/ et node_modules!# 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/
Vous avez récupéré un projet typescript qui semble fonctionner…
Application des chapitres précédents : vous avez initialisé un projet git local
On a du code. C’est un bon début. MAIS:
Nous souhaitons que notre livrable soit:
build: Compilation de l’applicationlint: Analyse statique de code pour détecter des problèmes ou risquestest: Exécution de la suite de tests automatiséespackage: Création du livrablerelease: Livraison du livrableNotre première étape va etre de faire en sorte de pouvoir lancer le serveur dans notre environment de développement.
Cela sigifie:
tsc)
Mais le pire, c’est que c’est un problème récursif! Nos dépendances ont aussi des dépendances!




package.jsonnpx <script>/workspace/vehicle-servernpm initpackage.jsonpaquet npmnpm distingue quatre types de dépendances:
--save-dev)--save-peers)--save-optional)⚠️ Selon où vous utilisez npm, vous n’avez pas besoin de toutes les dépendances ⚠️
node_modulesnode_modules est un problème 😭npm ls permets de lister les dépendances d’un projet (--all affiche tout l’arbre de dépendance!)npm install --save-dev typescript-D ou --save-dev: Typescript n’est pas utile a l´exécution!devDependencies est ajoutée au package.jsontypescript est listé avec la version ^5.6.3package-lock.json est aussi généré!node_modules est créé content le code du package 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. */
}
}
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
Mais tout ça dépends du bon vouloir et de la rigeur des mainteneurs 😅
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 majeure5.6.4, 5.9.10, 5.48.4~ : Toutes les versions ayant la meme version mineure5.6.4, 5.6.5, 5.6.6>, >= : Toutes les version superieures a la version indiquée5.6.4, 6.4.3 etc…5.6.4: Un range d’une valeur unique… on fixe la versionnpm 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
package.jsonnode_modulesVous 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?

package-lock.jsonversion exacte utiliséeresolved)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?
@types/xxxx)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!
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 qui permets de détecter (voir même de corriger automatiquement) des problèmes de sécurité sur l’arbre de dépendances!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
package-lock.json n’est pas reproductible!Votre dépôt est actuellement sur votre ordinateur.

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

Cela rends la manipulation un peu plus complexe, allons-y pas à pas :-)
Dans votre workspace
# Liste tous les commits présent sur la branche main.
git log
Git permet de manipuler des “remotes”
# 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
# git push <remote> <votre_branche_courante>
git push origin main
git a envoyé la branche main sur le remote origingit a créé localement une branche distante origin/main qui suis l’état de main sur le remote.git commit --allow-empty -m "Yet another commit"
git push origin main
git branch -agit 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!
GitHub crée directement un commit sur la branche main sur le dépôt distant
# 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 !
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
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.
Continuous Integration doesn’t get rid of bugs
But it does make them dramatically easier to find and remove.
Martin Fowler
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
Objectif : que l’intégration de code soit un non-évènement

GitHub Actions est un moteur de CI/CD intégré à GitHub
Une Step (étape) est une tâche individuelle à faire effectuer par le CI :
runusessteps: # 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 }}
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}}"
Un Job est un groupe logique de steps :
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'
# ...
Un Runner est un serveur distant sur lequel s’exécute un job.
runs-on dans la définition d’un jobUn Workflow est une procédure automatisée composée de plusieurs jobs, décrite par un fichier YAML.
.github/workflows/<nom du workflow>.yml.github/workflows
├── ci-cd.yaml
├── bump-dependency.yml
└── nightly-tests.yaml
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.
Un workflow spécifie le(s) évènement(s) qui déclenche(nt) son exécution
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
But : nous allons créer notre premier workflow dans GitHub Actions
N’hésitez pas à utiliser la documentation de GitHub Actions:
main,.github/workflows/bonjour.yml avec le contenu suivant :name: Bonjour
on:
- push
jobs:
dire_bonjour:
runs-on: ubuntu-24.04
steps:
- run: echo "Bonjour 👋"
echo ?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 ❌ )
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 :
cat README.md” doit être conservée et doit fonctionnername: 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
README.mdcat README.md | cowsay dans GitPodcat README.md” du workflow pour faire la même chose dans GitHub Actionscowsay n’est pas disponible dans le runner GitHub Actions)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 :
cat README.md | cowsay dans le workflow comme dans GitPodcowsay dans le runner GitHub (runs-on, paquet cowsay dans Ubuntu 22.04name: 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
cat README.md | cowsay dans le workflow comme dans GitPodname: 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

But : s’assurer que GitHub actions install et utilise cowsay le plus efficacement possible
C’est à vous de mettre à jour le workflow pour:
README.md dans un “output” (une variable temporaire de GitHub Actions)💡 Utilisez les GitHub Actions et documentations suivantes :
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 }}
👷🏽♀️ C’est à vous de modifier le projet “vehicle-server” pour faire l’intégration continue!
distPensez à supprimer/renommer le workflow bonjour.yaml
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/
main du Vehicle Server,=> On peut modifier notre code avec plus de confiance !

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

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
staging dans main est un évenement important qui doit figurer dans l’historique de notre codegit tag 1.0.0 -a -m "Première release 1.0.0"
Où sont stockées ces branches ?
Tous les développeurs envoient leur commits et branches sur le même remote
La motivation: le contrôle d’accès
C’est le modèle poussé par GitHub !
Dans la terminologie GitHub:

A vous de jouer: Corrigez la fonctionnalité “suppression d’un véhicule” dans projet de votre binôme
Première étape:
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 <...>
id) du path en utilisant req.params.number en utilisant parseIntdeleteVehicle du VehicleStore en passant l’identifiant.res.status(xxx).send(),N’oubliez pas de tester votre changements en utilisant les exemples du fichier README!
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.
Dernière étape: ouvrir une pull request!

Objectif : Valider les changements d’un contributeur
💡 Astuce:Proposez des solutions plutôt que simplement pointer les problèmes.
Objectif: Valider que le changement n’introduit pas de régressions dans le projet
Ces “checks” peuvent êtres exécutés par votre moteur de CI ou des outils externes.
CI s’exécuter!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/
Nous avons vu:
(lint)
linter est un programme qui parcours une base de code à la recherche d’erreurs sans exécuter le programmePar exemple
function sayName(value: any) {
console.log(value.name);
}
nameDans une nouvelle branche, à jour de main, de votre dépot vehicle-server
typescript-eslint en suivant le guide de mise en placestrict et stylisticnpm run lint excécute l’analyse statique sur la base de code typescript!npm install --save-dev eslint @eslint/js @types/eslint__js 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,
);
{
...
"scripts":{
// ...
"lint": "eslint ./src"
}
}
npx eslint --fix ./src
npm run lint
Nous voulons maintenant que le workflow de CI excécute le lint après la compilation
step executant npm run lint dans le workflow de CIname: 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/
Nous avons vu:
C’est du code qui:
On parle de SUT, System Under Test.
(La terminologie varie d’un développeur / langage / entreprise / écosystème à l’autre)

npm run test appelle jest# Installation des dépendances
npm install --save-dev jest ts-jest @types/jest
# Initialisation de la configuration de jest
npx ts-jest config:init
package.json{
...
"scripts":{
// ...
"test": "jest"
}
}
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.
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 🤬
app.tsController qui valide puis traduit une requète HTTP vers de la logique métier./src/controllerVehicleStore, qui permets de manipuler les véhicles en base./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èmatiquevalidateRequestPayload retourne une violation L16AppErrorCreateVehicleController, la méthode handlehandle du CreateVehicleControllerVehicleStore necessite un pg.Pool, un pool de connections a la base de données.VehicleStoreCré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,
);
}),
}
})
})));
beforeEach, afterEachdescribe, via beforeAll, afterAllAjoutez 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.
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!
200 dans l’objet réponsejson dans la réponse, il faudrait le valider.resp.gotJson12 au nouveau véhiculejest docexpect(resp.gotJson).toEqual({
vehicle: new Vehicle(
12,
'abac',
17,
{longitude: 45, latitude: 45},
)
});
try / catch pour capturer l’exceptionAppError, jest à une assertion pour çaAppError, il vous faut convertir l’exception avec le mot clé asconst myErr = err as AppErrorAppError se trouve dans ./src/errors.ts// 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" ],
});
}
});
});
Pensez a exécuter npm lint et a comitter une fois que le lint passe.


Tester des composants indépendamment ne prouve pas que le système fonctionne une fois intégré!
Une suite de tests d’intégration doit:
➡️ On se place ici d’un point de vue du client de l’application
On parle de “pyramide des tests”

Source octo.com
Dans notre cas, pour réaliser un test d’intégration il va nous falloir
Pour nous simplifier la tấche, on se propose d’utiliser les librairies suivantes:
🎓 Exercice: En suivant la documentation, installez ces dépendances!
/* 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
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
}
}
]
}
ListVehiclesController et le CreateVehicleControllerListVehiclesControllernewVehicleFromRow mélange la longitude avec la latitude (L90-91)GET /vehicles?lat=xxx&long=xxx&limit=10await 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)'));
`
);
GET /vehicles notre app en utilisant supertest/* 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},
]);
});
})
npm run lint et de corriger les problèmes reportés!npm run test soit exécuté après le lint!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/
Nous avons maintenant un moyen systèmatique de vérifier que la logique de notre application est correcte!
🎉 Vous pouvez merger votre PR!

Rappels et remise à niveau

Problème de temps exponentiel
L’IT n’est pas la seule industrie à résoudre des problèmes…

“Separation of Concerns”

“Virtualisation Légère”

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

Non exclusifs mutuellement

# 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 ?
docker run ... a envoyé une requête POST au serviceLe 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”
Quand la commande s’arrête : le conteneur s’arrête
Lancez un nouveau conteneur en tâche de fond, nommé webserver-1 et basé sur l’image nginx
docker container run --help ou Documentation en ligneAffichez les “logs” du conteneur (==traces d’exécution écrites sur le stdout + stderr de la commande conteneurisée)
docker container logs --help ou Documentation en ligneComparez les versions de Linux de Gitpod et du conteneur
/etc/os-releasedocker container execdocker 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 ...
$ 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
docker container inspect \
--format='{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' \
webserver-1
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”
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
# ...
Docker Hub : C’est le registre d’images “par défaut”
🎓 Cherchez l’image hello-world pour en voir la page de documentation
Il existe d’autre “registres” en fonction des besoins (GitHub GHCR, Google Artifact Registry, etc.)
C’est une “image” de conteneur, c’est à dire un modèle (template) représentant une application auto-suffisante.
C’est un système de fichier complet:
/=> Permet de fournir un livrable portable (ou presque).
🤔 Application Auto-Suffisante ?


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 !
git/workspace/docker-git/Dockerfile avec le contenu ci-dessous :FROM ubuntu:20.04
RUN apt-get update && apt-get install --yes --no-install-recommends git
docker image build --tag=docker-git <chemin/vers/docker-git/docker image lsmkdir -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
[REGISTRY/][NAMESPACE/]NAME[:TAG|@DIGEST]
registry.docker.comlibrarylatestlatestubuntu:20.04 => registry.docker.com/library/ubuntu:20.04dduportal/docker-asciidoctor => registry.docker.com/dduportal/docker-asciidoctor:latestghcr.io/dduportal/docker-asciidoctor:1.3.2@sha256:xxxxdocker image tag docker-git:latest docker-git:1.0.0
docker image lsdocker 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
Mettez à jour votre image en version 1.1.0 avec les changements suivants :
LABEL
dont la clef est description (et la valeur de votre choix)git pour utiliser une branche main par défaut au lieu de master (commande git config --global init.defaultBranch main)Indices :
docker image inspect <image name>git config --get init.defaultBranch (dans le conteneur)DockerfileDockerfilecat ./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 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
FROM node:20-alpine3.19
COPY . /app
WORKDIR /app
RUN npm install && \
npm run build
ENTRYPOINT ["node", "/app/dist/index.js"]
FROM dans un Dockerfile, qui vont créer plusieurs images intermédiairesFROM xxx AS <stage name>COPY --from <stage name> /path1 /pathDockerfile est taggée!En partant de votre Dockerfile, creez un build en deux étapes:
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?
Dockerfile et de la commande docker image buildUne fois que votre image fonctionne, vous pouvez ouvrir une PR avec ce Dockerifle et la merger
Nous sommes prêts, il est grand temps de livrer notre v1.0.
…mais c’est quoi l’objectif de notre livraison déjà?
💡 Activer le 2FA est une bonne idée également
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 ?Publiez l’image sur le DockerHub
docker logindocker image pushnpm run publish ?Publier le tag sur votre “remote” origin.
git push origin 1.0.0On 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 Delivery (CD)
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.
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)
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 :
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
# ...
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
🇬🇧 Continuous Deployment / “CD”

Source : http://blog.crisp.se/2013/02/05/yassalsundman/continuous-delivery-vs-continuous-deployment
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 :
main, alors il faut pousser l’image avec le tag main 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
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!
