Dagger.io - Utiliser dagger pour automatiser vos workflows CI
Automatisez vos workflows CI/CD avec Dagger.io et Python. Lancez rapidement vos outils de qualité de code (Pylint, Black, Ruff) et testez tout votre pipeline en local pour simplifier l'intégration.
Traduction automatique
Journal des modifications

Cet article montre comment « daggeriser » un projet Python avec Dagger.io afin que les étapes de CI (Pylint, Black, mypy, Ruff) soient définies une seule fois dans des fonctions Python et s'exécutent à l'identique en local (via un CLI dagger dockerisé, enveloppé dans des cibles make lint/make format) et dans une CI distante comme GitLab — fini la boucle lente « push et prie » pour déboguer un .gitlab-ci.yml uniquement sur le serveur. On y voit dagger init, l'écriture des fonctions du pipeline, leur exécution concurrente avec run-all, et la configuration du runner GitLab pour partager le socket Docker.
Attention, c'est une bombe. Docker a révolutionné le monde ; n'ayons pas peur de le dire haut et fort, et il y a fort à parier que Dagger.io, créé par les mêmes personnes que Docker, suivra le même chemin.
Dagger.io veut être un outil qui vous permet d'exécuter les étapes d'un workflow exactement de la même manière qu'un système de CI/CD (Continuous Integration / Continuous Delivery) comme ceux de GitHub, GitLab, Jenkins, ...
Mais qu'est-ce qu'une CI ? C'est une étape réalisée par votre serveur une fois que vous avez poussé une nouvelle version de votre projet. Pendant une CI, vous pouvez valider la syntaxe de votre code, vérifier qu'il respecte certaines règles (de formatage par exemple), lancer des outils d'analyse de qualité de code comme la vérification que vous n'avez pas de variables non déclarées ou non typées, ni de code mort (par exemple une fonction que vous n'utilisez plus).
Pendant une CI, vous pouvez aussi lancer vos tests unitaires et les exécuter à chaque nouvelle version poussée.
Le but de la CI, c'est de ... planter dès que quelque chose n'est pas dans l'état attendu ; par exemple vous avez oublié un ;, vous avez une violation (des espaces à la place de tabulations), un test unitaire ne passe plus, etc. L'objectif de la CI est donc de garantir que votre code est excellent.
Le résultat : un pipeline qui tourne, sans YAML, sans push
Une fois Dagger branché sur un petit projet Python, dagger init génère un pipeline fonctionnel en deux minutes environ — aucun serveur CI impliqué, aucun .gitlab-ci.yml à pousser puis attendre :
Et le pipeline est immédiatement appelable, directement sur votre machine :
Deux fonctions d'exemple (container-echo, grep-dir) existent dès le départ — la preuve qu'un pipeline Dagger n'est rien d'autre que des fonctions Python que vous pouvez lancer en local. La suite de cet article transforme ces exemples en véritables étapes Pylint/Black/mypy/Ruff, puis branche exactement le même pipeline dans une CI GitLab.
Pourquoi ça fonctionne
- Les étapes de CI sont de simples fonctions Python — aucun DSL YAML à apprendre, aucune syntaxe qui n'existe que sur le serveur.
- Exactement la même fonction tourne sur votre portable et dans GitLab/GitHub : même container, même sortie, plus de surprise après un push.
- Dagger exécute tout dans un container qu'il contrôle, donc les outils dont votre pipeline a besoin (Pylint, Black, mypy, Ruff, ...) sont figés et reproductibles, pas « ce qui traîne sur le runner ».
- Les fonctions se composent : appelez-en une pour linter, ou appelez une fonction
run-allpour lancer lint/format/type-check/ruff en parallèle et s'arrêter au premier échec. - La vieille boucle push → attendre → lire le log → deviner → repousser peut être entièrement jouée hors ligne, avant que quoi que ce soit n'atteigne le serveur.
Installation
Dans ce tutoriel, nous allons daggeriser un repository, c'est-à-dire partir de zéro, créer un petit script Python, créer une image Docker Dagger, puis initialiser notre projet pour utiliser Dagger.
Créons un dossier temporaire pour Dagger et plaçons-nous dedans : mkdir /tmp/dagger && cd $_.
Créez-y un script Python dans un sous-dossier src et appelons-le src/main.py :
if random.randint(0, 1) == 0: print("Hello world")else: print("Bonjour le monde!")Ce code génère un nombre aléatoire entre 0 et 1, puis affiche soit « Hello world » soit « Bonjour le monde! » selon le résultat obtenu. C'est une simple démonstration de branchement conditionnel basé sur une valeur aléatoire.
Formidable application qui nous dit, au hasard, Hello world ou Bonjour le monde!
Nos objectifs
Chaque fois que nous pousserons notre code vers notre outil de versioning (comme GitLab), nous voulons :
- Lancer Pylint, Lint python scripts using Pylint - Run analyses your code without actually running it,
- Lancer Black, Format the script using Black,
- Lancer Mypy, Mypy is a program that will type check your Python code et
- Lancer Ruff, an extremely fast Python linter and code formatter
(J'ai détaillé ces quatre outils dans Python - Les outils de qualité de code.)
Ces étapes sont déclenchées dans notre CI (GitLab, GitHub, ...) à chaque push et, pour faire les mêmes actions en local, nous devons créer par exemple quelques actions make (make lint, make format, ...).
Nous voulons Dagger

Sans surprise, à un moment il faut installer Dagger. Installer ? Aïe non ; on ne va pas l'installer car, en amoureux de Docker, nous allons utiliser Docker et créer notre image Dagger.
Créez un nouveau sous-dossier appelé .docker et, dans ce dossier, un fichier appelé Dockerfile :
# cspell:disable
FROM python:3.11-slim-buster AS base
# Dagger is using the Docker technology so we need to install Docker in the image# Also install required Linux dependencies# hadolint ignore=DL3008RUN set -e -x \ && apt-get update -yqq \ && apt-get install -y --no-install-recommends docker.io wget \ && apt-get clean \ && rm -rf /tmp/* /var/lib/apt/lists/*
# Install DaggerARG VERSION=0.15.1ARG ARCHIVE=dagger_v${VERSION}_linux_amd64.tar.gzARG URL=https://github.com/dagger/dagger/releases/download/v${VERSION}/${ARCHIVE}
# hadolint ignore=DL3003RUN set -e -x \ && cd /tmp \ && wget --quiet --output-document ${ARCHIVE} ${URL} \ && tar xvfz ${ARCHIVE} \ && mv dagger /usr/local/bin/ \ && rm -rf /tmp/
WORKDIR /app/src
ENTRYPOINT [ "/usr/local/bin/dagger" ]Ce Dockerfile crée une image Docker basée sur Python 3.11 allégée, ajoute Docker et wget via le gestionnaire de paquets, puis télécharge et installe Dagger (un outil d'orchestration) dans le répertoire /usr/local/bin. L'image est configurée pour exécuter Dagger par défaut.
Il faut construire notre image, lançons donc docker build -t dagger_daemon -f .docker/Dockerfile .
Daggeriser notre application
Comme dit plus haut, nous devons créer quelques éléments pour daggeriser notre application.
Pour cela, il faut exécuter la commande dagger init et, comme nous utilisons une image Docker où dagger est le point d'entrée, la commande à lancer est : docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock -v .:/app/src dagger_daemon init --sdk=python --source=./.pipeline
Décortiquons cette longue ligne de commande :
- avec
-itnous pourrons interagir (si besoin) avec le container et nous allouons un terminal TTY, c'est-à-dire que nous récupérons la sortie de la commande exactement comme si nous l'avions lancée sur notre machine, - vous devez partager votre
/var/run/docker.socklocal avec le container car Dagger utilise Docker-out-of-Docker (aliasDooD) et, pour cette raison, le container doit pouvoir interagir avec votre instance de Docker (-v /var/run/docker.sock:/var/run/docker.sock) et - vous devez monter votre dossier courant local dans le container (
-v .:/app/src). dagger_daemonest le nom de notre imageinit --sdk=python --source=./.pipelineest la commande Dagger à lancer
Il faudra environ deux minutes pour télécharger et initialiser Dagger (la première fois). En regardant votre système de fichiers, vous verrez, oh, que le propriétaire est root et pas vous — c'est le listing de fichiers montré plus haut.
Lancez sudo chown -R christophe:christophe . (et remplacez mon prénom par votre nom d'utilisateur Linux).
Regardons l'arborescence :
Rappelez-vous, dagger a été défini comme notre entrypoint (voir notre Dockerfile) donc, pour obtenir l'écran d'aide de Dagger, lancez simplement docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock -v .:/app/src dagger_daemon call --help — vous obtiendrez la même liste de fonctions que celle montrée en haut de cet article (cette première fois prendra plus de temps puisque Dagger doit construire le pipeline).
Plus de démos
Créer notre fonction de linting
Les fonctions sont définies dans le fichier .pipeline/src/src/main.py.
Ouvrez ce fichier et ajoutez une nouvelle fonction comme ci-dessous :
@functionasync def lint(self, source: str) -> str: """ Run Pylint on the codebase. """ return f"\033[33mI'm inside your Dagger pipeline and I'll lint {source}\033[0m"Cette fonction asynchrone simule l'exécution de Pylint sur une base de code en retournant un message formaté. Elle accepte une chaîne source en paramètre et renvoie un texte coloré en jaune (via les codes ANSI) indiquant qu'elle s'exécute dans un pipeline Dagger.
Enregistrez le fichier et relancez docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock -v .:/app/src dagger_daemon call --help. Regardez, nous avons notre nouvelle fonction :
Et nous obtenons aussi la liste des paramètres de notre fonction lint avec docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock -v .:/app/src dagger_daemon call lint --help :
Maintenant, retour dans .pipeline/src/src/main.py et remplacez tout le fichier (nous n'avons pas besoin des fonctions d'exemple) par ce contenu :
from dagger import dag, DefaultPath, Directory, function, object_typefrom typing import Annotated
@object_typeclass Src: @function async def lint(self, source: Annotated[Directory, DefaultPath("src")]) -> str: """ Run Pylint on the codebase. """ print(f"\033[33mI'm inside your Dagger pipeline and I'll lint {source}\033[0m")
return await ( dag.container() .from_("python:3.13-slim") .with_exec(["pip","install","pylint"]) .with_mounted_directory("/app/src", source) .with_workdir("/app/src") .with_exec(["pylint","."]) .stdout() )Ce code définit une classe Dagger appelée Src qui expose une fonction lint pour analyser du code Python. La fonction crée un conteneur Docker, installe pylint, monte le répertoire source fourni en paramètre, puis exécute pylint sur ce répertoire et retourne le résultat.
Nous supprimons donc les deux fonctions d'exemple et nous implémentons notre fonction de linting. Nous définissons aussi le dossier src comme dossier par défaut afin de ne plus devoir ajouter --source src à chaque appel de dagger.
En lançant docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock -v .:/app/src dagger_daemon call lint, nous demandons au pipeline de lancer notre linter (Pylint ici) sur notre dossier courant.
Comme c'est la première fois, Dagger doit faire quelques initialisations (comme installer PyLint) puis nous obtenons la sortie :

Oui ! PyLint a fonctionné et nous a alertés d'une docstring de module manquante.
Éditez src/main.py et ajoutez une docstring de module valide (ou ignorez simplement cet avertissement) :
# pylint: disable=missing-module-docstring
import random
if random.randint(0, 1) == 0: print("Hello world")else: print("Bonjour le monde!")Ce script utilise le module random pour générer aléatoirement 0 ou 1, puis affiche un message en anglais ou en français selon le résultat. C'est un exemple simple de branchement conditionnel basé sur un tirage aléatoire.
En relançant docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock -v .:/app/src dagger_daemon call lint, nous serons félicités avec un score de 10/10.
Éditez à nouveau le fichier src/main.py, faites par exemple une faute de frappe en modifiant la ligne else: et en supprimant le : final : le linter ne sera plus content.
Information
Nous avons créé notre première tâche avec succès et nous l'avons lancée avec succès sur notre machine.
Créer un makefile
Ça devient difficile de retenir toutes ces commandes docker xxx, non ? Simplifions cela en créant un makefile (si c'est nouveau pour vous, lisez d'abord Linux Makefile - Quand utiliser un makefile).
Grâce au makefile suivant, nous pourrons simplement lancer make build pour créer notre image Docker Dagger et make lint pour lancer la fonction lint.
Vous aurez aussi une action make bash pour entrer dans un shell interactif (tapez exit pour quitter le shell et revenir à la console de votre host). Facile, non ?
Et make help affichera l'écran d'aide de Dagger.
DAEMON_NAME=dagger_daemonDOCKER_SOCK=-v /var/run/docker.sock:/var/run/docker.sock
.PHONY:buildbuild: docker build -t ${DAEMON_NAME} -f .docker/Dockerfile .
.PHONY:bashbash: docker run -it --rm ${DOCKER_SOCK} -v .:/app/src --entrypoint /bin/bash ${DAEMON_NAME}
.PHONY:helphelp: docker run -it --rm ${DOCKER_SOCK} -v .:/app/src ${DAEMON_NAME} call --help
.PHONY:lintlint: docker run -it --rm ${DOCKER_SOCK} -v .:/app/src ${DAEMON_NAME} call lintCe Makefile définit des cibles pour gérer un conteneur Docker appelé dagger_daemon. Il expose des commandes pour compiler l'image Docker, lancer un shell interactif à l'intérieur du conteneur, afficher l'aide et exécuter le linting — chaque commande monte le socket Docker et le répertoire courant pour permettre au conteneur d'accéder aux ressources de l'hôte.
Formater le code avec Black
Éditez le fichier .pipeline/src/src/main.py et ajoutez cette nouvelle fonction :
from dagger import dag, DefaultPath, Directory, function, object_typefrom typing import Annotated
@object_typeclass Src: @function async def lint(self, source: Annotated[Directory, DefaultPath("src")]) -> str: """ Run Pylint on the codebase. """ print(f"\033[33mI'm inside your Dagger pipeline and I'll lint {source}\033[0m")
return await ( dag.container() .from_("python:3.13-slim") .with_exec(["pip","install","pylint"]) .with_mounted_directory("/app/src", source) .with_workdir("/app/src") .with_exec(["pylint","."]) .stdout() )
# highlight-start @function async def format(self, source: Annotated[Directory, DefaultPath("src")]) -> str: """ Format the codebase using Black. """ return await ( dag.container() .from_("python:3.13-slim") .with_exec(["pip","install","black"]) .with_mounted_directory("/app/src", source) .with_workdir("/app/src") .with_exec(["black","."]) .stdout() ) # highlight-endCe code définit une classe Dagger qui encapsule des tâches de qualité de code Python. Il expose deux fonctions asynchrones : lint() qui vérifie le code avec Pylint, et format() qui formate le code avec Black. Chaque fonction crée un conteneur Docker, installe l'outil requis, monte le répertoire source, puis exécute la commande correspondante.
Donc, à partir de maintenant, vous pouvez lancer dagger call format (depuis l'intérieur du container, c'est-à-dire après un make bash) ou docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock -v .:/app/src dagger_daemon call format (depuis votre host).
Vers l'univers et l'infini
Ok, nous avons maintenant deux tâches et il nous en reste beaucoup à implémenter. On peut faire plein de copier/coller ou prendre le temps de réfléchir :
- Ce serait bien de ne pas devoir spécifier à chaque fois le dossier source,
- Ce serait bien de lancer toutes les tâches d'un coup, en asynchrone,
- Ce serait bien d'avoir un dossier de configuration où stocker les fichiers de configuration
Nous devons faire une petite modification à notre image Docker :
# cspell:disable
FROM python:3.11-slim-buster AS base
# Dagger is using the Docker technology so we need to install Docker in the image# Also install required Linux dependencies# hadolint ignore=DL3008RUN set -e -x \ && apt-get update -yqq \ && apt-get install -y --no-install-recommends docker.io wget \ && apt-get clean \ && rm -rf /tmp/* /var/lib/apt/lists/*
# Install DaggerARG VERSION=0.15.1ARG ARCHIVE=dagger_v${VERSION}_linux_amd64.tar.gzARG URL=https://github.com/dagger/dagger/releases/download/v${VERSION}/${ARCHIVE}
# hadolint ignore=DL3003RUN set -e -x \ && cd /tmp \ && wget --quiet --output-document ${ARCHIVE} ${URL} \ && tar xvfz ${ARCHIVE} \ && mv dagger /usr/local/bin/ \ && rm -rf /tmp/
# highlight-start# anyio is required by Python to be able to run tasks concurrently and asynchronouslyRUN set -e -x \ && pip install anyio# highlight-end
WORKDIR /app/src
ENTRYPOINT [ "/usr/local/bin/dagger" ]Ce Dockerfile crée une image Docker basée sur Python 3.11 et y ajoute Docker, Dagger (un outil d'orchestration), et la bibliothèque anyio pour la concurrence asynchrone. L'image est configurée pour exécuter Dagger comme point d'entrée dans le répertoire /app/src.
Cela fait, lancez make build pour créer une nouvelle image Docker avec anyio installé.
Maintenant, nous allons mettre à jour le fichier .pipeline/src/src/main.py et ajouter quelques nouvelles fonctions :
# cspell:ignore anyio,rcfile,pylintrc,workdirimport anyio
from typing import Annotated
import daggerfrom dagger import DefaultPath, Doc, dag, function, object_type, Directory
@object_typeclass Src:
# This is the directory where source files are located (f.i. "/app/src") source: Annotated[ Directory, Doc("The source folder; where the codebase is located"), DefaultPath("src") ]
# This is the directory where configuration files are located (f.i. "/app/.config") config: Annotated[ Directory, Doc("The folder container configuration files"), DefaultPath(".config") ]
@function async def lint(self) -> str: """ Lint python scripts using Pylint - Run analyses your code without actually running it (https://pypi.org/project/pylint/) """ return await ( dag.container() .from_("python:3.13-slim") .with_exec(["pip","install","pylint"]) .with_mounted_directory("/app/src", self.source) .with_mounted_directory("/app/config", self.config) .with_workdir("/app/src") .with_exec(["pylint",".","--rcfile","/app/config/.pylintrc"]) .stdout() )
@function async def format(self) -> str: """ Format the script using Black (https://black.readthedocs.io/en/stable/) """ return await ( dag.container() .from_("python:3.13-slim") .with_exec(["pip","install","black"]) .with_mounted_directory("/app/src", self.source) .with_mounted_directory("/app/config", self.config) .with_workdir("/app/src") .with_exec(["black",".","--config","/app/config/black.toml"]) .stdout() )
@function async def mypy(self)-> str: """ Mypy is a program that will type check your Python code (https://github.com/python/mypy/) """ mypy_cache = dag.cache_volume("python_mypy")
return await ( dag.container() .from_("python:3.13-slim") .with_exec(["pip","install","mypy"]) .with_mounted_directory("/app/src", self.source) .with_mounted_directory("/app/config", self.config) .with_mounted_cache("/tmp/mypy", mypy_cache) .with_workdir("/app/src") # .with_exec(["ls","-alhR"]) .with_exec(["mypy","--config-file",f"/app/config/mypy.ini","."]) .stdout() )
@function async def ruff(self) -> str: """ An extremely fast Python linter and code formatter (https://github.com/astral-sh/ruff) """ return await ( dag.container() .from_("python:3.13-slim") .with_exec(["pip","install","ruff"]) .with_mounted_directory("/app/src", self.source) .with_mounted_directory("/app/config", self.config) .with_workdir("/app/src") .with_exec(["ruff","check","--config","/app/config/pyproject.toml", "."]) .with_exec(["ruff","format","--config","/app/config/pyproject.toml", "."]) .stdout() )
@function async def run_all(self): """Run linter, type-checker, unit tests concurrently""" async with anyio.create_task_group() as tg: tg.start_soon(self.ruff) tg.start_soon(self.lint) tg.start_soon(self.format) tg.start_soon(self.mypy)Ce fichier définit une classe Src qui encapsule des opérations de linting et formatage Python dans des conteneurs isolés avec Dagger. Chaque méthode crée un conteneur Python, installe un outil spécifique (Pylint, Black, Mypy ou Ruff), monte les répertoires source et config, puis exécute l'outil et retourne son résultat. La méthode run_all exécute tous ces outils en parallèle pour un contrôle de qualité complet.
Ce nouveau fichier apporte beaucoup de changements :
Nous avons défini deux variables globales appelées source et config. Désormais, nous ne passons plus le dossier source à la fonction lint (paramètre local) mais il suffit de le définir une seule fois (paramètre global). Nous avons aussi ajouté un dossier config pour pouvoir indiquer à Dagger où sont stockés nos fichiers de configuration.
source: Annotated[ Directory, Doc("The source folder; where the codebase is located"), DefaultPath("src")]
config: Annotated[ Directory, Doc("The folder container configuration files"), DefaultPath(".config")]Ce snippet définit deux variables annotées qui décrivent des chemins de répertoires : un dossier source où le code se trouve et un dossier de configuration pour les fichiers de configuration. Chaque variable utilise des annotations pour spécifier son type, sa documentation et son chemin par défaut.
Nous avons deux nouvelles fonctions appelées mypy et ruff et une dernière appelée run_all :
@function async def run_all(self): """Run linter, type-checker, unit tests concurrently""" async with anyio.create_task_group() as tg: tg.start_soon(self.ruff) tg.start_soon(self.lint) tg.start_soon(self.format) tg.start_soon(self.mypy)Cette méthode lance quatre tâches asynchrones en parallèle — ruff, lint, format et mypy — en utilisant un gestionnaire de groupe de tâches pour attendre que toutes se terminent. C'est un moyen efficace d'exécuter plusieurs outils de qualité de code simultanément au lieu de les faire s'attendre les uns après les autres.
Celle-ci démarre les quatre fonctions en parallèle et attend qu'une échoue ou que les quatre réussissent.
Sous le capot (passez votre chemin si vous voulez juste l'utiliser)
Fichiers de configuration
Avant de lancer ces nouvelles fonctions, nous devons créer quelques fichiers de configuration.
[MASTER]disable= broad-exception-caught, consider-using-assignment-expr, invalid-name, missing-module-docstring, too-few-public-methods, too-many-instance-attributes, too-many-positional-arguments
; Pickle collected data for later comparisons.persistent=yes
; List of plugins (as comma-separated values of python modules names) to load,; usually to register additional checkers.load-plugins= pylint.extensions.check_elif, pylint.extensions.bad_builtin, pylint.extensions.docparams, pylint.extensions.for_any_all, pylint.extensions.set_membership, pylint.extensions.code_style, pylint.extensions.overlapping_exceptions, pylint.extensions.typing, pylint.extensions.redefined_variable_type, pylint.extensions.comparison_placement, ; pylint.extensions.mccabe,
; Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the; number of processors available to use.jobs=0
; When enabled, pylint would attempt to guess common misconfiguration and emit; user-friendly hints instead of false-positive error messages.suggestion-mode=yes
; Minimum supported python versionpy-version = 3.13.0
; Specify a score threshold to be exceeded before program exits with error.fail-under=9.50
[FORMAT]; The same as in prospector.yaml, .pylintrc, black.toml and pyproject.tomlmax-line-length=120Ceci est un fichier de configuration pour Pylint, l'outil d'analyse statique de code Python. Il désactive certaines vérifications trop strictes, active des extensions supplémentaires pour des contrôles avancés, configure l'analyse parallèle, et fixe des paramètres comme la version Python minimale et la longueur maximale des lignes.
[tool.black]
# The same as in prospector.yaml, .pylintrc, black.toml and pyproject.tomlline-length = 120
# We'll format the code for Python 3.13.0; we can add multiple versions (the ones supported by# the script, all versions separated by a comma like in ['py312', 'py313'] (run "black --help" to get them)target-version = ['py313']Cette section configure l'outil Black, un formateur de code Python. Elle définit la longueur maximale des lignes à 120 caractères et spécifie que le code doit être formaté pour Python 3.13, ce qui garantit la compatibilité avec cette version tout en appliquant les conventions de formatage appropriées.
[mypy]show_error_codes = true
; First we turn on *all the checks*, and then we turn off those that are too annoying.strict = True
; Don't know why but mypy returns errors like; Customer? has no attribute "input_path" [attr-defined]; when that attribute is well present. Ignore the error temporarilydisable_error_code = attr-defined
cache_dir = /tmp/mypy
; Disallows defining functions without type annotations or with incomplete type annotationsdisallow_untyped_defs = true
no_implicit_optional = true
strict_equality = truewarn_redundant_casts = truewarn_unused_ignores = true
python_version = 3.13Ce fichier configure mypy, l'outil de vérification de types pour Python. Il active le mode strict pour détecter les erreurs de typage, puis désactive une vérification spécifique qui génère des faux positifs sur les attributs. Il configure également mypy pour exiger des annotations de types complètes, interdire les optionnels implicites, et cible Python 3.13.
[tool.ruff]
exclude=[]
# The same as in prospector.yaml, .pylintrc, black.toml and pyproject.tomlline-length = 120indent-width = 4Ce fichier configure l'outil Ruff, un linter et formateur Python rapide. Il désactive les exclusions par défaut, définit une longueur de ligne à 120 caractères et une indentation de 4 espaces, alignant ainsi la configuration avec d'autres outils de contrôle de qualité du projet.
Implémenter une CI GitLab avec Dagger
Revenons à notre objectif : simplifier le processus de pipeline à la fois au niveau de la CI (GitLab, GitHub, etc.) et en local.
Nous venons de faire la partie locale, attaquons la CI distante.
Pour les chapitres suivants, je suppose que vous utilisez un serveur GitLab auto-hébergé.
Autoriser le runner GitLab à accéder à Docker
Je ne suis pas expert en configuration de runner GitLab mais la configuration suivante fonctionne pour moi. J'ai détaillé cette mise en place, et le piège du partage de dossiers qui va avec, dans GitLab - Exécuter Docker-out-of-Docker dans votre CI.
Connectez-vous en SSH à votre serveur de runner GitLab et éditez le fichier /etc/gitlab-runner/config.toml (vous devez être root). Ajoutez simplement /var/run/docker.sock:/var/run/docker.sock à la propriété volumes :
[runners.docker] # Adding /var/run/docker.sock:/var/run/docker.sock # will allow to use commands like "docker image list" i.e. running Docker commands # in CI scripts. This is called "Docker socket binding". volumes = ["/cache", "/var/run/docker.sock:/var/run/docker.sock"]Cette configuration TOML définit les volumes montés pour les runners Docker utilisés en intégration continue. Elle expose deux chemins : /cache pour stocker des données persistantes entre exécutions, et /var/run/docker.sock pour permettre aux scripts CI d'exécuter des commandes Docker directement à l'intérieur des conteneurs.
Assurez-vous aussi que l'utilisateur Linux utilisé par votre runner GitLab (le nom par défaut est gitlab-runner) fait partie du groupe docker. Cela se fait en lançant sudo usermod -aG docker gitlab-runner en CLI (voir https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#use-the-shell-executor).
Information
Documentation officielle de GitLab à propos des volumes.
Pour vérifier que ça marche, lancez sudo su gitlab-runner pour basculer sur cet utilisateur puis lancez docker info et docker image list et regardez si ça fonctionne. Si oui, votre utilisateur fait bien partie du groupe Docker.
Configurer votre CI
Créez un repository GitLab et poussez-y votre projet existant.
Maintenant, créez un fichier appelé .gitlab-ci.yml avec ce contenu :
.docker: image: docker:latest services: - docker:${DOCKER_VERSION}-dind variables: DOCKER_HOST: unix:///var/run/docker.sock # Docker Socket Binding.dagger: extends: [.docker] before_script: - apk add curl - curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR=/usr/local/bin shlint: extends: [.dagger] script: - dagger call --source src --config .config lintformat: extends: [.dagger] script: - dagger call --source src --config .config formatmypy: extends: [.dagger] script: - dagger call --source src --config .config mypyruff: extends: [.dagger] script: - dagger call --source src --config .config ruffCe fichier de configuration GitLab CI définit des modèles réutilisables pour exécuter des outils de qualité de code (lint, format, mypy, ruff) via Dagger dans des conteneurs Docker. Le modèle .docker configure l'accès à Docker en tant que service, le modèle .dagger l'étend et installe Dagger, puis quatre jobs héritent de .dagger pour lancer chacun un outil de vérification spécifique.
Et poussez les changements vers GitLab. La présence du fichier .gitlab-ci.yml indiquera à GitLab d'instancier un pipeline après chaque commit et, dans notre exemple, de lancer les quatre jobs.
Docker Socket Binding
L'exemple fourni utilise la technique appelée Docker Socket Binding : nous n'avons pas besoin de définir la variable DOCKER_HOST comme on peut le voir dans la documentation officielle de Dagger. En effet, si elle n'est pas spécifiée, DOCKER_HOST vaut unix:///var/run/docker.sock (doc).
Comme nous avons partagé le daemon Docker (/var/run/docker.sock) dans notre fichier de configuration GitLab /etc/gitlab-runner/config.toml, nous avons autorisé la CI à accéder au socket.
Mais vous pouvez aussi utiliser le mode asynchrone puisque nous avons implémenté une fonctionnalité run-all :
.docker: image: docker:latest services: - docker:${DOCKER_VERSION}-dind variables: DOCKER_HOST: unix:///var/run/docker.sock # Docker Socket Binding.dagger: extends: [.docker] before_script: - apk add curl - curl -fsSL https://dl.dagger.io/dagger/install.sh | BIN_DIR=/usr/local/bin shrun-all: extends: [.dagger] script: - dagger call --source src --config .config run-allCe fichier définit des configurations réutilisables pour un pipeline CI/CD utilisant Docker et Dagger. La section .docker configure l'accès à Docker, .dagger l'étend en ajoutant l'installation de Dagger, et run-all utilise cette configuration pour exécuter un job qui lance des commandes Dagger.
Conclusion
Nous avons construit une image Docker Dagger sur mesure, daggerisé un projet existant, et défini quatre fonctions (lint, format, mypy et ruff) plus une fonction run-all pour toutes les lancer en parallèle. Les actions make nous évitent de retenir les longues commandes du CLI Docker, et exactement les mêmes fonctions tournent maintenant aussi dans une CI GitLab, en partageant le socket Docker avec le runner.
La boucle push → attendre → lire le log → deviner → repousser de l'introduction a disparu : tout ce qui se déclenche en CI peut d'abord être joué et débogué en local, en quelques secondes, et non en minutes sur un serveur distant.
Si ce n'est pas déjà fait, lisez Python - Les outils de qualité de code pour plus de détails sur Pylint, Black, mypy et Ruff eux-mêmes.




