Pipelines CI/CD
Trois pipelines, dans timemanager-devops/jenkins/pipelines/ : le site web, l'API et cette documentation. Ils sont écrits en Groovy scripté : node { ... } réserve un exécuteur, et chaque stage('...') est une étape visible dans Jenkins.
Accéder aux pipelines
Les pipelines tournent dans Jenkins, à l'adresse ci.time.jostud.com.
| Page | Lien direct |
|---|---|
| Accueil de Jenkins | ci.time.jostud.com |
| Le dossier des jobs | ci.time.jostud.com/job/TimeManager |
| Pipeline backend | ci.time.jostud.com/job/TimeManager/job/backend |
| Pipeline frontend | ci.time.jostud.com/job/TimeManager/job/frontend |
| Pipeline documentation | ci.time.jostud.com/job/TimeManager/job/docs |
| Vue Blue Ocean (plus visuelle) | ci.time.jostud.com/blue |
Se connecter
Sans connexion, Jenkins n'affiche rien (code 403) : un visiteur anonyme n'a aucun droit. Il faut se connecter avec :
| Champ | Valeur |
|---|---|
| Utilisateur | admin |
| Mot de passe | jenkins_admin_password, dans le vault group_vars/tooling.yml |
Pour lire le mot de passe, depuis timemanager-devops/ansible/ :
ansible-vault view group_vars/tooling.ymlLe mot de passe du vault se demande à Elias. Voir Secrets.
Ce qu'on y trouve
Sur la page d'un pipeline :
| Élément | À quoi il sert |
|---|---|
| Stage View | Le tableau des derniers builds, une colonne par étape (Test, Build, Scan, Push, Deploy), en vert ou en rouge, avec la durée de chacune |
| Build History (à gauche) | La liste des builds, du plus récent au plus ancien. Le numéro ouvre le détail d'un build. |
| Console Output (dans un build) | Le journal complet : chaque commande lancée et sa sortie. C'est là qu'on lit pourquoi un build a échoué. |
| Changes (dans un build) | Les commits qui ont déclenché ce build |
| Build Now | Relance le pipeline à la main sur le dernier commit de main |
| Polling Log | Le journal de la surveillance de GitHub (toutes les 2 minutes). Utile si un commit n'a rien déclenché. |
La vue Blue Ocean montre les mêmes informations sous forme de frise, plus lisible pour suivre un build en cours.
Lire un échec
Ouvre le build en rouge, puis Console Output, et descends jusqu'à la première ligne ERROR ou script returned exit code. Les causes fréquentes sont dans Dépannage.
Vue d'ensemble
| Étape | Frontend | Backend |
|---|---|---|
| Test | aucune (pas encore de tests, le build sert de vérification de compilation) | mix test contre une base PostgreSQL jetable |
| Build | docker build avec VITE_API_URL | docker build |
| Scan | Trivy, échoue sur toute faille CRITICAL | pareil |
| Push | :<sha du commit> et :latest vers GHCR | pareil |
| Deploy | docker compose pull front && up -d front | pareil pour back, les migrations tournent au démarrage |
Si une étape échoue, les suivantes ne sont pas lancées. Une image qui rate ses tests ou son scan n'arrive jamais en production. Après chaque déploiement, les images inutilisées sont supprimées sur tm-prod pour que le disque ne se remplisse pas.
Le début commun
node {
checkout scm
def image = "ghcr.io/${env.GHCR_OWNER.toLowerCase()}/timemanager-api"
def tag = sh(script: 'git rev-parse --short HEAD', returnStdout: true).trim()
...
}| Ligne | Rôle |
|---|---|
checkout scm | Récupère le commit qui a déclenché le build |
image | Le nom de l'image. toLowerCase() parce que GHCR refuse les majuscules. |
tag | Le hash court du commit, par exemple cece380 |
Le pipeline backend
Test
stage('Test') {
try {
sh "docker build --target builder -t tm-api-builder timemanager-api/"
sh "docker run -d --name ${testDb} -e POSTGRES_PASSWORD=postgres postgres:17-alpine"
sh "timeout 60 sh -c 'until docker exec ${testDb} pg_isready -U postgres; do sleep 1; done'"
sh "docker create --name ${testRunner} --network container:${testDb} -e MIX_ENV=test -v tm-test-build:/app/_build tm-api-builder sh -c 'mix deps.get && mix test'"
sh "docker cp timemanager-api/. ${testRunner}:/app"
sh "docker start -a ${testRunner}"
} finally {
sh "docker rm -f ${testRunner} ${testDb} || true"
}
}| Détail | Pourquoi |
|---|---|
--target builder | On n'a besoin que de l'étape avec Elixir, pas de l'image finale |
${BUILD_NUMBER} dans les noms | Deux builds en même temps n'utilisent pas les mêmes conteneurs |
--network container:${testDb} | Le conteneur de test partage l'espace réseau de la base : localhost dans config/test.exs la joint, sans rien changer au code |
-v tm-test-build:/app/_build | Un volume garde la compilation entre deux builds : les tests suivants sont beaucoup plus rapides |
docker cp puis start -a | Le code est copié dans le conteneur de test au lieu d'y monter les volumes de Jenkins : le code exécuté pendant les tests n'a ainsi jamais accès au socket Docker ni à la clé de déploiement |
try / finally | Les conteneurs de test sont supprimés même si les tests échouent |
Build
sh "docker build -t ${image}:${tag} timemanager-api/"L'image finale, étiquetée avec le hash du commit.
Scan
sh "docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy:0.74.0 image --exit-code 1 --severity CRITICAL --no-progress ${image}:${tag}"Trivy compare chaque paquet de l'image à une base de failles connues (CVE). --severity CRITICAL --exit-code 1 : s'il trouve une faille critique, il échoue, et le pipeline s'arrête. Les failles moins graves sont affichées dans les journaux sans bloquer.
Push
withCredentials([usernamePassword(credentialsId: 'ghcr', usernameVariable: 'GHCR_USER', passwordVariable: 'GHCR_TOKEN')]) {
sh 'echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_USER" --password-stdin'
sh "docker push ${image}:${tag}"
sh "docker tag ${image}:${tag} ${image}:latest"
sh "docker push ${image}:latest"
sh 'docker logout ghcr.io'
}withCredentialsrend le jeton disponible le temps du bloc, masqué dans les journaux.--password-stdin: le jeton ne passe pas en argument de commande (visible avecps).- L'image est poussée deux fois : avec le hash (pour garder l'historique) et avec
latest(ce que la production utilise). docker logoutà la fin : aucune session ne reste ouverte.
Guillemets simples ou doubles
'echo "$GHCR_TOKEN" ...' est entre guillemets simples : c'est le shell qui remplace $GHCR_TOKEN, pas Groovy. Le secret n'est jamais inséré dans le texte du script.
Deploy
sshagent(['vm-ssh-key']) {
sh """
ssh -o StrictHostKeyChecking=accept-new deploy@${env.PROD_HOST} '
cd /opt/timemanager/app &&
docker compose pull back &&
docker compose up -d back &&
docker image prune -af
'
"""
}sshagentcharge la clévm-ssh-keypour la durée du bloc.- Connexion en
deployà tm-prod par l'IP privée. docker compose pull back: télécharge la nouvelle imagelatest.docker compose up -d back: recrée seulement le conteneurback. La base et le site ne sont pas touchés.docker image prune -af: supprime les images qui ne servent plus, pour ne pas remplir le disque.
StrictHostKeyChecking=accept-new accepte l'empreinte de tm-prod la première fois, puis refuse si elle change.
Au redémarrage, le conteneur applique les migrations (voir Docker).
Les étiquettes d'images
Chaque image est poussée deux fois :
:<sha du commit>dit exactement de quel commit vient une image, et permet de revenir en arrière ;:latestest ce que la production fait tourner.
Le pipeline frontend
Les mêmes étapes, sans Test, avec un argument de build :
stage('Build') {
sh "docker build --build-arg VITE_API_URL=${env.VITE_API_URL} -t ${image}:${tag} timemanager-web/"
}VITE_API_URL vaut https://api.time.jostud.com/api. Il vient de casc.yml, qui le tient du .env, qui le tient d'Ansible (vite_api_url). Le déploiement fait pull front et up -d front.
Le pipeline documentation
docs.groovy suit le même modèle que le frontend (Build, Scan, Push, Deploy, sans Test), avec deux différences.
L'image est construite depuis la racine du dépôt :
sh "docker build -f timemanager-docs/Dockerfile -t ${image}:${tag} ."La page Base de données inclut docs/database.mmd, qui est hors de timemanager-docs/. Le fichier timemanager-docs/Dockerfile.dockerignore ne laisse entrer dans le contexte que timemanager-docs/, docs/ et .git.
L'historique Git sert au build : chaque page affiche « Mis à jour le », calculé à partir des commits. L'étape de build installe Git et reçoit .git. L'image finale, elle, ne contient que nginx et le site compilé.
| Étape | Image | Rôle |
|---|---|---|
deps | node:22-slim | npm ci, en cache tant que package-lock.json ne change pas |
builder | node:22-slim + Git | vitepress build |
runner | nginxinc/nginx-unprivileged:stable-alpine | Sert .vitepress/dist sur le port 8080, sans root |
La configuration nginx (timemanager-docs/nginx.conf) sert les adresses sans .html (/guide/projet lit guide/projet.html), renvoie la page 404 de la doc, et met en cache d'un an les fichiers compilés. Le déploiement fait pull docs et up -d docs sur tm-prod, et Traefik publie le conteneur sur docs.time.jostud.com.
La première mise en ligne
L'ordre compte : tant que l'image timemanager-docs n'existe pas sur GHCR, docker compose up -d ne peut pas démarrer le service.
- Ajouter l'enregistrement A
docs.time.jostud.comvers l'IP detm-prod, chez Hostinger. - Mettre à jour Jenkins pour qu'il crée le job :
ansible-playbook playbook.yml --tags jenkins --ask-vault-pass. - Fusionner dans
main, ou lancer Build Now sur le jobTimeManager/docs. L'image est construite et poussée. L'étape Deploy échoue cette première fois : le servicedocsn'existe pas encore surtm-prod. - Envoyer le nouveau compose et la nouvelle route :
ansible-playbook playbook.yml --tags traefik,app --ask-vault-pass. Le conteneur démarre, Traefik demande le certificat. - Ouvrir docs.time.jostud.com.
Les déploiements suivants sont automatiques.
Ce qui déclenche quoi
Changement dans main | Frontend | Backend | Documentation |
|---|---|---|---|
timemanager-web/** | ✓ | ||
timemanager-api/** | ✓ | ||
timemanager-docs/** | ✓ | ||
docs/database.mmd (inclus dans la doc) | ✓ | ||
timemanager-devops/jenkins/pipelines/frontend.groovy | ✓ | ||
timemanager-devops/jenkins/pipelines/backend.groovy | ✓ | ||
timemanager-devops/jenkins/pipelines/docs.groovy | ✓ | ||
timemanager-mobile/**, le reste de docs/ | |||
Autre chose dans timemanager-devops/ |
Un changement dans Ansible, Traefik ou le compose de production n'est pas déployé par Jenkins : il faut lancer Ansible. Voir Opérations courantes.
Combien de temps
| Étape | Ordre de grandeur |
|---|---|
| Détection du commit | jusqu'à 2 min |
| Tests backend | environ 4 minutes la première fois (les dépendances sont compilées de zéro), beaucoup moins ensuite grâce au cache tm-test-build |
| Build | 1 à 5 min selon le cache Docker |
| Scan | moins d'une minute |
| Push et déploiement | moins d'une minute |