Introduction

Références

Identification du module

Définition

Le terme DevOps est un mot-valise anglais formé à partir de development (développement) et operations (exploitation). Il désigne un ensemble de pratiques visant à automatiser et à industrialiser les processus de production des applications logicielles, depuis le développement jusqu’à la mise en production.

Historique

Contexte et défis initiaux

Au début des années 2000, l’industrie du logiciel connaissait une révolution rapide avec l’essor d’Internet. Malgré ces avancées, les entreprises se heurtaient à des obstacles structurels majeurs dans la livraison de logiciels :

  1. Silotage des équipes : Les équipes de développement (Dev) et des opérations (Ops) travaillaient en silos séparés, avec des objectifs opposés — les développeurs cherchaient à livrer de nouvelles fonctionnalités rapidement, tandis que les opérations privilégiaient la stabilité des systèmes.
  2. Longs cycles de développement : Les méthodologies en cascade (waterfall) impliquaient des cycles longs, des déploiements rares et risqués.
  3. Problèmes de communication : Le manque de collaboration entre équipes engendrait des erreurs de configuration et des délais dans la résolution des incidents.
  4. Environnements incohérents : Les environnements de développement, de test et de production différaient souvent, causant des problèmes de compatibilité lors des déploiements.

Émergence de nouvelles pratiques

Au milieu des années 2000, des réponses concrètes à ces défis ont commencé à émerger :

Formalisation du terme DevOps

Le terme DevOps a été popularisé en 2009 lors de la conférence Velocity, où John Allspaw et Paul Hammond de Flickr ont présenté 10+ Deploys per Day: Dev and Ops Cooperation at Flickr. Leur retour d’expérience a démontré concrètement comment la collaboration entre Dev et Ops permet d’améliorer la fréquence et la fiabilité des déploiements.

Depuis, DevOps est devenu un mouvement mondial, intégrant progressivement de nouvelles pratiques comme l’infrastructure en tant que code (IaC), le monitoring continu, et la sécurité intégrée dès le début du cycle de développement (DevSecOps).

CI/CD

La CI et la CD sont deux pratiques fondamentales du mouvement DevOps.

CI — Intégration Continue (Continuous Integration)

La CI consiste à intégrer fréquemment les modifications de code dans un dépôt partagé. Chaque modification est automatiquement testée et validée, ce qui permet de détecter et corriger les erreurs au plus tôt.

CD — Livraison Continue (Continuous Delivery)

La CD automatise le processus de déploiement des applications. Les modifications de code sont construites, testées et déployées en production de manière automatique, réduisant les délais de mise en production tout en améliorant la qualité du logiciel.

QCM — Introduction à DevOps et CI/CD


1. Que signifie le terme DevOps ?


2. Quelle méthodologie traditionnelle de développement impliquait de longs cycles et des déploiements rares ?


3. Lors de la conférence Velocity en 2009, qui a présenté le discours fondateur du mouvement DevOps ?


4. Dans quelle entreprise travaillaient les présentateurs de la conférence Velocity en 2009 ?


5. Quel était le titre du discours présenté à la conférence Velocity en 2009 ?


6. Lequel de ces outils N’est PAS mentionné comme outil d’automatisation dans le texte ?


7. Qu’est-ce que la CI (Continuous Integration) ?


8. Quel est le principal bénéfice de la CD (Continuous Delivery) ?


9. Qu’est-ce que le DevSecOps ?


10. Quel problème causaient les environnements incohérents entre développement, test et production ?

Outils

Il existe de nombreux outils qui peuvent être utilisés pour mettre en œuvre les pratiques DevOps. Voici quelques-uns des outils les plus populaires :

Termes Outils

Vous ne devrez pas tous les connaître et je ne pense pas qu’il existe une personne qui les maîtrise tous. Nous pratiquerons quelqu’uns de ces outils et vous vous adapterez à ceux de votre futur entreprise.

Automatisation

Connaître des possibilités d’utilisation des outils d’automatisation dans l’environnement de développement (p. ex. linting, build, exécuter, tester, versionner, dépendances/paquets).

Vue d’ensemble

L’automatisation dans un environnement de développement couvre quatre grandes étapes, dans un ordre logique :

  1. Linting — analyser et garantir la qualité du code avant toute exécution
  2. Tests unitaires — valider le comportement des composants de manière isolée
  3. Documentation — générer la documentation à partir du code source
  4. Build — compiler et packager l’application pour la production

Linting

Le linting est une pratique de développement logiciel qui consiste à analyser le code source pour détecter les erreurs de syntaxe, les problèmes de style et les mauvaises pratiques de programmation. Les outils de linting permettent de garantir la cohérence du code, d’améliorer sa lisibilité et de réduire les erreurs.

ESLint

ESLint est un outil de linting pour JavaScript qui permet de détecter et de corriger les erreurs de syntaxe, les problèmes de style et les mauvaises pratiques de programmation. Il est largement utilisé dans l’industrie du logiciel pour améliorer la qualité du code JavaScript.

Un fichier de configuration standard de linting ressemble à ceci :

{
  "rules": {
    "no-unused-vars": "error",
    "no-undef": "error"
  }
}

Dans cet exemple, deux règles de linting sont définies : no-unused-vars et no-undef. La règle no-unused-vars signale une erreur si une variable est déclarée mais jamais utilisée, tandis que la règle no-undef signale une erreur si une variable est utilisée sans être déclarée.

Les niveaux de sévérité peuvent également être exprimés sous forme numérique :

{
  "rules": {
    "quotes": [2, "double"],
    "no-unused-vars": 1,
    "no-undef": 0
  }
}

Les valeurs 0, 1, 2 correspondent respectivement à off, warn et error :

off or 0 - turn the rule off

warn or 1 - turn the rule on as a warning (doesn’t affect exit code)

error or 2 - turn the rule on as an error (exit code is 1 when triggered)

Il existe de nombreuses autres règles de linting que vous pouvez configurer en fonction de vos besoins. Vous trouverez des exemples de règles de linting de différentes entreprises sur github.

Exercice ESLint

Pré-requis : Node.js doit être installé sur votre machine.

Pour cet exercice, vous allez installer ESLint et créer un fichier de configuration de linting pour un projet JavaScript. Le travail sera réalisé dans Visual Studio Code.

  1. Créez un nouveau projet JavaScript dans Visual Studio Code. Placez ce projet dans un dossier nommé jseslint.
  2. Créez un fichier index.js dans le dossier du projet et ajoutez-y le code suivant :
console.log("Demo ESLint")
  1. Installez l’extension ESLint dans Visual Studio Code en recherchant ESLint dans la barre de recherche des extensions.
  2. Ouvrez le terminal dans Visual Studio Code et installez ESLint en exécutant les commandes suivantes :
$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help init` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg>` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
package name: (jseslint)
version: (1.0.0)
description: ESLint projet for learning
entry point: (index.js)
test command:
git repository:
keywords:
author: Pierre Ferrari
license: (ISC)
type: (commonjs) module
About to write to /your/path/jseslint/package.json:

{
  "name": "jseslint",
  "version": "1.0.0",
  "description": "ESLint projet for learning",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Pierre Ferrari",
  "license": "ISC",
  "type": "module"
}

Is this OK? (yes) yes
$ npx eslint --init
Need to install the following packages:
eslint@10.8.1
Ok to proceed? (y) 
You can also run this command directly using 'npm init @eslint/config@latest'.
Need to install the following packages:
@eslint/create-config@2.0.0
Ok to proceed? (y) 


> jseslint@1.0.0 npx
> create-config

@eslint/create-config: v2.0.0
✔ What do you want to lint? · javascript
✔ How would you like to use ESLint? · problems
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · none
✔ Does your project use TypeScript? · No / Yes
✔ Where does your code run? · node
ℹ The config that you've selected requires the following dependencies:

eslint, @eslint/js, globals
✔ Would you like to install them now? · No / Yes
✔ Which package manager do you want to use? · npm
☕️Installing...

added 71 packages, and audited 72 packages in 1s

18 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
✔ Successfully created /home/pepito/Téléchargements/jseslint/eslint.config.js file.
$ tree -L 1
.
├── eslint.config.mjs
├── index.js
├── node_modules
├── package.json
└── package-lock.json

2 directories, 4 files

Déclarez une variable non utilisée dans le fichier index.js et observez comment ESLint signale une erreur. Vous pouvez voir que la variable est soulignée en rouge et si vous placez votre curseur dessus, ESLint affiche un message d’erreur.

'i' is assigned a value but never used. eslint (no-unused-vars)

On peut également voir les erreurs dans la console de Visual Studio Code :

$ npx eslint inDex.js

/your/path/jseslint/inDex.js
  1:5  error  'i' is assigned a value but never used  no-unused-vars

✖ 1 problem (1 error, 0 warnings)

Dans le fichier de configuration package.json, ajoutez une commande pour lancer ESLint ainsi qu’une commande pour démarrer l’exécution du script inDex.js :

{
  "name": "jseslint",
  "version": "1.0.0",
  "description": "ESLint projet for learning",
  "main": "inDex.js",
  "scripts": {
    "start": "node inDex.js",
    "lint": "eslint \"**/*.js\" --ignore-pattern node_modules/"
  },
  "author": "Pierre Ferrari",
  "license": "ISC",
  "devDependencies": {...}
}

Pour ajouter les rules, ça se passe dans le fichier de configuration d’eslint eslint.config.mjs :

eslint.config.mjs

...

export default defineConfig([
  {
    ...

    languageOptions: {
      globals: globals.browser
    },
    rules: {
      "no-unused-vars": "error",
      "no-undef": "error"
    }
  },

  ...
]

Et inDex.js sans changer quoi que ce soit dans un premier temps :

var i = 0
var y
let arr = [
  [1, 2],
  2,
  [3, 4],
  4,
  [5, 6],
  7,
  [8, 9],
  10,
  11,
  12,
  13,
  14,
  15,
  16,
  17,
  18,
  19,
  20,
]
console.log("Demo ESLint")

for (y = 0; y < 10; y++) {
  console.log(arr[y])
}

if (i == 10) {
  console.log('i is 10')
} else {
  console.log('i is not 10')
}

const myVariable = 12
myVariable = 13

REMARQUE : Le fichier se nomme bien inDex.js et pas index.js.

Ensuite, démarrez le script avec la commande npm run start 2>/dev/null et lancez ESLint avec la commande npm run lint.

Vous pouvez constater que le script fonctionne très bien mais qu’il ne respecte pas les règles de linting.

Exercice 1

Uniquement à l’aide de la documentation officielle, ajoutez les règles suivantes :

TIPS : array-bracket-spacing, semi, no-undef, no-unused-vars, no-const-assign, no-var, no-implicit-globals, eqeqeq, indent, prefer-const

Exercice 2

Uniquement à l’aide de la documentation officielle, trouvez et ajoutez les règles de linting permettant de détecter les erreurs suivantes :

if (condition) {
  // code
} else {
  // code
}

Si vous reprenez le code d’exemple fourni plus haut sans faire de modification, vous devriez obtenir les erreurs suivantes :

$  npm run lint

> jseslint@1.0.0 lint
> eslint "**/*.js" --ignore-pattern node_modules/

/your/path/jseslint/inDex.js
   1:1   error  Filename 'inDex.js' does not match snake_case
   1:1   error  Unexpected var, use let or const instead
   1:10  error  Extra semicolon
   2:1   error  This line has a length of 97. Maximum allowed is 80
   2:5   error  'arr' is never reassigned. Use 'const' instead
   2:31  error  A space is required before ']'
   2:35  error  A space is required after '['
   2:47  error  A space is required after '['
   2:52  error  A space is required before ']'
   2:97  error  A space is required before ']'
   3:13  error  Strings must use doublequote
   5:5   error  'y' is not defined
   5:12  error  'y' is not defined
   5:20  error  'y' is not defined
   6:1   error  Expected indentation of 0 spaces but found 2
   6:3   error  Opening curly brace does not appear on the same line as controlling statement
   7:19  error  'y' is not defined
   7:22  error  Extra semicolon
  10:7   error  Expected '===' and instead saw '=='
  11:1   error  Opening curly brace does not appear on the same line as controlling statement
  12:15  error  Strings must use doublequote
  13:1   error  Closing curly brace does not appear on the same line as the subsequent block
  15:1   error  Opening curly brace does not appear on the same line as controlling statement
  16:15  error  Strings must use doublequote
  19:7   error  Identifier 'myVariable' does not match the pattern '^[a-z_]+$'
  20:1   error  'myVariable' is constant
  20:1   error  'myVariable' is assigned a value but never used

✖ 27 problems (27 errors, 0 warnings)
  17 errors and 0 warnings potentially fixable with the `--fix` option.

Une fois que vous obtenez toutes les erreurs et que votre fichier de configuration ESLint est correct, corrigez le code pour faire disparaître les messages d’eslint.

Tests unitaires

Les tests unitaires sont une pratique de développement logiciel qui consiste à tester les composants individuels d’une application de manière isolée. Ils permettent de vérifier le bon fonctionnement des fonctions, des classes et des modules, et de détecter les erreurs de manière précoce.

Cela suppose également que le code soit organisé de manière à ce que les composants soient facilement testables. C’est pourquoi il est important de suivre les principes de conception logicielle tels que le découplage, l’encapsulation et la modularité.

Chaque langage possède ses propres outils de test unitaire. Voici quelques-uns des outils les plus populaires :

Vitest

Vitest est un outil de test unitaire pour JavaScript. Il est pensé pour être compatible avec l’outil Vite. Il est compatible avec le framework de test Jest très largement utilisé dans l’industrie.

Vitest possède des atouts par rapport à Jest :

L’avantage de Vitest réside dans sa console interactive. En effet, le développeur peut voir les résultats des tests en temps réel dans la console de son navigateur. Cela permet de détecter les erreurs plus rapidement et de faciliter le processus de débogage. Ainsi, au moment d’envoyer du code à la partie ops, il est possible de garantir que le code est fonctionnel.

Exercice Vitest

Pré-requis : Node.js doit être installé sur votre machine.

Pour cet exercice, vous allez installer Vitest et configurer un fichier de test unitaire pour un projet JavaScript. Le travail sera réalisé dans Visual Studio Code.

L’ensemble des instructions pour réaliser cet exercice se trouve dans la vidéo DevTheory.

Regardez la vidéo jusqu’au bout avant de pratiquer car dans la vidéo, il fait un comparatif entre Jest et Vitest et donc, il va faire beaucoup de modification en cours de vidéo

Exercice Vitest en shell

Dans l’optique d’utiliser les tests unitaires dans un pipeline CI/CD, il est important de pouvoir lancer les tests en ligne de commande. On vous demande de réaliser toutes les étapes de mise en place d’un environnement de test permettant de tester un programme réalisé en Javascript.

Le programme sera basique. Il comportera un module mathématique qui contiendra trois fonctions :

math.js

export function add(a, b) {
  // Votre code ici
}

export function sub(a, b) {
  // Votre code ici
}

export function rotate(arr, n) {
  // Votre code ici
}

Les tests unitaires seront réalisés avec Vitest.

math.test.js

import { add, sub, rotate } from './math.js'

test('[add] expect the result of 1 + 2 to be 3', () => {
  expect(add(1, 2)).toBe(3)
})

// les autres tests
...

Les tests seront les suivants :

Le résultat attendu est le suivant :

$ npm run vitest

> js-test@1.0.0 vitest
> vitest --globals

 DEV  v2.0.5 /your/path/js-test

 ✓ math.test.js (16)
   ✓ [add] expect the result of 1 + 2 to be 3
   ✓ [add] expect the result of -1 + -0 to be -1
   ✓ [add] expect the result of -1.0 + 1.0 to be 0
   ✓ [add] expect the result of 1 + 'a' to be an error
   ✓ [add] expect the result of 'a' + [1] to be an error
   ✓ [sub] expect the result of 1 - 2 to be -1
   ✓ [sub] expect the result of -1 - -0 to be -1
   ✓ [sub] expect the result of -1.0 - 1.0 to be -2
   ✓ [sub] expect the result of 1 + 'a' to be an error
   ✓ [sub] expect the result of v + [a] to be an error
   ✓ [rotate] expect the result of [1, 2, 3, 4] to be [2, 3, 4, 1]
   ✓ [rotate] expect the result of [a, 2, 3, 4] to be an error
   ✓ [rotate] expect the result of [1, 2, 3, 4, 5] to be [2, 3, 4, 5, 1]
   ✓ [rotate] expect the result of [1] to be [1]
   ✓ [rotate] expect the result of [] to be []
   ✓ [rotate] expect the result of [[1], 2, 3, 4, 5, 6, 7, 8, 9, 10] to be an error

 Test Files  1 passed (1)
      Tests  16 passed (16)
   Start at  13:52:56
   Duration  239ms (transform 22ms, setup 0ms, collect 17ms, tests 6ms, environment 0ms, prepare 67ms)

Exercice Vitest — coverage et format de sortie

Vitest permet également d’obtenir une information importante lors des tests unitaires : le code coverage, c’est-à-dire quel pourcentage du code est couvert par les tests.

Les informations se trouvent ici.

L’objectif de cet exercice est d’obtenir une commande npm run coverage qui affiche ceci :

$ npm run coverage

> js-test@1.0.0 coverage
> vitest run --coverage

 RUN  v2.1.9 /your/path/js-test
      Coverage enabled with v8

 ✓ src/math.test.js (16)
   ✓ [add] expect the result of 1 + 2 to be 3
   ✓ [add] expect the result of -1 + -0 to be -1
   ✓ [add] expect the result of -1.0 + 1.0 to be 0
   ✓ [add] expect the result of 1 + 'a' to be an error
   ✓ [add] expect the result of 'a' + [1] to be an error
   ✓ [sub] expect the result of 1 - 2 to be -1
   ✓ [sub] expect the result of -1 - -0 to be -1
   ✓ [sub] expect the result of -1.0 - 1.0 to be -2
   ✓ [sub] expect the result of 1 + 'a' to be an error
   ✓ [sub] expect the result of v + [a] to be an error
   ✓ [rotate] expect the result of [1, 2, 3, 4] to be [2, 3, 4, 1]
   ✓ [rotate] expect the result of [a, 2, 3, 4] to be an error
   ✓ [rotate] expect the result of [1, 2, 3, 4, 5] to be [2, 3, 4, 5, 1]
   ✓ [rotate] expect the result of [1] to be [1]
   ✓ [rotate] expect the result of [] to be []
   ✓ [rotate] expect the result of [[1], 2, 3, 4, 5, 6, 7, 8, 9, 10] to be an error

 Test Files  1 passed (1)
      Tests  16 passed (16)
   Start at  10:54:40
   Duration  492ms (transform 27ms, setup 0ms, collect 19ms, tests 4ms, environment 0ms, prepare 89ms)

 % Coverage report from v8
----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files |   98.52 |    85.71 |     100 |   98.52 |
 math.js  |   98.52 |    85.71 |     100 |   98.52 | 68
----------|---------|----------|---------|---------|-------------------

En même temps, la commande doit créer un fichier coverage-final.json ainsi qu’une page HTML présentant le coverage. Cette page pourrait directement être affichée sur un site dédié.

Documentation

La documentation est une partie essentielle du développement logiciel. Elle permet de décrire le fonctionnement d’une application, d’une bibliothèque ou d’un module, et de fournir des informations sur son utilisation, ses fonctionnalités et ses API.

Il existe de nombreux outils de documentation pour les différents langages de programmation. Voici quelques-uns des outils les plus populaires :

JSDoc

JSDoc est un outil de documentation pour JavaScript qui permet de générer une documentation à partir des commentaires du code source. Il est largement utilisé dans l’industrie du logiciel pour documenter les bibliothèques et les modules JavaScript.

Un exemple de commentaire JSDoc ressemble à ceci :

/**
 * Add two numbers.
 * @param {number} a - The first number to add.
 * @param {number} b - The second number to add.
 * @returns {number} The sum of the two numbers.
 */
function add(a, b) {
  return a + b
}

Dans cet exemple, la fonction add est documentée en spécifiant le type des paramètres et de la valeur de retour. Cela permet de générer une documentation claire et précise à partir du code source.

Pour générer la documentation à partir des commentaires JSDoc, il suffit d’exécuter la commande suivante :

$ npx jsdoc src/math.js

ou alors

$ node_modules/.bin/jsdoc src/math.js

Cette commande génère un répertoire out contenant les fichiers HTML de la documentation. Vous pouvez ouvrir le fichier index.html dans un navigateur pour visualiser la documentation.

Exercice JSDoc

Reprenez l’exercice précédent des tests unitaires et ajoutez des commentaires JSDoc au code source du module mathématique. Générez ensuite la documentation à partir des commentaires JSDoc.

La documentation doit contenir les informations suivantes pour chaque fonction :

Le résultat doit se trouver dans un répertoire docs à la racine du projet. La commande pour générer la documentation doit aller chercher la configuration, notamment du répertoire de sortie, dans le fichier jsdoc-conf.js.

Build

Le build est une étape du processus de développement logiciel qui consiste à compiler, à transformer et à regrouper les fichiers source en un exécutable ou en un package distribuable.

Chaque environnement de développement possède ses propres outils de build avec des finalités bien différentes. Si vous écrivez un programme en C, vous utiliserez make ou cmake pour obtenir un fichier binaire exécutable tel qu’un fichier ELF pour Linux ou PE pour Windows. Si vous écrivez un programme en Java, vous utiliserez maven ou gradle pour obtenir un fichier interprétable par la Java VM. Si vous écrivez un programme en Javascript, vous utiliserez Webpack, Vite, Parcel ou Rollup pour fournir un répertoire contenant toutes les dépendances nécessaires.

Bundler

Un bundler est un outil qui automatise le processus de regroupement qui consiste à combiner plusieurs fichiers, tels que JavaScript, CSS, HTML, des images, des polices et d’autres ressources, en un plus petit nombre de fichiers plus faciles à charger et à gérer.

Programme de démo

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="images/vite.svg" />
    <link rel="stylesheet" href="/css/style.css" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Demo</title>
  </head>
  <body>
    <main id="app"></main>
    <script type="text/javascript" src="/js/math.js" defer></script>
    <script type="module" src="/js/main.js"></script>
  </body>
</html>

js/main.js

import { sayHi } from "./module1.js"
import { showMeta } from "./module2.js"

sayHi()

showMeta()

js/math.js

mathjs

js/module1.js

export function sayHi() {
  alert("Hello world!")
}

export function UnusedFunction2() {
  // This is an unused function
}

js/module2.js

export function showMeta() {
  console.log(math.sqrt(9))
}

export function UnusedFunction() {
  // This is an unused function
}

Installation

$ npm create vite@latest . -- --template vanilla
Need to install the following packages:
create-vite@6.4.1
Ok to proceed? (y) y

> npx
> create-vite . --template vanilla

│
◇  Scaffolding project in /demo...
│
└  Done. Now run:

  npm install
  npm run dev

$ npm install

added 13 packages, and audited 14 packages in 6s

5 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

$ npm install mathjs

$ tree /demo
C:.
│ index.html
│ package-lock.json
│ package.json
├─node_modules
│ ├─ mathjs
│ └─ ...
├─public
│  vite.svg
└─src
   main.js
   module1.js
   module2.js
   style.css

Build

$ npm run build

> demo_editable@0.0.0 build
> vite build

vite v6.3.4 building for production...
✓ 5 modules transformed.
dist/index.html                0.38 kB │ gzip: 0.26 kB
dist/assets/index--E2UjqNH.js  0.79 kB │ gzip: 0.44 kB
✓ built in 152ms

$ tree dist

C:.
└─dist
  └─assets
      index--E2UjqNH.js
   index.html
   vite.svg

index–E2UjqNH.js

( ... function de vite js ) ();
function n() {
  alert('Hello world!')
}
function c() {
  console.log(import.meta)
}
n();
c();

Avec le bundler, seules les fonctions utilisées sont présentes dans le fichier .js final.

Implication ops

À cette étape, il est important de définir les particularités de l’environnement de production. C’est une étape qui doit être réalisée en collaboration avec les équipes ops. En effet, les équipes ops doivent être en mesure de déployer l’application en production et, comme elles sont responsables de l’infrastructure, elles doivent être impliquées dans le processus de build.

Prenons un exemple simple. Imaginons le développement d’une application web en JavaScript qui est en mesure de faire des conversions monétaires.

Le développeur travaille avec les import / export de module Javascript et une API personnelle basée sur l’API fetch en mode asynchrone. Dans son environnement, tout se passe à merveille. Cependant, lorsqu’il envoie son code à l’équipe ops, il reçoit un message qui l’informe que son application ne fonctionne pas.

vite v5.2.13 building for production...
✓ 5 modules transformed.
x Build failed in 97ms
error during build:
[vite:esbuild-transpile] Transform failed with 2 errors:
assets/index-!~{001}~.js:223:20: ERROR: Top-level await is not available in the configured target environment ("chrome87", "edge88", "es2020", "firefox78", "safari14" + 2 overrides)

C’est normal car l’équipe ops utilise, pour ses tests, un environnement qui ne supporte pas l’utilisation des méthodes asynchrones directement dans le module de premier niveau.

La différence se résume à la simple ligne suivante de configuration dans le fichier vite.config.js :

export default {
  build: {
    target: "esnext",
  },
}

Malheureusement, le développeur n’a pas fourni ce fichier de configuration à l’équipe ops et l’a ignoré dans le dépôt Git, pensant qu’il s’agissait d’un fichier de configuration local. Comme l’équipe ops utilise la version présente dans le dépôt Git, elle n’a pas pu déployer l’application.

Inévitablement, les deux équipes seront en conflit pour savoir qui a raison. C’est pourquoi il est important de définir les particularités de l’environnement de production dès le début du projet et de les documenter.

Exercice Build

Pour cet exercice, nous utiliserons vitejs pour construire un site web simple basé sur du Javascript.

L’objectif est de réaliser le site web présenté ci-dessous.

Ce site permet de convertir des devises. Il se base sur l’API ExchangeRate. L’API fournit un service gratuit pour obtenir les taux de change entre différentes devises. Dans sa version gratuite, on ne peut pas directement faire la conversion d’une devise en une autre mais on peut passer par le Dollar US :

€ → CHF revient à faire € ⇢ $ ⇢ CHF.

Particularités

Grâce à vitejs, vous pourrez réaliser la partie build du projet. Le build fournit un répertoire dist contenant tous les fichiers nécessaires pour le déploiement en production.

Quelques exemples de contenus de fichier

.env

VITE_API_KEY=b596b2.................ea8d68b
VITE_API_ENDPOINT=https://openexchangerates.org/api

api.js

/**
 * Retrieve latest exchange rates
 * @returns {Promise<Object>} The latest exchange rates.
 * @throws {Error} If the API request fails.
 * VITE_API_KEY and VITE_API_ENDPOINT must be defined in the .env file.
 */
const getLatestRates = async () => {
  const response = await fetch(
    `${import.meta.env.VITE_API_ENDPOINT}/latest.json?app_id=${
      import.meta.env.VITE_API_KEY
    }`
  )
  if (!response.ok) {
    throw new Error("Failed to fetch latest exchange rates")
  }
  return response.json()
}

api.test.js

import { describe, it, expect, vi } from "vitest"
import { getLatestRates } from "./api.js"

describe("getLatestRates", () => {
  it("should return latest exchange rates", async () => {
    const mockResponse = {
      rates: {
        USD: 1,
        EUR: 0.85,
      },
    }

    vi.spyOn(global, "fetch").mockResolvedValueOnce({
      ok: true,
      json: async () => mockResponse,
    })

    const rates = await getLatestRates()

    expect(rates).toEqual(mockResponse)
  })

  it("should throw an error if the API request fails", async () => {
    vi.spyOn(global, "fetch").mockResolvedValueOnce({
      ok: false,
    })

    await expect(getLatestRates()).rejects.toThrow(
      "Failed to fetch latest exchange rates"
    )
  })
})

Au final, l’arborescence de votre projet doit ressembler à ceci :

.
├── dist
│   ├── assets
│   │   ├── index-BKEa98Qj.js
│   │   └── index-Col1SUQS.css
│   ├── favicon.png
│   └── index.html
├── css
│   └── style.css
├── index.html
├── main.js
├── .env
├── package.json
├── package-lock.json
├── public
│   └── favicon.png
├── README.md
├── src
│   ├── api.js
│   └── api.test.js
├── docs
├── coverage
├── vite.config.js
└── eslint.config.mjs

Vous pouvez remarquer que le répertoire public contient le favicon du site. Si le fichier favicon se trouve dans le dossier public, c’est parce que vitejs le copie automatiquement dans le répertoire dist lors du build.

Le dossier racine comporte également un fichier .env qui permet de définir des variables d’environnement utilisables par vitejs.

Contrôle

Une fois votre build réalisé, vérifiez que le site fonctionne correctement :

  1. Placez le contenu de votre répertoire dist dans un serveur web local (par exemple Laragon ou Wamp).
  2. Ouvrez le fichier index.html dans un navigateur via le protocole HTTP.
  3. Vérifiez que les listes déroulantes sont bien construites.
  4. Vérifiez que les drapeaux sont bien affichés.
  5. Vérifiez que la conversion des devises fonctionne correctement.
  6. Vérifiez dans la console du navigateur que les appels à l’API se font correctement.
  7. Vérifiez qu’il n’y a pas d’erreurs dans la console du navigateur. Par exemple, une erreur de type 404 sur le favicon.ico signifie que le navigateur n’a pas pu trouver ce fichier.

REMARQUES

Vous n’avez pas besoin d’avoir un coverage de 100%. L’idée est de couvrir une partie des cas pour pouvoir générer chaque étape : vitest, coverage, doc et build.

Question Vite

Le fichier .env est ignoré par git et ne doit pas être versionné. Doit-il être fourni à l’équipe ops lors du déploiement en production ?

Introduction à git

Gitflow

Connaître des procédures de développement en équipe dont le suivi est compréhensible et traçable (p. ex. liaison des commits et des récits utilisateur, PullRequest/revue par les pairs). Connaître des pratiques de gestion du code source basée fonctionnalités (p. ex. flux de travail GIT, versionnage sémantique, etc.).

La partie Git du cours est entièrement basé sur le livre gratuit et officiel que l’on peut trouver à cette adresse : https://git-scm.com/book/fr/v2.

Cette partie s’articule autour de la lecture de ce livre (pas complètement 😓) et de la réalisation d’exercices pratiques. Voici comment il faut procéder :

Installez-vous confortablement sur votre chaise, téléchargez et ouvrez le livre officiel de Git.

Lisez les 3 premiers chapitres du livre. Au fur et à mesure de votre avancement, mettez en pratique les commandes qui sont présentées. Aucune aide externe ne vous est autorisée pour ces 3 chapitres. Vous devez vous débrouiller seul ou avec votre enseignant. Il ne vous est pas possible de demander à vos camarades de classe ni d’utiliser Internet!

Vous avez bien entendu le droit de créer des dépôts locaux et/ou distant sur GitLab pour vous entraîner. Vous pourrez supprimer ces dépôts une fois que vous n’en aurez plus besoin.

Chapitre Démarrage rapide

Dans cette partie on vous explique les différents gestionnaires de versions, on vous parle de l’origine de Git et de ses prédécesseurs et on vous explique comment installer et configurer Git.

Comme il est indiqué dans ce chapitre:

Dans ce livre, nous utiliserons Git en ligne de commande. Tout d’abord, la ligne de commande est la seule interface qui permet de lancer toutes les commandes Git - la plupart des interfaces graphiques simplifient l’utilisation en ne couvrant qu’un sous-ensemble des fonctionnalités de Git. Si vous savez comment utiliser la version en ligne de commande, vous serez à même de comprendre comment fonctionne la version graphique, tandis que l’inverse n’est pas nécessairement vrai.

Votre objectif est d’installer Git sur votre machine si ce n’est pas déjà fait, et de passer en revue les différents niveaux de configuration, system, global et local.

Déjà que vous y êtes, configurer votre nom et votre adresse email dans la configuration globale.

$ git config --global --list
user.email=pierre.ferrari@rpn.ch
user.name=piferrari
credential.helper=/usr/share/doc/git/contrib/credential/libsecret/git-credential-libsecret
init.defaultbranch=main
pull.rebase=false
alias.last=log -1 HEAD

Les différentes valeurs de configuration sont expliquées sur la page de manuel de git-config que vous pouvez consulter avec la commande man git-config ou en ligne à cette adresse : https://git-scm.com/docs/git-config.

Chapitre Les bases de Git

Dans ce chapitre, vous allez apprendre les bases de Git. Vous allez apprendre à créer un dépôt, à y ajouter des fichiers, à les valider et à les pousser sur un serveur distant. Vous apprendrez également à ignorez des fichiers et à les renommer.

Commandes pratiquées dans ce chapitre :

$ git init
$ git add
$ git status
$ nano .gitignore
$ git clone
$ git commit
$ git status
$ git diff
$ git mv
$ git rm
$ git log
$ git reset
$ git checkout
$ git restore
$ git remote
$ git push
$ git pull
$ git fetch
$ git tag
$ git show
$ git last

Chapitre Les branches avec Git

Dans ce chapitre, vous allez apprendre à créer des branches, à les fusionner et à les supprimer. Vous allez également apprendre à gérer les conflits de fusion.

Supplément : Chapitre Commandes Git

Ce chapitre constitue une référence des commandes Git. Vous pouvez le consulter à tout moment pour retrouver une commande ou une option.

Exercices git

GitLab S2

Après avoir pratiqué les exercices précédents, vous devriez être en mesure de réaliser l’exercice ci-dessous. Ce pourrait être un exercice d’examen.

  1. Créez un dépôt Git locale sur votre machine dans un dossier de votre choix.
  2. Ajoutez-y un fichier votreNom contenant votre nom. Par exemple ferrarip contenant Pierre Ferrari.
  3. Validez ce changement dans votre dépôt avec un message de commit explicite. Vous trouverez les bonnes pratiques ici
  4. Créez un lien origin vers le dépôt distant exercice-base. ATTENTION, on ne vous demande pas de le cloner, mais de créer un lien vers ce dépôt.
  5. Rapatriez uniquement le contenu de la branch main du dépôt distant dans votre dépôt local. En l’occurrence, un fichier README.md.
  6. Mettez à jour le dépôt local pour le faire coïncider avec votre dépôt distant. C’est-à-dire, faite apparaître le fichier README.md dans votre dépôt local.

binCraft decoder

Vous trouvez un dépôt binCraft à cette adresse.

Le dépôt original est différent de ce dépôt pour plusieurs raison mais une des raisons c’est que le dépôt original contient des erreurs dans le décodage des valeurs binaires alors que dans ce dépôt, les erreurs ont étés corrigées. Votre travail consiste à retrouver les erreurs pour être en mesure de proposer des corrections au mainteneur du dépôt original.

Bien entendu, il ne faut pas chercher à lire le code pour trouver les erreurs. Vous devez utiliser Git pour faire le travail.

Une fois les erreurs identifiées, préparez un patch par erreur identifiée comme si vous vouliez soumettre une demande de fusion (merge request) au mainteneur du dépôt original.

Currency converter (le retour)

Reprenez l’exercice du currency converter que vous avez déjà réalisé précédemment dans la partie build. Durant la réalisation de cet exercice, on ne vous demandait pas de versionner votre code. Vous allez maintenant versionner votre code.

$ git log --graph --abbrev-commit --decorate --format=format:'%C(bold blue)%h%C(reset) - %C(auto)%d%C(reset) %C(white)%s%C(reset) %C(dim white)- %an%C(reset)' --all
* 1517d42 -  (HEAD -> main, tag: v2.0.0, origin/main, origin/HEAD) feat(api)!: changement d'api et adaptation des tests unitaires - piferrari
* 7e5262d -  (tag: v1.1.0) feat(app): ajout de la fonctionnalité permettant d'inverser la conversion, par exemple € → CHF par CHF → € en un clic - piferrari
* b9d7042 -  (tag: v1.0.0) refactor(readme): mise à jour du fichier readme - piferrari
* 5a369fa -  fix(js): suppression de l'event change inutile - piferrari
* c5eae45 -  fix(html): correction du titre de la page - piferrari
* 9e758fb -  refactor(vite): ajout du dossier public et utilisation de ce dossier pour favicon - piferrari
* 7a9ca98 -  feat(js): ajout du programme de conversion, suppression des tests et import css via vite - piferrari
* 79955f1 -  feat(html): ajout des formulaires html avec le css - piferrari
* 9ed7492 -  feat(api): ajout de l'api exchange ainsi que des tests unitaires - piferrari
* 5e85721 -  feat(vite): mise en place de l'environnement vite avec sa config - piferrari
* 3dd38d0 -  Initial commit - Ferrari Pierre (PDF)

Exercices supplémentaires

Vous pouvez également pratiquer les exercices suivants chez vous :

Utiliser un processus d’intégration automatisé

Utiliser un processus d’intégration automatisé est essentiel pour optimiser la qualité et la rapidité des cycles de développement logiciel. L’intégration automatisée consiste à déclencher automatiquement une série de tests et de validations à chaque modification du dépôt.

Ce processus permet de détecter rapidement les erreurs de compilation, les conflits de fusion, ou les bugs introduits par des modifications récentes, réduisant ainsi les délais entre la détection d’un problème et sa résolution. De plus, l’automatisation garantit que les tests sont exécutés de manière cohérente et exhaustive, augmentant ainsi la fiabilité du logiciel. Par exemple, dans un environnement DevOps, l’intégration automatisée peut inclure des tests unitaires, des tests d’intégration, des vérifications de la qualité du code, et même le déploiement sur des environnements de staging, permettant ainsi aux équipes de déceler les problèmes potentiels avant que le code n’atteigne la production. Cette approche non seulement accélère le cycle de développement, mais aussi améliore la qualité et la stabilité du produit final, tout en réduisant les risques de régressions et en favorisant une culture de développement itératif et collaboratif.

Principe de l’intégration continue

Il existe plusieurs manières de procéder pour faire de l’intégration continue. Chaque entreprise possède sa propre manière de faire même si elle se base sur des concepts communs. Ce que vous allez mettre en pratique est un exemple parmi d’autres.

Dans cet exemple, on voit un développeur qui va ajouter une fonctionnalité ou corriger un bug.

Il va créer une branche à partir de la branche main pour réaliser son travail. Il fera des commits réguliers pour sauvegarder son travail. Une fois qu’il pense être au bout de son travail, il va pousser sa branche sur le dépôt distant. Cette action va démarrer un pipeline CI/CD qui va tester le code, le compiler, le valider. Une fois que tous les tests mis en place seront passés (✔), le code sera proposé à une revue par un pair, un autre développeur ou un responsable.

Une fois la revue terminée, l’auteur va créer une demande de fusion (merge request) pour fusionner sa branche avec la branche main. Cette demande de fusion va déclencher un nouveau pipeline CI/CD qui va tester le code, le compiler, le valider et le déployer sur un environnement de staging.

Dans la phase de staging, un groupe de testeur utilisera la nouvelle version proposée pour vérifier que tout fonctionne correctement. Si tout est OK, le code validé sera déployé en production.

GitLab CI/CD

GitLab propose un outil d’intégration continue et de déploiement continu intégré directement dans son outil de gestion de versions. Cet outil s’appelle GitLab CI/CD.

GitLab CI/CD permet de définir des pipelines d’intégration continue et de déploiement continu directement dans le fichier de configuration du dépôt Git. Ces pipelines sont déclenchés automatiquement à chaque modification du dépôt, permettant ainsi de tester, de compiler et de déployer le code de manière automatisée.

Les pipelines GitLab CI/CD sont définis dans un fichier .gitlab-ci.yml à la racine du dépôt Git. Ce fichier contient les étapes du pipeline, les tâches à exécuter, les dépendances entre les tâches, et les conditions de déclenchement du pipeline.

Ce fichier est de type YAML. Il faut respecter la syntaxe YAML pour qu’il soit considéré comme valide. Gitlab propose un éditeur intégré au site web. Cet éditeur comporte un linter qui vous avertis des erreurs présentes lors de l’édition.

Voici un exemple de fichier .gitlab-ci.yml pour un pipeline simple :

stages:
  - build
  - test
  - deploy

build:
  stage: build
  script:
    - npm install
    - npm run build

test:
  stage: test
  script:
    - npm test

deploy:
  stage: deploy
  script:
    - npm run deploy
  only:
    - main

Dans cet exemple, le pipeline est composé de trois étapes : build, test et deploy. Chaque étape contient une ou plusieurs tâches à exécuter, définies dans la section script. Les tâches sont exécutées dans l’ordre défini par les stages, et les dépendances entre les étapes sont gérées automatiquement par GitLab CI/CD.

Exercices basiques GitLab CI/CD

Exercice 1, le plus basique

Créez un dépôt sur le serveur GitLab de l’école. Nommez ce dépôt cicd-basique.

Dans le menu de gauche, cliquez sur Build puis sur Pipeline editor. Activez les pipelines GitLab CI/CD pour ce projet en créant le fichier .gitlab-ci.yml par un simple appui sur le bouton Configure pipeline. Vous vous trouvez alors dans l’éditeur de pipeline.

Commiter le fichier .gitlab-ci.yml en cliquant sur le bouton Commit changes. Rendez-vous ensuite dans l’onglet Pipelines pour voir le pipeline se déclencher.

Exercice 2, un peu de système

Modifiez votre fichier .gitlab-ci.yml depuis le Pipeline editor pour:

Dans la partie script:

Exercice 3, l’erreur classique

Dans votre dépôt, créez une branche cicd et dans le Pipeline Editor mettez ce contenu:

stages:
  - learn

learn-job:
  stage: learn
  rules:
    - if: '$CI_COMMIT_BRANCH == "cicd"'
      when: never
  script:
    - echo "Ca tourne en boucle"

Au moment de valider votre modification, vous pourrez constatez que:

Pourquoi ?

Exercice 4, on vous prends par la main

Cet exercice est en réalité une vidéo réalisée par M. Droz lors d’un cours en ligne. Vous allez suivre pas à pas les instructions de la vidéo pour réaliser un pipeline GitLab CI/CD.

La vidéo se trouve à cette adresse : Ubicast S2.

Exercice 5, un peu plus complexe

Pour cet exercice, vous allez créer un pipeline GitLab CI/CD pour un projet JavaScript en NodeJS. Le pipeline doit inclure les étapes suivantes :

  1. Installer les dépendances du projet.
  2. Lancer les tests unitaires.
  3. Exécuter le code.
  4. Déployer le code sur un serveur de staging (simulation pour l’instant).

Le travail sera réalisé dans l’outil de développement Visual Studio Code.

  1. Créez un nouveau projet sur le serveur GitLab de l’école. Nommez ce projet js-cicd. Le projet est vide pour l’instant.
  2. Cloner ce projet localement dans un dossier nommé js-cicd.
  3. Initialisez un projet JavaScript dans le dossier js-cicd en exécutant la commande npm init.
  4. Installez les dépendances suivantes dans le projet :
$ npm install dayjs
  1. Créez un fichier index.js dans le dossier du projet et ajoutez-y le code suivant :
import dayjs from "dayjs"

const today = dayjs()
const year_end = dayjs("2024-12-31")

console.log(year_end.diff(today, "day"))
  1. Modifier le fichier package.json pour ajouter le type module ainsi que les scripts start et test suivants :

"main": "index.js",
"type": "module",
"scripts": {
  "start": "node index.js",
  "test": "echo \"Error: no test specified\" && exit 1"
}
  1. Testez localement vos scripts en exécutant les commandes npm start et npm test.
$ npm start

> js-cicd@1.0.0 start
> node index.js

194
$ npm test

> js-cicd@1.0.0 test
> echo "Error: no test specified" && exit 1

Error: no test specified
  1. Créez un fichier .gitlab-ci.yml à la racine du projet et ajoutez le contenu suivant :
stages:
  - run
  - test
  - deploy

run:
  stage: run
  before_script:
    - placer ici le nécessaire pour installer node ainsi que les dépendances du projet
  script:
    - placer ici le code nécessaire pour ce stage

test:
  stage: test
  image: node:trixie-slim
  before_script:
    - placer ici le nécessaire pour installer les dépendances du projet
  script:
    - placer ici le code nécessaire pour ce stage

deploy:
  stage: deploy
  script:
    - echo "Deploying to staging server..."
  only:
    - main
  1. Commitez et poussez le projet sur le dépôt distant.
  2. Vérifiez que le pipeline est déclenché automatiquement et que les étapes du pipeline sont exécutées avec succès.

Erreurs :

Il y a des erreurs dans l’exécution du pipeline, c’est normal si vous n’avez rien changé au fichier YAML présent ci-dessus. En effet, dans ce fichier il n’y a actuellement aucune commande valide. Trouvez-les les commandes permettant d’effectuer les 3 premiers stages uniquement.

Runners

Les pipelines GitLab CI/CD sont exécutés par des agents appelés runners. Les runners sont des machines virtuelles ou physiques qui exécutent les tâches du pipeline. Ils peuvent être hébergés sur le cloud, sur site ou sur des machines dédiées. GitLab propose des runners partagés et des runners spécifiques à chaque projet, permettant ainsi de personnaliser l’environnement d’exécution des pipelines en fonction des besoins du projet.

Les runners GitLab CI/CD sont configurés dans les paramètres du projet GitLab. Vous pouvez définir des runners partagés ou spécifiques à chaque projet, en fonction de vos besoins. Les runners peuvent être configurés pour exécuter des tâches spécifiques, utiliser des images Docker personnalisées, ou se connecter à des services externes pour exécuter des tâches spécifiques.

Création d’un runner locale

Pour créer un runner local, il faut installer le logiciel gitlab-runner sur la machine qui va exécuter les tâches du pipeline. Le runner peut être installé sur n’importe quelle machine compatible avec GitLab CI/CD, y compris les machines virtuelles, les conteneurs Docker, les serveurs physiques, etc.

Nous installerons un runner dans une machine virtuelle Ubuntu 24.04. Pour cela, nous allons suivre les étapes suivantes :

  1. Créer une machine virtuelle Ubuntu Server ou Debian LTS.
  2. Installer le logiciel gitlab-runner sur la machine virtuelle en suivant les instructions de la documentation officielle.
ubuntu@m347:~$ curl -sL "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
[sudo] password for ubuntu:
Detected operating system as Ubuntu/jammy.
Checking for curl...
Detected curl...
Checking for gpg...
Detected gpg...
...
ubuntu@m347:~$ sudo apt install gitlab-runner
  1. Dans le projet précédemment créé js-cicd, ajoutez un runner spécifique au projet en suivant les instructions de la documentation officielle.

ubuntu@m347:~$ gitlab-runner register  --url https://git.s2.rpn.ch  --token glrt-ZF_11xZtbP6cJsdaDe5c
ubuntu@m347:~$ gitlab-runner run
Runtime platform                                    arch=amd64 os=linux pid=3420 revision=b92ee590 version=17.4.0
Starting multi-runner from /home/ubuntu/.gitlab-runner/config.toml...  builds=0 max_builds=0
WARNING: Running in user-mode.
WARNING: Use sudo for system-mode:
WARNING: $ sudo gitlab-runner...

Configuration loaded                                builds=0 max_builds=1
listen_address not defined, metrics & debug endpoints disabled  builds=0 max_builds=1
[session_server].listen_address not defined, session endpoints disabled  builds=0 max_builds=1
Initializing executor providers                     builds=0 max_builds=1
  1. Vérifiez que votre pipeline sera exécuté par le runner local.

  1. Exécutez le pipeline sur le runner local et vérifiez que les étapes du pipeline sont exécutées avec succès. Vous pourrez voir les logs évoluer dans la console.
Checking for jobs... received                       job=6941 repo_url=https://git.s2.rpn.ch/versioning/js-cicd.git runner=ZF_11xZtb
Added job to processing list                        builds=1 job=6941 max_builds=1 project=5320 repo_url=https://git.s2.rpn.ch/versioning/js-cicd.git time_in_queue_seconds=1
Appending trace to coordinator...ok                 code=202 job=6941 job-log=0-317 job-status=running runner=ZF_11xZtb sent-log=0-316 status=202 Accepted update-interval=1m0s
Removed job from processing list                    builds=0 job=6941 max_builds=1 project=5320 repo_url=https://git.s2.rpn.ch/versioning/js-cicd.git time_in_queue_seconds=1
Checking for jobs... received                       job=6942 repo_url=https://git.s2.rpn.ch/versioning/js-cicd.git runner=ZF_11xZtb
Added job to processing list                        builds=1 job=6942 max_builds=1 project=5320 repo_url=https://git.s2.rpn.ch/versioning/js-cicd.git time_in_queue_seconds=3
Appending trace to coordinator...ok                 code=202 job=6942 job-log=0-1524 job-status=running runner=ZF_11xZtb sent-log=0-1523 status=202 Accepted update-interval=1m0s

Sources :

Remarques

Vous pouvez constater que la commande gitlab-runner run est lancé en tant qu’utilisateur standard (pas root). Cela à des conséquences systèmes logiques telles qu’il n’est, par exemple, pas possible d’utiliser une commande sudo apt update dans mon fichier .gitlab-ci.yml. En effet, cette commande sera exécutée par mon runner et il n’y a aucun moyen d’entrer le mot de passe dans mon runner. On peut contourner ce problème en créant un utilisateur spécifique qui n’aura pas besoin de mot de passe, un peu comme dans un Raspberry Pi.

Dans la machine où se trouve le runner

ubuntu@m347:~$ sudo adduser gitlab
Nouveau mot de passe : gitlab
Retapez le nouveau mot de passe : gitlab
passwd : mot de passe mis à jour avec succès
Modifier les informations associées à un utilisateur pour gitlab
Entrer la nouvelle valeur, ou appuyer sur ENTER pour la valeur par défaut
    NOM []: 
    Numéro de chambre []: 
    Téléphone professionnel []: 
    Téléphone personnel []: 
    Autre []: 
Is the information correct? [Y/n]
ubuntu@m347:~$ echo "gitlab ALL=(ALL) NOPASSWD: ALL" | sudo tee -a /etc/sudoers.d/gitlab
ubuntu@m347:~$ sudo chmod 440 /etc/sudoers.d/gitlab
ubuntu@m347:~$ su gitlab
Mot de passe : 
gitlab@m347:$ sudo apt update
Atteint : 1 http://security.debian.org/debian-security trixie-security InRelease
Atteint : 2 http://ftp.ch.debian.org/debian trixie InRelease
Atteint : 3 http://ftp.ch.debian.org/debian trixie-updates InRelease
Atteint : 4 https://packages.gitlab.com/runner/gitlab-runner/debian trixie InRelease
Tous les paquets sont à jour.

## Enregistrement du runner depuis cet utilisateur car le fichier de configuration est crée
## dans son répertoire personnel
gitlab@m347:~$ gitlab-runner register  --url https://git.s2.rpn.ch  --token glrt-XXXXXXXXXXXXXXXXX
gitlab@m347:~$ ls -la .gitlab-runner/
...
-rw------- 1 gitlab gitlab  477 25 sep 12:36 config.toml
gitlab@m347:~$ gitlab-runner run -c /home/gitlab/.gitlab-runner/config.toml

Dans votre runner, une fois que nodejs à été installé, il n’est pas nécessaire de le réinstaller à chaque fois. En effet, comme on est en mode shell, si on lance une commande sudo apt install nodejs elle installera les paquets en lien avec nodejs directement dans la machine virtuelle. Donc, il n’est plus nécessaire de le refaire. Dès lors le fichier .gitlab-ci.yml devient:

stages:
  - install
  - run
  - test
  - deploy

install:
  stage: install

  ## Install node in the machine only if it necessary
  before_script:
    - sudo apt update -y
    - |
      if ! command -v nodejs &> /dev/null; then
        sudo apt install nodejs -y
      fi
      if ! command -v npm &> /dev/null; then
        sudo apt install npm -y
      fi
  ## Test node
  script:
    - node --version
    - npm --version

run:
  stage: run
  before_script:
    - node --version
    - npm i
  script:
    - node index.js

test:
  stage: test
  before_script:
    - export NODE_NB_DAYS_TO_END_OF_YEAR=$(node index.js)
    - export BASH_TODAY_NUMBER=$(date +%j)
    - export BASH_NB_DAYS_TO_END_OF_YEAR=$(echo "365-$BASH_TODAY_NUMBER" | bc)
    - export NB_DAYS_COMPUTED=$(echo "$BASH_NB_DAYS_TO_END_OF_YEAR-$NODE_NB_DAYS_TO_END_OF_YEAR" | bc)
  script:
    - if [[ $NB_DAYS_COMPUTED -eq 0 ]] ; then echo "Test passed" ; else echo "Test failed" ; exit 1 ; fi

deploy:
  stage: deploy
  script:
    - echo "Deploying to staging server..."
  only:
    - main

Exercice

Pour faire un résumé de ce qui a été vu jusqu’ici, vous allez réaliser un exercice très simple. Le but de l’exercice est d’afficher une page HTML qui nous indique le date de la prochaine fête des vendanges.

Points à respecter:

Attention, le répertoire de publication FTP ne correspond pas à la racine du serveur mais au nom de l’utilisateur FTP. Par exemple, si vous utilisez l’utilisateur FTP zftpcicd00 le répertoire de publication sera zftpcicd00 et du coup, la racine de votre site web sera https://stark.s2dev.ch/zftpcicd00/

Exemple d’interface

Build runner gitlab

Running with gitlab-runner 15.1.0 (76984217)
  on Official Docker Runner 3 glrt-t1_
Preparing the "docker" executor
...
$ npm run build
> prochaine-fete-des-vendanges@0.0.0 build
> vite build
vite v7.1.11 building for production...
transforming...
✓ 11 modules transformed.
rendering chunks...
computing gzip size...
dist/index.html                 0.73 kB │ gzip: 0.43 kB
dist/assets/index-D7EljR_1.css  0.49 kB │ gzip: 0.31 kB
dist/assets/index-ChczilGb.js   9.48 kB │ gzip: 4.14 kB
✓ built in 257ms
$ echo "Compile complete."
Compile complete.
Uploading artifacts for successful job 00:00
Uploading artifacts...
dist/: found 6 matching files and directories      
Uploading artifacts as "archive" to coordinator... 201 Created  id=17120 responseStatus=201 Created token=glcbt-64
Cleaning up project directory and file based variables 00:01
Job succeeded

Publish runner local

Running with gitlab-runner 18.5.0 (bda84871)
  on debian j2q-TQgQc, system ID: s_ac7de2670f1d
Preparing the "shell" executor
...
$ lftp -u $FTP_USER,$FTP_PASS $FTP_HOST -e "..."
===== Remote directory structure =====
drwxr-xr-x                      - 2025-10-21 14:04:30 /root/zftpcicd00/
drwxr-xr-x                      - 2025-10-21 14:04:30 /root/zftpcicd00/assets/
-rw-r--r--                   9480 2025-10-21 14:04:30 /root/zftpcicd00/assets/index-ChczilGb.js
-rw-r--r--                    486 2025-10-21 14:04:30 /root/zftpcicd00/assets/index-D7EljR_1.css
-rw-r--r--                    731 2025-10-21 14:04:30 /root/zftpcicd00/index.html
-rw-r--r--                   1497 2025-10-21 14:04:30 /root/zftpcicd00/vite.svg
$ echo "Application successfully deployed."
Application successfully deployed.
Cleaning up project directory and file based variables 00:01
Job succeeded

Conditions de déclenchement

Cette partie est un copier / coller du blog de Stéphane Robert. Elle est copiée ici au cas où le blog disparaîtrait.

Il s’est lui-même inspiré de la documentation officielle de GitLab.

Tout bon consultant DevOps se doit d’optimiser ses pipelines CI/CD. GitLab CI/CD, offre une variété de conditions de workflow qui permettent une personnalisation et une optimisation importantes des pipelines. Ces conditions, comme if, when, rules et workflow, sont des instruments clés pour gérer le comportement des jobs dans vos pipelines.

Pourquoi les Conditions sont-elles essentielles ?

Simplement parce que toutes les étapes d’un pipeline ne doivent pas nécessairement être exécutées à chaque commit ou à chaque modification du code source. C’est pour cette raison que GitLab CI/CD permet l’utilisation de conditions dans le fichier YAML.

Les conditions définissent les règles qui déterminent si une étape du pipeline doit être exécutée ou non. Elles permettent d’optimiser le processus de développement en exécutant uniquement les étapes nécessaires.

Définir des Conditions

Pour définir des conditions sur un job du pipeline, il suffit d’y ajouter une section rules. Chaque règle spécifie une condition sous la forme d’une expression conditionnelle et le job sera exécuté si cette condition est évaluée comme vraie.

job:
  script:
    - echo "Cette étape s'exécute si la condition est satisfaite"
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

Dans cet exemple, le job sera exécuté uniquement si la branche du commit est main. Cet exemple est très simple mais les possibilités offertes par les conditions sont vastes. Vous pouvez créer des conditions complexes en utilisant des opérateurs logiques ET, OU ainsi que des variables d’environnement que vous aurez vous-même définies ou qui sont fournies par GitLab.

Voyons quelques exemples d’utilisation courants de conditions :

rules:
  - if: "$CI_COMMIT_TAG"

Ici, le job sera exécuté uniquement si le commit est associé à un tag.

rules:
  - if: '$CI_PIPELINE_SOURCE == "web"'

Ce job s’exécutera seulement si le pipeline a été lancé manuellement depuis l’interface GitLab (bouton Run pipeline).

Utilisation de when pour Contrôler le Déclenchement

when est utilisé à l’intérieur des règles rules pour définir le moment où un job doit être exécuté. GitLab offre plusieurs options pour when afin de personnaliser le déclenchement de vos jobs en fonction de scénarios spécifiques.

on_success

rules:
  - if: '$CI_COMMIT_BRANCH == "main"'
    when: on_success

Dans cet exemple, le job sera exécuté uniquement si la branche du commit est main et si tous les jobs précédents dans le pipeline ont été exécutés avec succès.

on_failure

rules:
  - if: '$CI_COMMIT_BRANCH == "main"'
    when: on_failure

Ce job s’exécutera uniquement si la branche du commit est main et si au moins l’un des jobs précédents dans le pipeline a échoué.

always

rules:
  - if: '$CI_COMMIT_BRANCH == "main"'
    when: always

Ce job sera toujours exécuté, quel que soit l’état des jobs précédents, tant que la branche du commit est main.

manual

rules:
  - if: '$CI_COMMIT_BRANCH == "main"'
    when: manual

Ce job ne s’exécute que lorsque l’utilisateur le déclenche manuellement, indépendamment des autres conditions.

delayed

rules:
  - if: '$CI_COMMIT_BRANCH == "main"'
    when: delayed
    start_in: 30 minutes

Ce job s’exécute automatiquement, mais il est retardé de 30 minutes à partir du déclenchement du pipeline.

scheduled

rules:
  - if: '$CI_PIPELINE_SOURCE == "schedule"'

Ce job s’exécute pour les pipelines planifiés. La planification (par exemple tous les jours à midi) se configure dans l’interface, sous Build > Pipeline schedules, avec l’expression cron 0 12 * * *.

Les règles combinées ET / OU

Syntaxe de base

- if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "push"
- if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop"
- if: ($CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop") && $CI_PIPELINE_SOURCE == "merge_request_event"

Les règles sont évaluées dans l’ordre, et la première qui correspond s’applique. Pour des règles simples sans attributs supplémentaires, l’effet revient à un OU.

rules:
  - if: '$CI_COMMIT_BRANCH == "main"'
  - if: '$CI_COMMIT_BRANCH == "develop"'

est équivalent à:

rules:
  - if: $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "develop"

Les règles en if / else

Fonctionnement général. Les rules sont évaluées dans l’ordre.

rules:
  # Le tiret ouvre une nouvelle liste contenant le if et le when qui sont alignés
  - if: '$CI_COMMIT_BRANCH == "main"'
    when: on_success   # IF
  # La première liste se termine et une nouvelle liste commence  
  - when: manual       # ELSE (par défaut, passe en manuel)

Les règles sur les pipelines : workflow

La directive workflow détermine si un pipeline doit être créé et exécuté. Contrairement aux règles appliquées à des jobs individuels, workflow permet de contrôler le déclenchement de l’ensemble du pipeline en se basant sur des critères globaux. Cette capacité de gestion à un niveau supérieur offre un contrôle précis et efficace sur l’exécution des pipelines, en particulier dans des projets complexes avec de multiples branches et conditions.

Un cas d’utilisation courant de workflow est la mise en place de conditions pour exécuter des pipelines uniquement pour certains événements ou pour certaines branches. Par exemple, vous pourriez vouloir déclencher un pipeline seulement sur la branche principale ou sur des branches de fonctionnalités, mais pas sur des branches de corrections de bugs. Voici comment cela peut être configuré :

workflow:
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
    - if: "$CI_COMMIT_BRANCH =~ /^feature-/"
    - when: never

Dans cet exemple, un pipeline est créé pour la branche main ou pour une branche dont le nom commence par feature-. Tous les autres cas ne créent pas de pipeline.

On peut raisonner en logique inverse. On bloque bugfix et hotfix et on autorise tout le reste :

workflow:
  rules:
    # Le tiret ouvre une nouvelle liste contenant le if et le when qui sont alignés
    - if: '$CI_COMMIT_BRANCH =~ /^(bugfix|hotfix)-/'
      when: never
    # La première liste se termine et une nouvelle liste commence
    - when: always

workflow peut également intégrer des logiques plus complexes, comme des conditions basées sur des variables d’environnement, des tags ou des changements spécifiques dans le code. Cela permet une flexibilité et une personnalisation élevées, adaptant vos pipelines aux besoins spécifiques de votre projet.

Exemple : Éviter les exécutions redondantes de pipelines

Imaginons un cas où vous souhaitez éviter que le même pipeline soit déclenché à la fois par un push et par un tag sur le même commit. En effet, si vous créez un tag avec la commande git tag <tagname>, le tag sera créé localement. Vous devrez donc poursuivre votre travail par un git push origin --tags. Ceci est un scénario courant où des pipelines redondants peuvent se lancer si les règles ne sont pas bien configurées. Voici comment workflow peut être utilisé pour gérer cela :

workflow:
  rules:
    - if: "$CI_COMMIT_TAG"
      when: never
    - when: always

Dans cet exemple, aucun pipeline n’est créé quand on pousse un tag (when: never). Tous les autres événements continuent de créer un pipeline. Un commit poussé sur une branche déclenche donc toujours son pipeline, mais pas le git push origin --tags correspondant.

Exemple : Gérer les pipelines pour les merge requests

Un autre cas d’usage fréquent est la gestion des pipelines pour les merge requests. Imaginez que vous avez ouvert une merge request et pour une raison quelconque, un commit est poussé sur la branche de la merge request. Vous ne voulez pas que le pipeline se déclenche à la fois pour le commit et pour la merge request. Voici comment workflow peut être utilisé pour gérer cela :

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
      when: always
    - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS && $CI_PIPELINE_SOURCE == "push"
      when: never
    - if: $CI_COMMIT_BRANCH
      when: always

Dans cette configuration, le pipeline se déclenche uniquement pour les événements de merge requests. Si un commit est poussé sur une branche qui a une merge request ouverte, le pipeline ne se déclenchera pas séparément pour cette branche, évitant ainsi des exécutions en double.

Ces exemples illustrent le fonctionnement de la directive workflow pour affiner la logique de déclenchement de vos pipelines dans GitLab CI/CD, vous permettant ainsi de gérer efficacement les ressources et d’éviter des exécutions inutiles ou redondantes.

Combinaison stratégique de rules et workflow

L’intégration et l’orchestration efficaces des pipelines dans GitLab CI/CD peuvent être grandement améliorées en combinant judicieusement les rules au niveau des jobs avec la directive workflow pour le pipeline global. Cette combinaison permet une gestion fine et une optimisation des déclenchements et exécutions des jobs, adaptant ainsi les pipelines aux exigences spécifiques et aux conditions de votre projet.

Exemple : Gestion de pipelines en fonction des branches et des événements

Imaginez un scénario où vous souhaitez exécuter des jobs différents en fonction de la branche sur laquelle les commits sont poussés, tout en gérant globalement le déclenchement des pipelines pour éviter les redondances. Voici comment cela peut être configuré :

workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH =~ /^release-/)'
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

deploy:
  script: deploy.sh
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual
    - if: "$CI_COMMIT_BRANCH =~ /^release-/"
      when: on_success

test:
  script: test.sh
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

Ici, le job deploy est configuré pour s’exécuter manuellement sur la branche principale et automatiquement en cas de succès sur les branches commençant par release-. Le job test est quant à lui déclenché uniquement pour les merge requests. Le workflow global est configuré pour déclencher le pipeline pour les push sur la branche principale, les événements de planification et les événements de merge requests, tout en évitant les déclenchements redondants.

En comprenant et en appliquant ces concepts dans vos projets, vous pourrez assurer que vos pipelines ne sont pas seulement fonctionnels, mais qu’ils sont également conçus de manière à répondre efficacement aux changements et exigences de votre environnement de développement. Cela conduira à une intégration et une livraison continues plus robustes, fiables et efficientes, un objectif clé dans toute démarche DevOps.

Exercices 1, comprendre les conditions

En parcourant un dépôt GitLab, vous trouvez le fichier .gitlab-ci.yml suivant :

stages:
  - build
  - test
  - deploy

workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: "$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS"
      when: never
    - if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"

💬 Comment connaître le nom de la branche par défaut $CI_DEFAULT_BRANCH de ce dépôt ?

Settings > Repository -> Branch defaults

💬 Quels sont les autres mots clés utilisables avec la variable $CI_PIPELINE_SOURCE ?

predefined-variable

💬 Quel est le comportement du pipeline si un commit est poussé sur la branche principale ?

Le pipeline est déclenché

💬 Quel est le comportement du pipeline si un commit est poussé sur une branche qui a une merge request ouverte ?

Le pipeline de branche (source push) n’est pas créé, car la règle 2 correspond ($CI_COMMIT_BRANCH et $CI_OPEN_MERGE_REQUESTS sont définies) et applique when: never. En revanche, ce push génère un événement de merge request, et le pipeline de merge request est créé grâce à la règle 1. Il n’y a donc qu’un seul pipeline, pas deux.

Merge request, tag et API GitLab

Objectif

Prendre en main trois opérations :

L’appel à l’API permettra de récupérer les informations du job en cours d’exécution.

Lisez toute la consigne avant de commencer. La configuration CI/CD doit être enregistrée dans la branche source avant d’ouvrir la merge request.

Environnement

Pour cet exercice, vous travaillerez sur une instance fournie en VM (VMWare). Ce sera la même instance que vous aurez à l’évaluation finale.

Tous les messages de commit, y compris ceux des commits de fusion, doivent respecter Conventional Commits : https://www.conventionalcommits.org/en/v1.0.0/

Le tag demandé est v0.0.1.

Créer le projet

  1. Créer dans GitLab un projet vide nommé preparation324, sans README initial.
  2. Créer le dépôt local correspondant, avec main comme branche initiale.
  3. Ajouter le remote origin pointant vers le projet distant.
  4. Créer un fichier README.md contenant :
# Préparation CI/CD

Mon premier exercice avec GitLab (VM).
  1. Committer le fichier et pousser main.
  2. Vérifier que main est la branche par défaut du projet GitLab.

Préparer la branche de travail

Créer une branche nommée exercice depuis main.

Sur cette branche :

  1. Ajouter à la fin du README la ligne suivante :
Je prépare ma première merge request.
  1. Créer un fichier .gitlab-ci.yml à la racine du dépôt.
  2. Configurer les deux jobs décrits dans la section suivante.
  3. Committer les fichiers et pousser la branche.

Configurer les jobs

Les pipelines doivent être créés uniquement pour les merge requests et lors du push d’un tag.

Un push de branche sans MR ne doit pas créer de pipeline. Après la fusion de la MR, le push résultant sur main ne doit pas créer de pipeline supplémentaire.

Les noms imposés sont des noms de jobs. Le nom visible du pipeline est libre.

Job bonjour-mr

Ce job s’exécute uniquement dans un pipeline de merge request.

Il affiche dans les logs :

Bonjour depuis la merge request

Job appel-api

Ce job s’exécute uniquement lors du push d’un tag.

Il doit :

  1. Afficher le nom du tag dans les logs.
  2. Effectuer une requête GET avec curl vers l’API GitLab pour obtenir les informations du job en cours.
  3. Afficher directement la réponse JSON dans les logs.

Informations utiles :

Élément Valeur ou utilité
Méthode HTTP GET
Point d’entrée /job, relativement à l’URL de base de l’API
CI_API_V4_URL URL de base de l’API GitLab
CI_JOB_TOKEN Jeton automatiquement fourni par GitLab
En-tête d’authentification JOB-TOKEN
CI_COMMIT_TAG Nom du tag

Le point d’entrée /job renvoie les informations du job associé au jeton utilisé. Aucun token personnel n’est nécessaire.

L’appel doit être effectué depuis le script du job. Il suffit d’afficher le JSON : aucun traitement de la réponse n’est demandé.

Documentation : http://192.168.177.129:4000/19.5/api/jobs/#retrieve-a-job-by-job-token

Ne jamais afficher la valeur de CI_JOB_TOKEN dans les logs ni la placer dans le dépôt.

Créer et fusionner la merge request

Dans GitLab :

  1. Créer une merge request de exercice vers main.
  2. Vérifier qu’un pipeline démarre avec uniquement bonjour-mr.
  3. Vérifier le message affiché dans les logs.
  4. Fusionner la merge request avec un message de commit conforme.

Créer et pousser le tag

Dans Git Bash :

  1. Revenir sur main.
  2. Récupérer les modifications fusionnées dans GitLab.
  3. Créer le tag v0.0.1 sur le commit courant.
  4. Pousser ce tag vers GitLab.

Dans GitLab :

  1. Vérifier qu’un pipeline démarre avec uniquement appel-api.
  2. Ouvrir les logs.
  3. Vérifier que le nom du tag et la réponse JSON sont affichés.
  4. Repérer dans la réponse l’identifiant du job, son nom et la référence Git utilisée.
default:
  tags:
    - shell

workflow:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_TAG'
    - when: never

stages:
  - preparation

bonjour-mr:
  stage: preparation
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
  script:
    - echo "Bonjour depuis la merge request"

appel-api:
  stage: preparation
  rules:
    - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_TAG'
  script:
    - echo "$CI_COMMIT_TAG"
    - |
      curl --fail-with-body --silent --show-error \
        --request GET \
        --header "JOB-TOKEN: ${CI_JOB_TOKEN}" \
        "${CI_API_V4_URL}/job"

Currency converter, projet complet 😟

Objectif

Être en mesure de créer un pipeline GitLab CI/CD complet pour un projet de conversion de devises en utilisant les conditions de déclenchement et les règles de workflow.

Le travail s’effectue dans des branches séparées. Interdictions de travailler directement sur la branche main.

Le pipeline doit inclure les étapes suivantes :

  1. [T] Passer un linter sur le code. Si le linter retourne des erreurs, le pipeline doit s’arrêter.
  2. [T] Exécuter les tests unitaires. Si un des tests échoue, le pipeline doit s’arrêter.
  3. [T] Générer un fichier Changelog.md contenant les modifications apportées en se basant sur les messages de commit.
  4. [M] Builder le site web. Si le build échoue, le pipeline doit s’arrêter.
  5. [M] Créer un artifact docs.zip contenant le site web de documentation. Ce fichier zip pourra être téléchargé pendant 1 heure depuis l’interface GitLab, dézippé dans un dossier www de Laragon par exemple, et visualisé.
  6. [M] Déployer le site web builder sur un serveur de staging. Le déploiement doit être manuel ⏵.

[T] = Toujours, [M] = Uniquement lors d’un merge request sur main

Pour pouvoir déployer le site web sur un serveur de staging, vous devez disposer d’un accès FTP. Reportez-vous aux exercices précédent.

Pour le nom d’utilisateur et le mot de passe à utiliser avec la connexion FTP, vous devez utiliser les variables CI/CD. L’utilisation est détaillée dans la vidéo de M. Droz.

image: node:20

stages:
  - lint
  - test
  - changelog
  - build
  - package
  - deploy

# Règles du pipeline entier
workflow:
  rules:
    # Pipeline de merge request : toujours créé
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    # Push sur une branche qui a une MR ouverte : pas de doublon
    - if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS'
      when: never
    # Pas de pipeline sur la branche principale (on ne travaille pas dessus)
    - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
      when: never
    # Push sur toutes les autres branches de travail
    - if: '$CI_COMMIT_BRANCH'

cache:
  key:
    files:
      - package-lock.json
  paths:
    - node_modules/

# Règle réutilisable : uniquement lors d'une MR vers main
.only_mr_to_main:
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'

# ---------- [T] Toujours ----------

lint:
  stage: lint
  script:
    - npm ci
    - npm run lint        # code de sortie non nul = job en échec = pipeline arrêté

unit-tests:
  stage: test
  script:
    - npm ci
    - npm test            # un test qui échoue arrête le pipeline

changelog:
  stage: changelog
  variables:
    GIT_DEPTH: "0"        # historique complet pour lire les commits
  script:
    - git fetch origin "$CI_DEFAULT_BRANCH"
    - echo "# Changelog" > Changelog.md
    - echo "" >> Changelog.md
    - git log --pretty=format:"- %s (%h, %an)" "origin/$CI_DEFAULT_BRANCH..HEAD" >> Changelog.md
  artifacts:
    paths:
      - Changelog.md
    expire_in: 1 week

# ---------- [M] Uniquement lors d'une MR vers main ----------

build-site:
  stage: build
  extends: .only_mr_to_main
  script:
    - npm ci
    - npm run build       # échec du build = pipeline arrêté
  artifacts:
    paths:
      - dist/
    expire_in: 1 hour

docs-zip:
  stage: package
  extends: .only_mr_to_main
  dependencies: []          # pas besoin des artifacts de build-site
  script:
    - npm ci
    - npm run docs          # génère la doc à partir des commentaires du code source
    - apt-get update && apt-get install -y zip
    - cd docs && zip -r ../docs.zip .
  artifacts:
    name: docs
    paths:
      - docs.zip
    expire_in: 1 hour       # téléchargeable pendant 1 heure

deploy-staging:
  stage: deploy
  image: alpine:3.20
  environment:
    name: staging
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
      when: manual        # déploiement déclenché à la main
      allow_failure: false
  needs:
    - job: build-site
      artifacts: true
  script:
    - apk add --no-cache lftp
    - lftp -u "$FTP_USER","$FTP_PASSWORD" "$FTP_HOST" -e "mirror -R --delete dist/ $FTP_DIR; quit"