
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.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Comprendre
workflow: rulespour 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: manualpour valider un déploiement en production - Distinguer un pipeline de Merge Request d'un pipeline de branche
Quel lab commencer ?
Section intitulée « Quel lab commencer ? »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.
Dans quel contexte ?
Section intitulée « Dans quel contexte ? »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
maina 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
Prérequis
Section intitulée « Prérequis »- Lab 05 complété, ou partir directement de la branche
starter/lab-06 - Avoir lu Rules GitLab CI/CD (conseillé)
Point de départ
Section intitulée « Point de départ »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.
-
Passez sur la branche de départ
Fenêtre de terminal cd pipeline-craftgit checkout starter/lab-06 -
Poussez pour déclencher le pipeline
Fenêtre de terminal git push origin starter/lab-06Le pipeline démarre sur cette branche de feature, et déclenche un
docker pushcomplet, alors que vous ne testez que des règles CI. C'est exactement le comportement que vous allez corriger.
Le problème
Section intitulée « Le problème »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 :
| Contexte | Lint + Tests | Build Docker | Déploiement staging | Dé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
Section intitulée « L'exercice »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.
-
Ajoutez un bloc
workflow: rulesen haut du fichier, aprèsstages:etvariables:. -
Définissez trois cas de démarrage du pipeline :
- pipeline de Merge Request
- push sur la branche par défaut
- push de tag Git
-
Vérifiez le comportement attendu :
- push feature sans MR => pas de pipeline
- ouverture de MR => pipeline déclenché
👉 Vérifier votre solution (Étape 1)
1️⃣ Ajouter un workflow: rules global
Section intitulée « 1️⃣ Ajouter un workflow: rules global »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_TAG2️⃣ Ce que fait chaque règle
Section intitulée « 2️⃣ Ce que fait chaque règle »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 surmain$CI_COMMIT_TAG: démarre un pipeline pour un tag Git
3️⃣ Comportement attendu
Section intitulée « 3️⃣ Comportement attendu »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.
| Contexte | Pipeline 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.
Étape 2, À vous d'ajouter des rules par job
Section intitulée « Étape 2, À vous d'ajouter des rules par job »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.
-
Ajoutez des
rules:surruff-lintetpytestpour les exécuter sur :- Merge Request
- branche par défaut
-
Ajoutez des
rules:plus restrictives surdocker-build:- exécution sur branche par défaut
- déclenchement aussi quand des fichiers critiques changent
-
Vérifiez la logique de sélection :
- lint/tests restent actifs sur MR
- build Docker reste lié à
mainou à 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_BRANCH2️⃣ Restreindre docker-build
Section intitulée « 2️⃣ Restreindre docker-build »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_TAG3️⃣ Résultat attendu
Section intitulée « 3️⃣ Résultat attendu »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-lintetpytesttournent sur MR et surmain- en MR,
docker-buildtourne seulement si les fichiers critiques changent - sur
mainet sur tag,docker-buildtourne systématiquement - une modification de documentation seule ne déclenche pas
docker-builden 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.
-
Ajoutez le stage
deploydans la liste des stages. -
Créez un job de déploiement staging avec exécution automatique sur la branche par défaut.
-
Créez un job de déploiement production en mode manuel sur la branche par défaut.
-
Validez le résultat attendu : staging auto, production avec bouton manuel.
👉 Vérifier votre solution (Étape 3)
1️⃣ Ajouter le stage deploy
Section intitulée « 1️⃣ Ajouter le stage deploy »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 - deploy2️⃣ Déploiement staging automatique
Section intitulée « 2️⃣ Déploiement staging automatique »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_BRANCH3️⃣ Déploiement production manuel
Section intitulée « 3️⃣ Déploiement production manuel »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: manual4️⃣ Ce que vous devez observer
Section intitulée « 4️⃣ Ce que vous devez observer »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-stagingdémarre automatiquement après le build deploy-productionapparaî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.
Le fichier complet
Section intitulée « Le fichier complet »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: manualPousser et vérifier
Section intitulée « Pousser et vérifier »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.
-
Committez et poussez
Fenêtre de terminal git add .gitlab-ci.ymlgit commit -m "ci: add workflow rules, job rules and deploy stages"git push origin starter/lab-06 -
Observez : aucun pipeline ne démarre
La branche
starter/lab-06n'est pasmainet il n'y a pas de MR ouverte. Leworkflow: rulesfiltre ce push, aucun pipeline ne démarre. -
Ouvrez une Merge Request de
starter/lab-06versmainsur votre forkAllez dans Merge Requests > New merge request. Un pipeline démarre, il exécute
ruff-lintetpytest. Le jobdocker-buildne s'exécute que si la MR modifie des fichiers critiques (app/**/*,Dockerfile,requirements.txt). -
Poussez directement sur
mainpour voir le pipeline completFenêtre de terminal git checkout maingit merge starter/lab-06git push origin mainCette fois, tous les jobs tournent : lint, test, build Docker et déploiement staging. Le job
deploy-productionapparaît avec le bouton ▶ et attend votre clic.
Vérification
Section intitulée « Vérification »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-06seul (sans MR) ne déclenche aucun pipeline - Une MR ouverte déclenche lint + tests, et
docker-buildseulement si des fichiers critiques changent - Un push sur
maindéclenche lint + tests + build Docker + deploy staging - Le job
deploy-productionaffiche un bouton ▶ et ne s'exécute pas automatiquement
Pièges fréquents
Section intitulée « Pièges fréquents »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ôme | Cause | Solution |
|---|---|---|
| Pipeline ne démarre jamais | workflow: rules trop restrictif | Vérifier que la branche ou l'événement correspond à une règle du workflow: |
| Job toujours skipped | rules: ne correspond à aucun contexte actuel | Vérifier les valeurs des variables avec env | grep CI_ dans un job de debug |
deploy-production démarre automatiquement | when: manual absent de la règle | Ajouter when: manual sur la ligne de la règle (pas au niveau du job) |
| Double pipeline sur les MR | workflow: manquant, GitLab crée un pipeline de MR ET un pipeline de branche | Ajouter workflow: rules pour éviter les doublons |
Pour aller plus loin
Section intitulée « Pour aller plus loin »only/exceptvsrules:: les anciennes documentations utilisentonly:etexcept:. Ces mots-clés sont dépréciés, utilisez toujoursrules:qui est plus flexible et plus lisible.- Comparez avec
solution/lab-06:git diff origin/starter/lab-06..origin/solution/lab-06montre les modifications complètes.
À retenir
Section intitulée « À retenir »workflow: rulescontrô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: manualsur 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