Aller au contenu
Développement medium

Task : le runner de tâches YAML pour DevOps

24 min de lecture

Fatigué des Makefiles illisibles et de leur syntaxe à tabulations ? Task offre une alternative moderne en YAML : lisible, portable, et conçue pour le DevOps.

Task (ou Taskfile) est un outil d’automatisation écrit en Go. Il exécute des tâches définies dans un fichier Taskfile.yml en respectant les dépendances, avec variables dynamiques, cache intelligent et support multi-fichiers. Un seul binaire, zéro dépendance.

Make existe depuis 1976 et reste un excellent outil. Mais sa syntaxe a vieilli et peut être déroutante pour les débutants (tabulations obligatoires, variables cryptiques). Task a été conçu pour résoudre ces irritants :

CritèreMakeTask
SyntaxePropre à Make, tabulations obligatoiresYAML standard, indentation libre
Variables$(VAR) et $@, $<, $^{{.VAR}} (templates Go)
InstallationPréinstallé Linux/macOSBinaire unique, multi-plateforme
Cache intelligentBasé sur dates de fichierssources/generates + checksum
Multi-fichiersinclude basiqueincludes avec namespaces
BouclesShell uniquementNatif (for, matrix)
Watch modeExterne (inotifywait)Intégré (--watch)
DocumentationCommentaires manuelsdesctask --list auto

La méthode recommandée — installe le binaire unique dans ~/.local/bin :

Fenêtre de terminal
sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b ~/.local/bin

Ajoutez ~/.local/bin à votre PATH si ce n’est pas déjà fait :

Fenêtre de terminal
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc && source ~/.bashrc
Fenêtre de terminal
task --version

Sortie attendue :

Task version: v3.48.0

Un Taskfile est un fichier YAML qui définit des tâches (tasks). Chaque tâche contient des commandes (cmds) à exécuter.

Créez un fichier Taskfile.yml avec task --init :

Fenêtre de terminal
task --init

Contenu généré :

version: '3'
vars:
GREETING: Hello, World!
tasks:
default:
cmds:
- echo "{{.GREETING}}"
silent: true

Exécution :

Fenêtre de terminal
task

Sortie :

Hello, World!

Sans argument, Task exécute la tâche default. L’option silent: true masque la commande (n’affiche que la sortie).

Une tâche Task se compose de plusieurs attributs optionnels qui contrôlent son comportement. Contrairement à Make où tout est implicite, Task rend chaque aspect explicite — vous voyez immédiatement ce que fait une tâche en lisant sa définition.

Voici une tâche typique avec tous les attributs courants :

version: '3'
tasks:
build:
desc: Compile l'application # Description (visible dans --list)
deps: [lint, test] # Dépendances (exécutées avant)
vars:
APP_NAME: myapp # Variables locales
cmds:
- echo "Building {{.APP_NAME}}" # Commandes à exécuter
silent: false # Afficher les commandes (défaut)

Chaque attribut a un rôle précis. Le tableau suivant résume les plus utilisés :

AttributRôleObligatoire
cmdsListe des commandes shell à exécuterOui
descDescription affichée par task --listNon (mais recommandé)
depsTâches à exécuter avant celle-ciNon
varsVariables locales à la tâcheNon
silentMasquer les commandes (true/false)Non
Fenêtre de terminal
task --list

Sortie :

task: Available tasks for this project:
* build: Compile l'application
* default: Tâche par défaut
* lint: Vérifie le code
* test: Lance les tests

Seules les tâches avec desc sont listées. Pour masquer une tâche de la liste, utilisez internal: true.

Les variables rendent vos Taskfiles réutilisables et configurables. Elles utilisent la syntaxe Go templates : {{.NOM_VARIABLE}}.

Définies au niveau global (section vars: racine) ou par tâche :

version: '3'
vars:
APP_NAME: myapp
VERSION: "1.0.0"
tasks:
build:
cmds:
- echo "Building {{.APP_NAME}} v{{.VERSION}}"

Évaluées au moment de l’exécution via le mot-clé sh:. La commande shell est exécutée et son résultat devient la valeur de la variable :

version: '3'
vars:
GIT_COMMIT:
sh: git rev-parse --short HEAD
TIMESTAMP:
sh: date +%Y%m%d-%H%M%S
tasks:
build:
cmds:
- echo "Commit {{.GIT_COMMIT}} at {{.TIMESTAMP}}"

Sortie :

task: [build] echo "Commit a1b2c3d at 20260210-142530"
Commit a1b2c3d at 20260210-142530

Chargez automatiquement les fichiers .env avec le mot-clé dotenv:. Task les lit dans l’ordre spécifié — la dernière valeur lue l’emporte :

version: '3'
dotenv: ['.env', '.env.local']
tasks:
show-config:
cmds:
- echo "API_KEY = {{.API_KEY}}"
- echo "DATABASE_URL = {{.DATABASE_URL}}"

Fichier .env :

Fenêtre de terminal
API_KEY=secret123
DATABASE_URL=postgres://localhost:5432/mydb
Fenêtre de terminal
task build VERSION=2.0.0

La variable VERSION passe de 1.0.0 à 2.0.0 pour cette exécution uniquement. Les variables CLI ont priorité sur toutes les autres sources.

Passez des variables d’environnement à vos commandes avec le mot-clé env: :

version: '3'
tasks:
deploy:
env:
DEPLOY_ENV: production
LOG_LEVEL: info
cmds:
- ./deploy.sh

Les dépendances (deps) garantissent un ordre d’exécution. Task les exécute en parallèle dès que possible pour gagner du temps.

version: '3'
tasks:
lint:
cmds:
- echo "Linting..."
test:
cmds:
- echo "Testing..."
build:
desc: Compile l'application
deps: [lint, test]
cmds:
- echo "Building..."

Exécution :

Fenêtre de terminal
task build

Sortie :

task: [lint] echo "Linting..."
Linting...
task: [test] echo "Testing..."
Testing...
task: [build] echo "Building..."
Building...

lint et test s’exécutent en parallèle, puis build s’exécute.

Pour un contrôle séquentiel (une tâche après l’autre), appelez les tâches dans cmds avec la syntaxe task: :

version: '3'
tasks:
deploy:
cmds:
- task: build
- task: push
- task: notify

Ici, buildpushnotify s’exécutent dans l’ordre, un à la fois.

Passez des variables aux tâches appelées via le bloc vars: :

version: '3'
tasks:
greet:
cmds:
- echo "Hello, {{.NAME}}!"
greet-all:
cmds:
- task: greet
vars:
NAME: Alice
- task: greet
vars:
NAME: Bob

Si vous connaissez déjà Make, vous retrouverez des concepts similaires dans Task — mais avec une syntaxe différente. La bonne nouvelle : Task est plus explicite, donc plus facile à comprendre une fois la correspondance établie.

Le piège principal : oublier que Task n’a pas de variables automatiques ($@, $<, $^). En Task, tout est explicite — ce qui rend le code plus lisible au prix d’un peu plus de verbosité.

Voici les équivalences les plus courantes :

MakeTaskNotes
$(VAR) / ${VAR}{{.VAR}}Syntaxe Go template
$@ (cible)Pas d’équivalent directUtilisez une variable explicite
$< (première dépendance)Pas d’équivalent directUtilisez une variable
$^ (toutes les dépendances)Pas d’équivalent directListez explicitement
VAR := $(shell cmd)VAR: { sh: cmd }Variable dynamique
target: dep1 dep2deps: [dep1, dep2]Exécution parallèle
.PHONY: targetAutomatiqueToutes les tâches sont “phony”
include file.mkincludes: { ns: file.yml }Avec namespace
@echo "msg"silent: trueAu niveau de la tâche
make -ntask --dryMode dry-run
make -j4Parallèle par défautVia dépendances

Exemple de migration :

# Makefile
VERSION := $(shell git describe --tags)
build:
go build -ldflags="-X main.Version=$(VERSION)" -o bin/app
Taskfile.yml
version: '3'
vars:
VERSION:
sh: git describe --tags 2>/dev/null || echo "dev"
tasks:
build:
cmds:
- go build -ldflags="-X main.Version={{.VERSION}}" -o bin/app

La théorie c’est bien, la pratique c’est mieux. Cette section présente des Taskfiles complets et testés que vous pouvez copier-coller dans vos projets. Chaque exemple suit les bonnes pratiques : variables dynamiques, dépendances explicites, et descriptions pour task --list.

Un workflow Docker typique comprend trois étapes : construire l’image, la pousser vers un registry, et la lancer localement pour tester. Ce Taskfile automatise ces trois étapes avec un tag basé sur le dernier commit Git.

version: '3'
vars:
REGISTRY: ghcr.io/myorg
IMAGE_NAME: myapp
VERSION:
sh: git describe --tags 2>/dev/null || echo "dev"
IMAGE: "{{.REGISTRY}}/{{.IMAGE_NAME}}:{{.VERSION}}"
tasks:
docker:build:
desc: Build l'image Docker
cmds:
- docker build -t {{.IMAGE}} .
docker:push:
desc: Push l'image vers le registry
deps: [docker:build]
cmds:
- docker push {{.IMAGE}}
docker:run:
desc: Lance le conteneur localement
cmds:
- docker run --rm -p 8080:8080 {{.IMAGE}}

Pour un projet Python, le workflow classique enchaîne installation des dépendances, linting, tests, puis packaging. Ce Taskfile utilise Poetry (gestionnaire de dépendances moderne) et ruff (linter ultra-rapide). Les dépendances entre tâches garantissent que test lance d’abord lint, et que build lance d’abord test.

version: '3'
tasks:
install:
desc: Installe les dépendances
cmds:
- poetry install
lint:
desc: Vérifie le code avec ruff
cmds:
- poetry run ruff check .
format:
desc: Formate le code avec ruff
cmds:
- poetry run ruff format .
test:
desc: Lance les tests avec pytest
deps: [lint]
cmds:
- poetry run pytest -v
build:
desc: Construit le package
deps: [test]
cmds:
- poetry build

L’avantage majeur de Task en CI/CD : les mêmes commandes fonctionnent partout. Fini les scripts de CI différents de ce que vous lancez en local. Si task test passe sur votre machine, il passera dans GitLab CI.

Voici comment intégrer Task dans un pipeline GitLab. Installez Task dans votre image Docker de base, puis appelez simplement les tâches :

.gitlab-ci.yml
stages:
- test
- build
test:
stage: test
script:
- task test
build:
stage: build
script:
- task docker:build
- task docker:push

Les bases couvertes, explorons les fonctionnalités qui font de Task un outil puissant pour les projets complexes. Ces fonctionnalités ne sont pas indispensables pour démarrer, mais elles deviennent précieuses à mesure que votre projet grandit.

Quand un Taskfile dépasse 200 lignes, il devient difficile à maintenir. La solution : découper en plusieurs fichiers, un par domaine (Docker, Kubernetes, tests…). Chaque fichier inclus reçoit un namespace — un préfixe qui évite les conflits de noms.

Exemple concret : un projet avec des tâches Docker et Kubernetes. Plutôt qu’un fichier monolithique, on sépare en trois fichiers :

Taskfile.yml
version: '3'
includes:
docker:
taskfile: ./taskfiles/Docker.yml
dir: .
k8s:
taskfile: ./taskfiles/Kubernetes.yml
vars:
NAMESPACE: production
taskfiles/Docker.yml
version: '3'
tasks:
build:
desc: Build Docker image
cmds:
- docker build -t myapp .

Les tâches incluses sont appelées avec leur namespace en préfixe :

Fenêtre de terminal
task docker:build
task k8s:deploy

Parfois vous devez exécuter la même commande sur plusieurs éléments : plusieurs fichiers à lint, plusieurs personnes à notifier, plusieurs services à redémarrer. Plutôt que de dupliquer les tâches, utilisez les boucles for.

Task supporte trois types de boucles :

  • Sur une liste : for: { var: MA_LISTE } — itère sur les mots d’une variable
  • Sur une matrice : for: { matrix: ... } — génère toutes les combinaisons
  • Sur des fichiers : variable dynamique avec find ou ls

Voici des exemples concrets :

version: '3'
tasks:
greet:
desc: Salue plusieurs personnes
vars:
NAMES: Alice Bob Charlie
cmds:
- for: { var: NAMES }
cmd: echo "Bonjour, {{.ITEM}}!"
lint-configs:
desc: Lint tous les fichiers YAML
cmds:
- for: { var: YAML_FILES }
cmd: yamllint {{.ITEM}}
vars:
YAML_FILES:
sh: find . -name "*.yml" -type f

Sortie de task greet :

task: [greet] echo "Bonjour, Alice!"
Bonjour, Alice!
task: [greet] echo "Bonjour, Bob!"
Bonjour, Bob!
task: [greet] echo "Bonjour, Charlie!"
Bonjour, Charlie!

La matrice est une boucle sur toutes les combinaisons de plusieurs listes. C’est idéal pour les builds cross-platform : au lieu d’écrire 6 tâches pour linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64, windows/arm64… vous définissez une matrice OS × ARCH et Task génère toutes les combinaisons.

Attention : le nombre de combinaisons explose vite. 3 OS × 2 architectures = 6 builds. Ajoutez 3 versions de Go = 18 builds. Utilisez avec parcimonie.

version: '3'
tasks:
build-all:
desc: Build pour toutes les plateformes
cmds:
- for:
matrix:
OS: [linux, darwin, windows]
ARCH: [amd64, arm64]
cmd: |
echo "Building for {{.ITEM.OS}}-{{.ITEM.ARCH}}"
GOOS={{.ITEM.OS}} GOARCH={{.ITEM.ARCH}} go build -o bin/app-{{.ITEM.OS}}-{{.ITEM.ARCH}}

Recompiler un projet qui n’a pas changé est une perte de temps. Task résout ce problème avec sources et generates : vous déclarez quels fichiers sont lus (sources) et quels fichiers sont produits (generates). Si aucune source n’a changé depuis la dernière exécution, Task affiche “Task is up to date” et ne fait rien.

Comment ça marche ? Task calcule un checksum des fichiers sources. Si le checksum n’a pas changé depuis la dernière exécution réussie, la tâche est considérée “à jour”. C’est plus fiable que la comparaison de dates de Make.

version: '3'
tasks:
build:
desc: Compile uniquement si les sources ont changé
sources:
- src/**/*.go
- go.mod
- go.sum
generates:
- bin/myapp
cmds:
- go build -o bin/myapp ./...

Première exécution :

Fenêtre de terminal
task build
# → Compile le projet

Deuxième exécution (sans modification) :

Fenêtre de terminal
task build
# → task: Task "build" is up to date

Modification d’un fichier :

Fenêtre de terminal
touch src/main.go
task build
# → Recompile

Le mode watch surveille les fichiers sources et relance automatiquement la tâche quand quelque chose change. C’est l’équivalent de nodemon pour Node.js ou cargo watch pour Rust — mais intégré à Task.

Prérequis : la tâche doit avoir un attribut sources défini (sinon Task ne sait pas quoi surveiller).

Fenêtre de terminal
task --watch build

Task reste en foreground et surveille les fichiers. Dès qu’un fichier source change (sauvegarde dans votre éditeur), Task relance la tâche. Appuyez sur Ctrl+C pour arrêter.

Cas d’usage typique : pendant le développement, lancez task --watch test dans un terminal. Chaque modification de code relance les tests automatiquement. Vous avez un feedback instantané sans quitter votre éditeur.

Certaines tâches ne doivent s’exécuter que si des conditions sont remplies : fichier présent, variable définie, état Git propre… Plutôt que d’ajouter des if dans vos scripts, utilisez preconditions. Si une précondition échoue, Task affiche un message clair et s’arrête avant d’exécuter les commandes.

Analogie : c’est comme le contrôle de sécurité avant le décollage d’un avion. On vérifie tout avant de démarrer les moteurs, pas pendant le vol.

version: '3'
tasks:
deploy:
desc: Déploie en production
preconditions:
- sh: test -f bin/myapp
msg: "Binaire non trouvé. Lancez 'task build' d'abord."
- sh: '[ "$(git status --porcelain)" = "" ]'
msg: "Working directory non propre. Committez vos changements."
cmds:
- ./deploy.sh

Si une précondition échoue :

task: Binaire non trouvé. Lancez 'task build' d'abord.
task: Failed to run task "deploy": task: precondition not met

Parfois une tâche a besoin de certaines variables pour fonctionner — un nom de registry, un tag d’image, une clé API. Plutôt que d’échouer avec une erreur cryptique au milieu de l’exécution, déclarez ces variables comme obligatoires avec requires. Task vérifie leur présence avant de lancer la moindre commande.

version: '3'
tasks:
docker:push:
desc: Push vers le registry
requires:
vars: [REGISTRY, IMAGE_TAG]
cmds:
- docker push {{.REGISTRY}}/myapp:{{.IMAGE_TAG}}

Appel sans les variables :

Fenêtre de terminal
task docker:push
# → task: Task "docker:push" cancelled because it is missing required variables: REGISTRY, IMAGE_TAG

Appel correct :

Fenêtre de terminal
REGISTRY=ghcr.io IMAGE_TAG=v1.0 task docker:push

Certaines tâches n’ont de sens que sur un OS spécifique : apt-get sur Linux, brew sur macOS, choco sur Windows. Avec platforms, vous déclarez où une tâche peut s’exécuter. Sur une autre plateforme, Task la skip silencieusement.

Cas d’usage : un Taskfile partagé entre développeurs Linux et macOS. Chacun peut lancer task install-deps — seules les commandes compatibles s’exécutent.

version: '3'
tasks:
install-deps:
platforms: [linux, darwin]
cmds:
- apt-get install -y build-essential || brew install gcc
windows-only:
platforms: [windows]
cmds:
- choco install make

Même avec un outil simple comme Task, des problèmes peuvent survenir. Cette section recense les erreurs les plus fréquentes et leurs solutions. Si votre problème n’est pas listé, lancez task --verbose pour obtenir plus de détails.

ProblèmeCause probableSolution
task: command not foundTask pas dans le PATHexport PATH="$HOME/.local/bin:$PATH"
task: No Taskfile foundMauvais répertoire ou nom de fichierVérifiez : Taskfile.yml, taskfile.yml ou Taskfile.yaml
yaml: line X: mapping values not allowedIndentation YAML incorrecteUtilisez 2 espaces, pas de tabulations
Variable non remplacée ({{.VAR}} affiché tel quel)Variable non définieVérifiez l’orthographe et la portée de la variable
Tâche exécutée alors qu’elle devrait être à joursources mal configuréVérifiez les patterns glob (ex: **/*.go)
Dépendances exécutées dans le mauvais ordredeps exécute en parallèleUtilisez cmds: [task: X] pour du séquentiel

Si vous ne devez retenir que l’essentiel de ce guide :

  1. Task = Make moderne en YAML. Plus lisible, plus portable, conçu pour le DevOps.

  2. task --init crée un Taskfile minimal. Partez de là et enrichissez.

  3. Les variables utilisent la syntaxe Go templates : {{.VAR}}. Dynamiques avec sh:.

  4. deps = parallèle, cmds: [task: X] = séquentiel. Choisissez selon votre besoin.

  5. sources/generates active le cache intelligent. Task skip les tâches si les sources n’ont pas changé.

  6. includes avec namespaces organise les gros projets : task docker:build, task k8s:deploy.

  7. --watch relance automatiquement une tâche quand les fichiers changent. Idéal pour le dev.

  8. Même commande en local et en CI. task test fonctionne partout — finies les surprises.

Ce site vous est utile ?

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

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.