Aller au contenu
CI/CD & Automatisation medium

Lab 06, Contrôler l'exécution

21 min de lecture

logo gitlab

Votre pipeline s'exécute sur chaque push, peu importe la branche. Un développeur qui pousse 10 commits sur une branche de feature déclenche 10 builds Docker complets et 10 docker push, pour rien. Et le déploiement en production s'exécute automatiquement dès qu'un commit atterrit sur main, sans validation humaine. Dans ce lab, vous allez mettre des conditions d'exécution sur le pipeline et les jobs pour qu'ils ne tournent qu'au bon moment.

  • Comprendre workflow: rules pour contrôler quand un pipeline démarre
  • Écrire des rules: par job avec des conditions sur la branche, l'événement ou les fichiers modifiés
  • Créer un job manuel avec when: manual pour valider un déploiement en production
  • Distinguer un pipeline de Merge Request d'un pipeline de branche

Ce lab est la suite logique du Lab 05. Si vous débutez sur GitLab CI/CD, faites 01 à 05 avant de traiter les rules. Si vous êtes déjà à l'aise avec la syntaxe YAML, vous pouvez démarrer directement sur starter/lab-06.

Un pipeline sans condition d'exécution est un pipeline qui fait trop de choses trop souvent. Sur un projet avec 5 développeurs qui poussent chacun 5 commits par jour sur leurs branches, ça représente 25 builds Docker inutiles chaque jour. Les runners partagés de gitlab.com ont des quotas de minutes CI, mieux vaut ne pas les gaspiller.

Plus important encore : un déploiement en production qui se déclenche automatiquement à chaque merge est risqué. En pratique, toutes les équipes mettent un frein au moins sur la production : quelqu'un doit valider manuellement avant que le code parte en prod.

Situations réelles où ce lab vous aide :

  • Votre quota de minutes CI est épuisé mi-mois à cause de builds inutiles sur des branches de feature
  • Un merge accidentel sur main a déclenché un déploiement en production avant que vous puissiez réagir
  • Vous voulez que le lint et les tests tournent sur les MR, avec un build Docker seulement quand des fichiers critiques changent
  • Vous cherchez à ajouter un bouton « déployer en production » dans GitLab sans plugins supplémentaires

La branche starter/lab-06 contient le pipeline tel qu'il sort du lab précédent : fonctionnel, mais sans aucune condition. Poussez-la telle quelle avant de modifier quoi que ce soit, pour observer le comportement que vous allez corriger. Ce premier push consomme volontairement des minutes CI, c'est le coût du constat.

  1. Passez sur la branche de départ

    Fenêtre de terminal
    cd pipeline-craft
    git checkout starter/lab-06
  2. Poussez pour déclencher le pipeline

    Fenêtre de terminal
    git push origin starter/lab-06

    Le pipeline démarre sur cette branche de feature, et déclenche un docker push complet, alors que vous ne testez que des règles CI. C'est exactement le comportement que vous allez corriger.

Le .gitlab-ci.yml de départ ne contient aucune condition d'exécution. Chaque push, sur n'importe quelle branche, déclenche l'intégralité du pipeline : lint, test, build et push Docker.

Le pipeline dont vous avez besoin doit se comporter différemment selon le contexte :

ContexteLint + TestsBuild DockerDéploiement stagingDéploiement prod
Push sur une branche de feature
Merge Request ouverte✅ si fichiers critiques modifiés
Push sur main✅ (auto)✅ (manuel)
Tag Git

Pour obtenir ce comportement, vous allez utiliser deux mécanismes de GitLab CI : les workflow: rules (qui contrôlent si un pipeline démarre) et les rules: par job (qui contrôlent si un job s'exécute dans un pipeline démarré).

L'exercice se fait en trois temps, du plus global au plus fin : d'abord le workflow: qui décide de l'existence même du pipeline, ensuite les rules: de chaque job, enfin les deux jobs de déploiement. Chaque étape énonce ce qu'il faut obtenir, pas la solution ; le bloc dépliant qui la suit donne le code et l'explication. Écrivez votre version avant de l'ouvrir, c'est là que le mécanisme s'ancre.

Étape 1, À vous de contrôler quand le pipeline démarre

Section intitulée « Étape 1, À vous de contrôler quand le pipeline démarre »

Le mot-clé workflow: s'évalue avant tout le reste : si aucune de ses règles ne correspond au contexte, GitLab ne crée aucun pipeline et vous ne verrez rien du tout dans l'interface, pas même un pipeline « skipped ». Les règles sont lues dans l'ordre et la première qui correspond l'emporte.

  1. Ajoutez un bloc workflow: rules en haut du fichier, après stages: et variables:.

  2. Définissez trois cas de démarrage du pipeline :

    • pipeline de Merge Request
    • push sur la branche par défaut
    • push de tag Git
  3. Vérifiez le comportement attendu :

    • push feature sans MR => pas de pipeline
    • ouverture de MR => pipeline déclenché
👉 Vérifier votre solution (Étape 1)

Placez ce bloc après stages: et avant les jobs :

workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG

Aucune de ces règles ne porte de when:, elles utilisent donc la valeur par défaut when: on_success, ce qui revient à autoriser le pipeline. Les trois contextes couverts sont exclusifs les uns des autres.

  • $CI_PIPELINE_SOURCE == "merge_request_event" : démarre un pipeline de Merge Request
  • $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH : démarre un pipeline sur main
  • $CI_COMMIT_TAG : démarre un pipeline pour un tag Git

Confrontez ce tableau à ce que vous voyez dans Build > Pipelines après chaque push. La première ligne est celle qui surprend le plus : le push réussit, mais rien n'apparaît côté CI.

ContextePipeline démarre ?
Push sur feature/foo sans MR❌ Non
Ouverture ou mise à jour d'une MR✅ Oui
Push direct sur main✅ Oui
Création d'un tag Git✅ Oui

Le point important : workflow: décide si GitLab crée un pipeline. Si aucune règle ne matche, le pipeline n'existe même pas.

Le workflow: a réduit le nombre de pipelines, il reste à réduire le nombre de jobs à l'intérieur de chacun. Un job sans rules: s'exécute dans tous les pipelines créés, ce qui rendrait le travail précédent à moitié inutile. L'objectif ici est de garder le lint et les tests partout où ils apportent une information, et de réserver le build Docker aux cas où l'image change réellement.

  1. Ajoutez des rules: sur ruff-lint et pytest pour les exécuter sur :

    • Merge Request
    • branche par défaut
  2. Ajoutez des rules: plus restrictives sur docker-build :

    • exécution sur branche par défaut
    • déclenchement aussi quand des fichiers critiques changent
  3. Vérifiez la logique de sélection :

    • lint/tests restent actifs sur MR
    • build Docker reste lié à main ou à une modification applicative significative
👉 Vérifier votre solution (Étape 2)

1️⃣ Ajouter des rules: sur ruff-lint et pytest

Section intitulée « 1️⃣ Ajouter des rules: sur ruff-lint et pytest »

Ces deux jobs reçoivent exactement les mêmes conditions, parce qu'ils répondent au même besoin : donner un retour rapide sur la qualité du code. Le reste de leur définition ne change pas, seul le bloc rules: est ajouté en fin de job.

ruff-lint:
stage: lint
image: python:3.12-slim@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de
cache:
key:
files:
- requirements-dev.txt
paths:
- .pip-cache/
policy: pull
before_script:
- pip install ruff
script:
- ruff check app/ tests/
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
pytest:
stage: test
image: python:3.12-slim@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de
cache:
key:
files:
- requirements-dev.txt
paths:
- .pip-cache/
before_script:
- pip install -r requirements-dev.txt
script:
- pytest -v --junitxml=report.xml
artifacts:
when: always
paths:
- report.xml
reports:
junit: report.xml
expire_in: 7 days
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

C'est le job le plus coûteux du pipeline, donc celui qui mérite la règle la plus fine. La clé changes: placée sous une condition la restreint encore : le job ne tourne en Merge Request que si l'un des chemins listés a été modifié. Sur la branche par défaut et sur un tag, aucune restriction de ce type, l'image doit exister quoi qu'il arrive.

docker-build:
stage: build
image: docker:27@sha256:aa3df78ecf320f5fafdce71c659f1629e96e9de0968305fe1de670e0ca9176ce
services:
- docker:27-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker build -t $CI_REGISTRY_IMAGE:latest .
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- app/**/*
- Dockerfile
- requirements.txt
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG

Un job dont aucune règle ne correspond n'échoue pas : il apparaît en skipped dans le pipeline, ou disparaît de la vue selon le contexte. Voici ce que vous devez constater.

  • ruff-lint et pytest tournent sur MR et sur main
  • en MR, docker-build tourne seulement si les fichiers critiques changent
  • sur main et sur tag, docker-build tourne systématiquement
  • une modification de documentation seule ne déclenche pas docker-build en MR

Dans la branche solution/lab-06, les règles de docker-build sont explicites par contexte : MR ciblée avec changes, branche par défaut, et tag Git.

Étape 3, À vous d'ajouter un stage de déploiement

Section intitulée « Étape 3, À vous d'ajouter un stage de déploiement »

Les deux jobs de cette étape partagent la même condition de branche et ne diffèrent que par un mot-clé. Staging se déclenche seul, production attend un clic. Un stage deploy déclaré en dernier garantit qu'aucun déploiement ne démarre avant que le build ait réussi.

  1. Ajoutez le stage deploy dans la liste des stages.

  2. Créez un job de déploiement staging avec exécution automatique sur la branche par défaut.

  3. Créez un job de déploiement production en mode manuel sur la branche par défaut.

  4. Validez le résultat attendu : staging auto, production avec bouton manuel.

👉 Vérifier votre solution (Étape 3)

L'ordre de cette liste est l'ordre d'exécution : deploy doit venir en dernier. Un job dont le stage: ne figure pas dans cette liste fait échouer la validation du fichier entier.

stages:
- lint
- test
- build
- deploy

Une seule règle, sans when:, donc exécution automatique dès que le pipeline tourne sur la branche par défaut. Le script d'exemple ne déploie rien de réel, il matérialise l'étape pour que vous observiez l'enchaînement.

deploy-staging:
stage: deploy
image: alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc
script:
- echo "Deploying $CI_COMMIT_SHORT_SHA to staging..."
- ./scripts/deploy-demo.sh staging
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Ce job est identique au précédent à un mot-clé près. Notez l'indentation de when: manual : il est placé sous la condition if:, à l'intérieur de la règle, et non au niveau du job. Placé au mauvais endroit, il s'appliquerait indépendamment du contexte.

deploy-production:
stage: deploy
image: alpine:3.20
script:
- echo "Deploying $CI_COMMIT_SHORT_SHA to production..."
- ./scripts/deploy-demo.sh production
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual

Le pipeline se termine en statut bloqué, pas en succès, tant que le job manuel n'a pas été lancé. C'est normal et c'est précisément l'effet recherché.

  • sur main, deploy-staging démarre automatiquement après le build
  • deploy-production apparaît avec un bouton ▶
  • tant que personne ne clique, la production ne part pas

when: manual doit être placé dans la règle qui matche le contexte. C'est ce qui ajoute une validation humaine avant l'exécution.

Voici le .gitlab-ci.yml résultant des trois étapes, à comparer avec le vôtre. Attention à l'emplacement du bloc workflow: : il est au même niveau d'indentation que stages: et que les jobs, jamais à l'intérieur de l'un d'eux.

📄 Voir le fichier .gitlab-ci.yml complet
stages:
- lint
- test
- build
- deploy
workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
ruff-lint:
stage: lint
image: python:3.12-slim@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de
cache:
key:
files:
- requirements-dev.txt
paths:
- .pip-cache/
policy: pull
before_script:
- pip install ruff
script:
- ruff check app/ tests/
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
pytest:
stage: test
image: python:3.12-slim@sha256:57cd7c3a7a273101a6485ba99423ee568157882804b1124b4dd04266317710de
cache:
key:
files:
- requirements-dev.txt
paths:
- .pip-cache/
before_script:
- pip install -r requirements-dev.txt
script:
- pytest -v --junitxml=report.xml
artifacts:
when: always
paths:
- report.xml
reports:
junit: report.xml
expire_in: 7 days
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
docker-build:
stage: build
image: docker:27@sha256:aa3df78ecf320f5fafdce71c659f1629e96e9de0968305fe1de670e0ca9176ce
services:
- docker:27-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA .
- docker build -t $CI_REGISTRY_IMAGE:latest .
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes:
- app/**/*
- Dockerfile
- requirements.txt
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_TAG
deploy-staging:
stage: deploy
image: alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc
script:
- echo "Deploying $CI_COMMIT_SHORT_SHA to staging..."
- ./scripts/deploy-demo.sh staging
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
deploy-production:
stage: deploy
image: alpine:3.20@sha256:d9e853e87e55526f6b2917df91a2115c36dd7c696a35be12163d44e6e2a4b6bc
script:
- echo "Deploying $CI_COMMIT_SHORT_SHA to production..."
- ./scripts/deploy-demo.sh production
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual

Les quatre manipulations suivantes couvrent les trois contextes configurés : push simple, Merge Request, puis push sur la branche par défaut. Faites-les dans l'ordre, chacune valide une règle différente et l'absence de pipeline à la deuxième étape est un résultat, pas une panne.

  1. Committez et poussez

    Fenêtre de terminal
    git add .gitlab-ci.yml
    git commit -m "ci: add workflow rules, job rules and deploy stages"
    git push origin starter/lab-06
  2. Observez : aucun pipeline ne démarre

    La branche starter/lab-06 n'est pas main et il n'y a pas de MR ouverte. Le workflow: rules filtre ce push, aucun pipeline ne démarre.

  3. Ouvrez une Merge Request de starter/lab-06 vers main sur votre fork

    Allez dans Merge Requests > New merge request. Un pipeline démarre, il exécute ruff-lint et pytest. Le job docker-build ne s'exécute que si la MR modifie des fichiers critiques (app/**/*, Dockerfile, requirements.txt).

  4. Poussez directement sur main pour voir le pipeline complet

    Fenêtre de terminal
    git checkout main
    git merge starter/lab-06
    git push origin main

    Cette fois, tous les jobs tournent : lint, test, build Docker et déploiement staging. Le job deploy-production apparaît avec le bouton ▶ et attend votre clic.

Le lab est réussi quand les quatre affirmations ci-dessous sont vraies en même temps. La première se vérifie dans Build > Pipelines, où rien ne doit apparaître ; les trois autres se lisent sur la page du pipeline concerné.

  • Un push sur starter/lab-06 seul (sans MR) ne déclenche aucun pipeline
  • Une MR ouverte déclenche lint + tests, et docker-build seulement si des fichiers critiques changent
  • Un push sur main déclenche lint + tests + build Docker + deploy staging
  • Le job deploy-production affiche un bouton ▶ et ne s'exécute pas automatiquement

Les erreurs sur les rules: se ressemblent toutes : quelque chose ne se déclenche pas, et rien n'explique pourquoi. Le réflexe utile est de remonter dans l'ordre d'évaluation, du workflow: vers les règles du job, car un pipeline absent ne produit aucun message d'erreur.

SymptômeCauseSolution
Pipeline ne démarre jamaisworkflow: rules trop restrictifVérifier que la branche ou l'événement correspond à une règle du workflow:
Job toujours skippedrules: ne correspond à aucun contexte actuelVérifier les valeurs des variables avec env | grep CI_ dans un job de debug
deploy-production démarre automatiquementwhen: manual absent de la règleAjouter when: manual sur la ligne de la règle (pas au niveau du job)
Double pipeline sur les MRworkflow: manquant, GitLab crée un pipeline de MR ET un pipeline de brancheAjouter workflow: rules pour éviter les doublons
  • only/except vs rules: : les anciennes documentations utilisent only: et except:. Ces mots-clés sont dépréciés, utilisez toujours rules: qui est plus flexible et plus lisible.
  • Comparez avec solution/lab-06 : git diff origin/starter/lab-06..origin/solution/lab-06 montre les modifications complètes.
  • workflow: rules contrôle si un pipeline démarre. rules: par job contrôle si un job s'exécute dans un pipeline démarré. Les deux se complètent.
  • Les variables clés : $CI_PIPELINE_SOURCE (source du pipeline), $CI_DEFAULT_BRANCH (branche principale), $CI_COMMIT_TAG (tag Git)
  • when: manual sur une règle ajoute un bouton ▶ dans l'interface, le job ne démarre que sur clic humain. C'est la façon la plus simple d'ajouter une validation avant un déploiement
  • La règle changes: évite des builds inutiles quand seule la documentation change

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn