Skip to content

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.

PageLien direct
Accueil de Jenkinsci.time.jostud.com
Le dossier des jobsci.time.jostud.com/job/TimeManager
Pipeline backendci.time.jostud.com/job/TimeManager/job/backend
Pipeline frontendci.time.jostud.com/job/TimeManager/job/frontend
Pipeline documentationci.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 :

ChampValeur
Utilisateuradmin
Mot de passejenkins_admin_password, dans le vault group_vars/tooling.yml

Pour lire le mot de passe, depuis timemanager-devops/ansible/ :

bash
ansible-vault view group_vars/tooling.yml

Le 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 ViewLe 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 NowRelance le pipeline à la main sur le dernier commit de main
Polling LogLe 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 ​

ÉtapeFrontendBackend
Testaucune (pas encore de tests, le build sert de vérification de compilation)mix test contre une base PostgreSQL jetable
Builddocker build avec VITE_API_URLdocker build
ScanTrivy, échoue sur toute faille CRITICALpareil
Push:<sha du commit> et :latest vers GHCRpareil
Deploydocker compose pull front && up -d frontpareil 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 ​

groovy
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()
    ...
}
LigneRôle
checkout scmRécupère le commit qui a déclenché le build
imageLe nom de l'image. toLowerCase() parce que GHCR refuse les majuscules.
tagLe hash court du commit, par exemple cece380

Le pipeline backend ​

Test ​

groovy
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étailPourquoi
--target builderOn n'a besoin que de l'étape avec Elixir, pas de l'image finale
${BUILD_NUMBER} dans les nomsDeux 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/_buildUn volume garde la compilation entre deux builds : les tests suivants sont beaucoup plus rapides
docker cp puis start -aLe 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 / finallyLes conteneurs de test sont supprimés même si les tests échouent

Build ​

groovy
sh "docker build -t ${image}:${tag} timemanager-api/"

L'image finale, étiquetée avec le hash du commit.

Scan ​

groovy
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 ​

groovy
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'
}
  • withCredentials rend le jeton disponible le temps du bloc, masqué dans les journaux.
  • --password-stdin : le jeton ne passe pas en argument de commande (visible avec ps).
  • 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 ​

groovy
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
        '
    """
}
  1. sshagent charge la clé vm-ssh-key pour la durée du bloc.
  2. Connexion en deploy à tm-prod par l'IP privée.
  3. docker compose pull back : télécharge la nouvelle image latest.
  4. docker compose up -d back : recrée seulement le conteneur back. La base et le site ne sont pas touchés.
  5. 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 ;
  • :latest est ce que la production fait tourner.

Le pipeline frontend ​

Les mêmes étapes, sans Test, avec un argument de build :

groovy
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 :

groovy
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é.

ÉtapeImageRôle
depsnode:22-slimnpm ci, en cache tant que package-lock.json ne change pas
buildernode:22-slim + Gitvitepress build
runnernginxinc/nginx-unprivileged:stable-alpineSert .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.

  1. Ajouter l'enregistrement A docs.time.jostud.com vers l'IP de tm-prod, chez Hostinger.
  2. Mettre à jour Jenkins pour qu'il crée le job : ansible-playbook playbook.yml --tags jenkins --ask-vault-pass.
  3. Fusionner dans main, ou lancer Build Now sur le job TimeManager/docs. L'image est construite et poussée. L'étape Deploy échoue cette première fois : le service docs n'existe pas encore sur tm-prod.
  4. 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.
  5. Ouvrir docs.time.jostud.com.

Les déploiements suivants sont automatiques.

Ce qui déclenche quoi ​

Changement dans mainFrontendBackendDocumentation
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 ​

ÉtapeOrdre de grandeur
Détection du commitjusqu'à 2 min
Tests backendenviron 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
Build1 à 5 min selon le cache Docker
Scanmoins d'une minute
Push et déploiementmoins d'une minute