Ce tutoriel construit un pipeline Jenkins complet avec Jenkinsfile déclaratif : checkout Git, installation des dépendances, tests unitaires, build de l'image Docker, push sur le registry et déploiement automatique — le tout automatisé à chaque push.
CI/CD : Continuous Integration / Continuous Deployment. Automatisation de la compilation, tests et déploiement chaque fois que vous poussez du code. Versus déploiement manuel et lent.
Pipeline : Séquence d'étapes automatisées pour traiter votre code (compiler → tester → packager → déployer). Chaque étape est un "stage".
Stage : Une étape du pipeline (ex: "Build", "Test", "Deploy"). Les stages s'exécutent séquentiellement par défaut, mais peuvent être parallèles.
Jenkins : Serveur open-source qui exécute des pipelines. Il peut être déclenché par un webhook Git (code poussé = pipeline lancé automatiquement).
Jenkinsfile : Fichier qui décrit le pipeline en code (versionnéavec votre code source). Deux syntaxes : déclarative (simple) ou scripted (flexible).
Webhook : URL HTTP que Git appelle chaque fois qu'on pousse du code. Jenkins écoute et démarre le pipeline automatiquement.
Credential : Données sensibles stockées dans Jenkins de manière sécurisée (tokens, clés SSH, passwords). Jamais en clair dans le Jenkinsfile.
Artifact : Résultat d'une étape du pipeline (ex: fichiers compilés, images Docker, reports de tests). Jenkins peut les archiver et les servir.
Git Push → Jenkins Webhook
└── Stage 1 : Checkout (clone le code)
└── Stage 2 : Install (npm ci)
└── Stage 3 : Lint & Test (ESLint + Jest) [parallèles]
└── Stage 4 : Build (npm run build → dist/)
└── Stage 5 : Docker Build & Push (image :latest + :git-sha)
└── Stage 6 : Deploy (docker pull + restart sur serveur SSH)
└── Stage 7 : Notify (Slack / Email)
Créons une application React basique avec des tests unitaires. Jenkins exécutera ces tests automatiquement.
npx create-react-app mon-dashboard
cd mon-dashboard
# Installer les dépendances de test
npm install --save-dev @testing-library/react @testing-library/jest-dom
toBeInTheDocument()).
Ajoutons un test unitaire simple :
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import App from './App';
test('renders application title', () => {
render(<App />);
const title = screen.getByText(/Mon Dashboard/i);
expect(title).toBeInTheDocument();
});
test('navigation is visible', () => {
render(<App />);
expect(screen.getByRole('navigation')).toBeInTheDocument();
});
Utilisons une build multi-stage : compiler avec Node en stage 1, puis servir les fichiers statiques avec Nginx en stage 2. Avantage : l'image finale ne contient que Nginx + fichiers compilés, sans Node.js.
# ── Stage 1 : Build ──
FROM node:20-alpine AS builder
WORKDIR /app
# Installer les dépendances (cache optimisé)
COPY package*.json ./
RUN npm ci
# Copier le code et compiler
COPY . .
RUN npm run build
# ── Stage 2 : Serve ──
FROM nginx:alpine AS runner
# Config Nginx pour React Router (SPA)
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Copier le build depuis le stage précédent
COPY --from=builder /app/build /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
build/ avec l'HTML minifié et le JavaScript optimisé.server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Nécessaire pour React Router (toutes les routes → index.html)
location / {
try_files $uri $uri/ /index.html;
}
# Cache pour les assets (JS, CSS, images)
location ~* \.(js|css|png|jpg|svg|ico)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# Health check pour les orchestrateurs
location /health {
return 200 '{"status":"healthy"}';
add_header Content-Type application/json;
}
}
Le Jenkinsfile décrit le pipeline en code Groovy. Syntaxe déclarative : lisible, structurée, avec pipeline { agent ... environment ... stages ... }.
pipeline {
agent any
// ── Variables d'environnement du pipeline ──
environment {
REGISTRY = 'registry.example.com'
IMAGE_NAME = 'mon-dashboard'
IMAGE_TAG = "${REGISTRY}/${IMAGE_NAME}:${GIT_COMMIT[0..7]}"
IMAGE_LATEST = "${REGISTRY}/${IMAGE_NAME}:latest"
DEPLOY_SERVER = 'user@mon-serveur.com'
NODE_VERSION = '20'
}
// ── Options du pipeline ──
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
timeout(time: 20, unit: 'MINUTES')
disableConcurrentBuilds() // Pas de builds parallèles sur la même branche
}
stages {
// ── Stage 1 : Checkout ──
stage('📥 Checkout') {
steps {
checkout scm
sh 'git log --oneline -5' // Afficher les derniers commits
script {
env.GIT_SHORT = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
env.BRANCH = env.GIT_BRANCH.replace('origin/', '')
}
echo "Branch: ${env.BRANCH} | Commit: ${env.GIT_SHORT}"
}
}
// ── Stage 2 : Installation des dépendances ──
stage('📦 Install') {
steps {
sh '''
node --version
npm --version
npm ci --prefer-offline
'''
}
}
// ── Stage 3 : Tests (parallèles) ──
stage('🧪 Lint & Test') {
parallel {
stage('ESLint') {
steps {
sh 'npm run lint -- --format checkstyle --output-file reports/eslint.xml || true'
}
post {
always {
// Publier les résultats ESLint dans Jenkins
recordIssues tools: [esLint(pattern: 'reports/eslint.xml')]
}
}
}
stage('Unit Tests') {
steps {
sh '''
npm test -- \
--watchAll=false \
--coverage \
--coverageReporters=cobertura \
--coverageDirectory=reports/coverage \
--reporters=jest-junit \
--testResultsProcessor=jest-junit
'''
}
post {
always {
junit 'reports/junit.xml'
cobertura coberturaReportFile: 'reports/coverage/cobertura-coverage.xml'
}
}
}
}
}
// ── Stage 4 : Build ──
stage('🔨 Build') {
steps {
sh '''
GENERATE_SOURCEMAP=false npm run build
echo "Build terminé — taille du bundle :"
du -sh build/
'''
archiveArtifacts artifacts: 'build/**', fingerprint: true
}
}
// ── Stage 5 : Docker Build & Push ──
stage('🐳 Docker') {
when {
anyOf {
branch 'main'
branch 'staging'
}
}
steps {
script {
withCredentials([usernamePassword(
credentialsId: 'docker-registry-credentials',
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASS'
)]) {
sh """
echo "$DOCKER_PASS" | docker login ${REGISTRY} -u "$DOCKER_USER" --password-stdin
docker build -t ${IMAGE_TAG} -t ${IMAGE_LATEST} .
docker push ${IMAGE_TAG}
docker push ${IMAGE_LATEST}
docker logout ${REGISTRY}
"""
}
}
}
}
// ── Stage 6 : Déploiement ──
stage('🚀 Deploy') {
when { branch 'main' }
steps {
script {
withCredentials([sshUserPrivateKey(
credentialsId: 'deploy-ssh-key',
keyFileVariable: 'SSH_KEY'
)]) {
sh """
ssh -i $SSH_KEY -o StrictHostKeyChecking=no ${DEPLOY_SERVER} \\
"docker pull ${IMAGE_TAG} && \\
docker stop mon-dashboard || true && \\
docker rm mon-dashboard || true && \\
docker run -d --name mon-dashboard \\
-p 80:80 \\
--restart unless-stopped \\
${IMAGE_TAG}"
"""
}
}
}
}
}
// ── Notifications post-build ──
post {
success {
echo "✅ Build ${env.BUILD_NUMBER} réussi — ${env.BRANCH}:${env.GIT_SHORT}"
// slackSend channel: '#deploys', color: 'good',
// message: "✅ ${env.IMAGE_NAME} déployé — ${env.GIT_SHORT}"
}
failure {
echo "❌ Build ${env.BUILD_NUMBER} échoué"
// emailext to: 'team@example.com', subject: "❌ Build failed", body: "..."
}
always {
// Nettoyage du workspace
cleanWs()
}
}
}
${REGISTRY}/${IMAGE_NAME}:${GIT_COMMIT[0..7]} génère un tag comme registry.example.com/mon-dashboard:a1b2c3d4.stage('📥 Checkout') {
steps {
checkout scm // Clone le repo Git
sh 'git log --oneline -5' // Affiche derniers commits
script {
env.GIT_SHORT = sh(script: ...).trim() // Hash court (a1b2c3d)
env.BRANCH = env.GIT_BRANCH.replace() // Nom branche sans 'origin/'
}
}
}
scm = "Source Code Management".stage('📦 Install') {
steps {
sh '''
node --version // Vérifie que Node est dispo
npm ci --prefer-offline // Clean install (vs npm install)
'''
}
}
package-lock.json (reproductible, vs npm install qui peut varier). --prefer-offline : Utilise le cache npm local si possible.
stage('🧪 Lint & Test') {
parallel { // Deux stages en parallèle = plus rapide
stage('ESLint') {
steps {
sh 'npm run lint -- --format checkstyle ...'
}
post {
always {
recordIssues tools: [esLint(...)] // Jenkins affiche les warnings
}
}
}
stage('Unit Tests') {
steps {
sh 'npm test -- --watchAll=false --coverage ...'
}
post {
always {
junit 'reports/junit.xml' // Publier résultats tests
cobertura ... // Publier couverture de code
}
}
}
}
}
stage('🔨 Build') {
steps {
sh '''
GENERATE_SOURCEMAP=false npm run build // Compile React (optimisé, sourcemaps désactivés)
du -sh build/ // Affiche taille du build
'''
archiveArtifacts artifacts: 'build/**' // Archiver les fichiers pour téléchargement
}
}
build/.stage('🐳 Docker') {
when { anyOf { branch 'main'; branch 'staging' } } // Only on main/staging
steps {
script {
withCredentials([usernamePassword( // Credentials chiffré
credentialsId: 'docker-registry-credentials',
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASS'
)]) {
sh """
echo "$DOCKER_PASS" | docker login ... // Login sécurisé (pas en clair)
docker build -t ${IMAGE_TAG} -t ${IMAGE_LATEST} .
docker push ${IMAGE_TAG} // :a1b2c3d4 (hash commit)
docker push ${IMAGE_LATEST} // :latest
docker logout ${REGISTRY} // Cleanup
"""
}
}
}
}
main ou staging (pas sur chaque branche de feature).registry.example.com/mon-dashboard:a1b2c3d4 et :latest).:latest permet de toujours référencer "la dernière version". Le tag par hash (ex: :a1b2c3d4) permet de tracer exactement quelle version du code est en prod (utile si un déploiement échoue, vous pouvez rollback au hash précédent).
stage('🚀 Deploy') {
when { branch 'main' } // Only main deploys to prod
steps {
script {
withCredentials([sshUserPrivateKey(
credentialsId: 'deploy-ssh-key',
keyFileVariable: 'SSH_KEY'
)]) {
sh """
ssh -i $SSH_KEY -o StrictHostKeyChecking=no ${DEPLOY_SERVER} \\
"docker pull ${IMAGE_TAG} && \\
docker stop mon-dashboard || true && \\
docker rm mon-dashboard || true && \\
docker run -d --name mon-dashboard \\
-p 80:80 \\
--restart unless-stopped \\
${IMAGE_TAG}"
"""
}
}
}
}
ubuntu@prod.example.com) avec la clé SSH.|| true évite une erreur si le conteneur n'existe pas.-p 80:80 expose le port 80 (HTTP). --restart unless-stopped redémarre automatiquement le conteneur si l'instance crash (mais pas si vous l'arrêtez manuellement).
Clés SSH : Jenkins utilise une clé SSH privée pour se connecter au serveur sans password. Cette clé doit être protégée (permissions 600) et stockée sécurisée dans Jenkins.
StrictHostKeyChecking=no : Désactive la vérification du host key (pratique en CI, mais moins sûr — en prod mieux utiliser yes et pré-ajouter la clé).
Credentials chiffré : Jenkins chiffre tous les secrets (passwords, clés) et les injecte que lors de l'exécution (jamais dans les logs).
# Lancer Jenkins en local
docker run -d \
--name jenkins \
-p 8080:8080 \
-p 50000:50000 \
-v jenkins_home:/var/jenkins_home \
-v /var/run/docker.sock:/var/run/docker.sock \ # Accès au daemon Docker de l'hôte
jenkins/jenkins:lts-jdk17
# Récupérer le mot de passe initial
docker exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
http://localhost:8080.docker build et docker push). C'est du "Docker-in-Docker" via socket.
docker-registry-credentials — username/password du registry Dockerdeploy-ssh-key — clé SSH pour le serveur de déploiementConfigurez un webhook dans votre repo GitHub pour déclencher le pipeline Jenkins automatiquement : Settings → Webhooks → Ajouter URL http://jenkins.example.com/github-webhook/ et sélectionner "Push events". À chaque git push, GitHub notifie Jenkins qui lance le pipeline immédiatement.