
Votre pipeline doit créer des jobs différents selon le contexte ? Les pipelines dynamiques permettent de générer la configuration CI/CD à la volée. Un job crée le YAML, un autre l’exécute.
Ce guide est fait pour vous si…
Section intitulée « Ce guide est fait pour vous si… »Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »À la fin de ce module, vous saurez :
- Générer un pipeline YAML : script qui crée le fichier de config
- Déclencher avec un artefact :
include: artifactpour l’enfant - Détecter les changements : git diff pour générer les jobs nécessaires
- Utiliser une config externe : YAML, JSON ou API comme source
- Valider le YAML généré : éviter les erreurs de syntaxe
- Gérer le cas vide : que faire quand aucun job n’est nécessaire
Prérequis
Section intitulée « Prérequis »Avant de continuer, assurez-vous de maîtriser :
Principe des pipelines dynamiques
Section intitulée « Principe des pipelines dynamiques »La différence avec les pipelines classiques
Section intitulée « La différence avec les pipelines classiques »Pipeline classique : tout est écrit à l’avance dans .gitlab-ci.yml. GitLab lit le fichier et exécute les jobs définis.
Pipeline dynamique : un job génère un fichier YAML, puis GitLab exécute ce fichier généré.
Le flux en 3 étapes
Section intitulée « Le flux en 3 étapes »┌──────────────┐ ┌──────────────┐ ┌──────────────────┐│ generate │───▶│ child.yml │───▶│ pipeline enfant ││ (script) │ │ (artefact) │ │ (exécution) │└──────────────┘ └──────────────┘ └──────────────────┘ ① GÉNÈRE ② STOCKE ③ EXÉCUTEÉtape par étape :
- Générer : Un job (shell, Python, etc.) analyse le contexte et écrit un fichier YAML valide
- Stocker : Ce fichier est sauvegardé comme artefact du job
- Exécuter : Un job trigger utilise
include: artifact:pour lancer ce pipeline comme enfant

Pourquoi c’est puissant ?
Section intitulée « Pourquoi c’est puissant ? »| Besoin | Solution statique (rules) | Solution dynamique |
|---|---|---|
| 3 clients fixes | ✅ parallel:matrix | Overkill |
| Clients lus depuis une base | ❌ Impossible | ✅ Script qui lit la DB |
| Jobs selon fichiers modifiés | ⚠️ changes: (limité) | ✅ git diff + génération |
| 1 job par ligne d’un fichier | ❌ Impossible | ✅ Script qui parse le fichier |
Configuration de base
Section intitulée « Configuration de base »Comprendre la structure
Section intitulée « Comprendre la structure »Un pipeline dynamique nécessite deux jobs :
- Le générateur : crée le fichier YAML
- Le trigger : exécute le fichier généré
Pipeline parent : exemple commenté
Section intitulée « Pipeline parent : exemple commenté »stages: - generate # 1️⃣ Étape : générer le YAML - trigger # 2️⃣ Étape : exécuter le YAML généré
# 🛠️ Job qui GÉNÈRE le pipelinegenerate_pipeline: stage: generate image: alpine:3.18 # Image légère pour le scripting script: - | # Script multi-lignes cat > child-pipeline.yml << 'EOF' # Écrit dans un fichier stages: - test
test_job: stage: test script: - echo "Generated job!" EOF artifacts: paths: - child-pipeline.yml # 👈 CRUCIAL : sauvegarder le fichier généré
# 🚀 Job qui EXÉCUTE le pipeline générétrigger_child: stage: trigger trigger: # Mot-clé spécial : ce n'est pas un job normal include: - artifact: child-pipeline.yml # 👈 Le fichier généré job: generate_pipeline # 👈 Le job qui l'a créé strategy: depend # Attend la fin du pipeline enfantDécortiquons la magie :
cat > child-pipeline.yml << 'EOF'— Crée un fichier avec le contenu entreEOFetEOFartifacts: paths:— Sauvegarde le fichier pour qu’il survive au jobinclude: artifact:— Différent deinclude: local:! Ici on référence un artefact, pas un fichier du repojob: generate_pipeline— Indique quel job a produit l’artefact
Cas d’usage : tests multi-clients
Section intitulée « Cas d’usage : tests multi-clients »Le problème
Section intitulée « Le problème »Vous avez une application SaaS avec plusieurs clients, chacun ayant sa propre configuration. Vous devez tester l’application pour chaque client avant de déployer.
Pourquoi pas parallel:matrix ?
- La liste des clients peut changer (nouveaux clients, clients supprimés)
- La liste vient d’un fichier de config ou d’une API, pas du YAML
- Vous avez besoin de logique conditionnelle (certains clients ont des tests spéciaux)
Solution : génération dynamique
Section intitulée « Solution : génération dynamique »Idée : Un script lit la liste des clients et génère un job par client.
stages: - generate - test
generate: stage: generate image: alpine:3.18 script: - | # 📋 Liste des clients (pourrait venir d'un fichier, d'une API...) CLIENTS="client1 client2 client3"
# 🛠️ Création du fichier YAML echo "stages:" > test-pipeline.yml echo " - test" >> test-pipeline.yml echo "" >> test-pipeline.yml
# 🔁 Boucle : un job par client for client in $CLIENTS; do cat >> test-pipeline.yml << EOF test_${client}: stage: test script: - echo "Testing for ${client}" - ./run-tests.sh --client ${client} variables: CLIENT_NAME: "${client}"
EOF done
# 👀 Affiche le résultat (utile pour debug) echo "=== Pipeline généré ===" cat test-pipeline.yml artifacts: paths: - test-pipeline.yml
run_tests: stage: test trigger: include: - artifact: test-pipeline.yml job: generate strategy: dependCe que le script génère
Section intitulée « Ce que le script génère »Le fichier test-pipeline.yml contient :
stages: - test
test_client1: stage: test script: - echo "Testing for client1" - ./run-tests.sh --client client1 variables: CLIENT_NAME: "client1"
test_client2: stage: test script: - echo "Testing for client2" - ./run-tests.sh --client client2 variables: CLIENT_NAME: "client2"
test_client3: stage: test script: - echo "Testing for client3" - ./run-tests.sh --client client3 variables: CLIENT_NAME: "client3"Résultat dans GitLab : 3 jobs s’exécutent en parallèle, chacun testant un client différent. Si vous ajoutez client4 à la liste, un 4ème job apparaît automatiquement.
Générer selon les fichiers modifiés
Section intitulée « Générer selon les fichiers modifiés »Le cas d’usage : monorepo
Section intitulée « Le cas d’usage : monorepo »Vous avez un monorepo avec plusieurs services :
monorepo/├── frontend/│ └── Dockerfile├── backend/│ └── Dockerfile├── api/│ └── Dockerfile└── docs/ └── (pas de Dockerfile)Objectif : Ne builder que les services modifiés. Si seul frontend/ change, ne pas rebuilder backend/ ni api/.
Pourquoi pas rules: changes: ?
changes: fonctionne, mais vous devez lister tous les services manuellement. Si vous ajoutez un service, vous devez modifier le YAML.
Avec la génération dynamique, le script découvre automatiquement les services modifiés.
Le piège de HEAD~1
Section intitulée « Le piège de HEAD~1 »Solution complète
Section intitulée « Solution complète »Détectez les services modifiés et générez leurs jobs :
stages: - detect - generate - build
# 🔍 Étape 1 : Détecter ce qui a changédetect_changes: stage: detect image: alpine:3.18 before_script: - apk add --no-cache git # Git n'est pas installé dans alpine script: - | # Cas spécial : premier push (BEFORE_SHA = 0000...) if [ "$CI_COMMIT_BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then echo "🆕 Premier push : considérer tous les dossiers" CHANGED=$(git ls-tree -r --name-only HEAD | cut -d'/' -f1 | sort -u | tr '\n' ' ') else echo "🔄 Comparaison $CI_COMMIT_BEFORE_SHA → $CI_COMMIT_SHA" CHANGED=$(git diff --name-only "$CI_COMMIT_BEFORE_SHA" "$CI_COMMIT_SHA" | cut -d'/' -f1 | sort -u | tr '\n' ' ') fi
echo "📁 Dossiers modifiés : $CHANGED"
# Stocker le résultat pour les jobs suivants if [ -z "$CHANGED" ]; then echo "PIPELINE_EMPTY=true" >> detect.env else echo "PIPELINE_EMPTY=false" >> detect.env echo "SERVICES=$CHANGED" >> detect.env fi artifacts: reports: dotenv: detect.env # 👈 Variables disponibles pour les jobs suivants
# 🛠️ Étape 2 : Générer le pipelinegenerate_pipeline: stage: generate image: alpine:3.18 needs: ["detect_changes"] rules: - if: '$PIPELINE_EMPTY == "false"' # Ne génère que si quelque chose a changé script: - | echo "stages:" > build-pipeline.yml echo " - build" >> build-pipeline.yml
for service in $SERVICES; do # Vérifier que c'est un service avec Dockerfile if [ -f "${service}/Dockerfile" ]; then echo "✅ Génération du job pour ${service}" cat >> build-pipeline.yml << EOF
build_${service}: stage: build image: docker:24 services: - docker:24-dind script: - cd ${service} - docker build -t ${service}:\$CI_COMMIT_SHA .EOF else echo "⏭️ Ignoré ${service} (pas de Dockerfile)" fi done
echo "=== Pipeline généré ===" cat build-pipeline.yml artifacts: paths: - build-pipeline.yml
# 🚀 Étape 3 : Exécuter le pipeline générétrigger_builds: stage: build needs: ["generate_pipeline"] trigger: include: - artifact: build-pipeline.yml job: generate_pipeline strategy: depend rules: - if: '$PIPELINE_EMPTY == "false"'Comment ça marche :
detect_changestrouve les dossiers modifiés (frontend backend)generate_pipelinecrée un job Docker build pour chaque dossier qui a un Dockerfiletrigger_buildsexécute le pipeline généré
Générer depuis une configuration
Section intitulée « Générer depuis une configuration »Le cas d’usage
Section intitulée « Le cas d’usage »Votre équipe veut définir les environnements de déploiement sans toucher au CI/CD. Ils éditent un fichier YAML simple, et le pipeline s’adapte automatiquement.
Fichier de configuration
Section intitulée « Fichier de configuration »Un fichier YAML lisible par les non-experts CI/CD :
environments: - name: staging url: https://staging.example.com auto_deploy: true # Déploiement automatique - name: production url: https://example.com auto_deploy: false # Manuel seulement approval_required: true # Validation obligatoireGénérateur Python
Section intitulée « Générateur Python »Un script qui lit cette config et génère le pipeline :
#!/usr/bin/env python3import yaml
# 📖 Lire la configurationwith open('config/environments.yml') as f: config = yaml.safe_load(f)
# 🛠️ Construire le pipelinepipeline = { 'stages': ['deploy'],}
for env in config['environments']: job_name = f"deploy_{env['name']}"
# Job de base job = { 'stage': 'deploy', 'script': [ f"./deploy.sh {env['name']}" ], 'environment': { 'name': env['name'], 'url': env['url'] } }
# 🔒 Si pas auto_deploy : job manuel if not env.get('auto_deploy', True): job['when'] = 'manual'
# ✅ Si approval_required : échec interdit if env.get('approval_required'): job['allow_failure'] = False
pipeline[job_name] = job
# 💾 Écrire le pipeline généréwith open('deploy-pipeline.yml', 'w') as f: yaml.dump(pipeline, f, default_flow_style=False)
print("✅ Pipeline généré : deploy-pipeline.yml")Pipeline parent
Section intitulée « Pipeline parent »stages: - generate - deploy
generate_deploy: stage: generate image: python:3.11 script: - pip install pyyaml - python scripts/generate-deploy-pipeline.py - cat deploy-pipeline.yml # Debug : voir le résultat artifacts: paths: - deploy-pipeline.yml
deploy: stage: deploy trigger: include: - artifact: deploy-pipeline.yml job: generate_deploy strategy: dependRésultat : L’équipe peut ajouter un environnement en éditant config/environments.yml. Le pipeline s’adapte sans modifier .gitlab-ci.yml.
Génération avec Ansible
Section intitulée « Génération avec Ansible »Pour des cas complexes, utilisez des templates Jinja2 avec Ansible :
Template Jinja2
Section intitulée « Template Jinja2 »stages: - test
{% for host in groups['all'] %}test_{{ host }}: stage: test script: - pytest --host {{ host }} variables: TARGET_HOST: "{{ host }}" HOST_NAME: "{{ hostvars[host]['name'] }}"
{% endfor %}Playbook de génération
Section intitulée « Playbook de génération »---- hosts: localhost gather_facts: false tasks: - name: Generate pipeline from template template: src: templates/test-pipeline.yml.j2 dest: ./generated-pipeline.ymlPipeline GitLab
Section intitulée « Pipeline GitLab »generate: stage: generate image: ansible/ansible-runner script: - ansible-playbook -i inventory generate-pipeline.yml artifacts: paths: - generated-pipeline.yml
run_tests: stage: test trigger: include: - artifact: generated-pipeline.yml job: generate strategy: dependBonnes pratiques
Section intitulée « Bonnes pratiques »1. Toujours valider le YAML généré
Section intitulée « 1. Toujours valider le YAML généré »Une erreur de syntaxe dans le YAML généré = pipeline qui échoue. Validez avant de sauvegarder :
generate: script: - ./generate.sh > pipeline.yml # ✅ Valide la syntaxe YAML - python -c "import yaml; yaml.safe_load(open('pipeline.yml'))" artifacts: paths: - pipeline.yml2. Toujours afficher le pipeline généré
Section intitulée « 2. Toujours afficher le pipeline généré »Quand quelque chose ne marche pas, la première question est “qu’est-ce qui a été généré ?” :
generate: script: - ./generate.sh > pipeline.yml - echo "=== Pipeline généré ===" && cat pipeline.yml3. Gérer le cas “aucun job à exécuter”
Section intitulée « 3. Gérer le cas “aucun job à exécuter” »Si votre script ne génère aucun job (rien n’a changé), GitLab refuse le pipeline vide. Deux solutions :
Option A : Générer un job “noop” (recommandé)
generate: script: - | if [ -z "$JOBS_TO_CREATE" ]; then # Pas de jobs → pipeline minimal echo 'noop: { stage: test, script: ["echo Rien \u00e0 faire"] }' > pipeline.yml else ./generate.sh > pipeline.yml fiOption B : Conditionner le trigger avec dotenv
generate: script: - | if ./generate.sh > pipeline.yml 2>/dev/null; then echo "HAS_JOBS=true" >> build.env else echo "HAS_JOBS=false" >> build.env fi artifacts: reports: dotenv: build.env paths: - pipeline.yml
trigger: trigger: include: - artifact: pipeline.yml job: generate rules: - if: '$HAS_JOBS == "true"' # Ne trigger que s'il y a des jobs- Utilisez des templates réutilisables dans le pipeline généré.
Erreurs fréquentes
Section intitulée « Erreurs fréquentes »1. “invalid YAML”
Section intitulée « 1. “invalid YAML” »Symptôme : Le trigger échoue avec une erreur de syntaxe.
Cause : Le script a généré du YAML invalide (indentation, caractères spéciaux…).
Solution :
- Ajoutez
cat pipeline.ymlpour voir le contenu - Validez avec un parser :
python -c "import yaml; yaml.safe_load(open('pipeline.yml'))" - Attention aux variables avec
:ou#(les entourer de guillemets)
2. “artifact not found”
Section intitulée « 2. “artifact not found” »Symptôme : Le trigger ne trouve pas le fichier.
Cause : Le job generate a échoué, ou le fichier n’est pas dans artifacts: paths:.
Solution :
- Vérifiez que
generateest passé en vert - Vérifiez que le nom de fichier correspond exactement
- Vérifiez
artifacts: paths:dans le job générateur
3. Pipeline vide
Section intitulée « 3. Pipeline vide »Symptôme : Le pipeline enfant refuse de se créer (“pipeline has no jobs”).
Cause : Le YAML généré ne contient aucun job.
Solution : Générez un job “noop” par défaut (voir Bonnes pratiques).
4. rules: exists ne fonctionne pas
Section intitulée « 4. rules: exists ne fonctionne pas »Symptôme : exists: ignore le fichier généré.
Cause : exists: vérifie les fichiers dans le repo, pas les artefacts.
Solution : Utilisez une variable dotenv (HAS_JOBS, PIPELINE_EMPTY).
5. Diff vide sur schedule/web
Section intitulée « 5. Diff vide sur schedule/web »Symptôme : git diff retourne rien sur un pipeline déclenché manuellement.
Cause : CI_COMMIT_BEFORE_SHA n’existe pas ou vaut 0000....
Solution : Prévoyez un fallback :
if [ "$CI_COMMIT_BEFORE_SHA" = "0000000000000000000000000000000000000000" ] || [ -z "$CI_COMMIT_BEFORE_SHA" ]; then # Pas de commit précédent : tout considérer comme modifié CHANGED=$(git ls-tree -r --name-only HEAD | cut -d'/' -f1 | sort -u)else CHANGED=$(git diff --name-only "$CI_COMMIT_BEFORE_SHA" "$CI_COMMIT_SHA" | cut -d'/' -f1 | sort -u)fiÀ retenir
Section intitulée « À retenir »- Génération : un job crée un fichier YAML
- Artefact : le fichier est sauvegardé
- Trigger :
include: artifact:exécute le pipeline strategy: depend: le parent attend la fin- Validez toujours le YAML généré
- Limites : 1 Mo max, 3 fichiers include par child pipeline
- Piège
exists: ne teste pas les artefacts, utilisez des variables dotenv - Piège
HEAD~1: préférezCI_COMMIT_BEFORE_SHA→CI_COMMIT_SHA