
J’ai réduit notre temps de validation CI de 95 %.
Pas grâce au cache. Pas grâce au sharding. Pas grâce à un outil tiers sophistiqué qui ajoute une dépendance supplémentaire à votre pipeline.
Avec 15 lignes de bash. Zéro dépendance. Zéro risque sur la chaîne d’approvisionnement.
La PR qui a livré ce changement était plus petite que la plupart des messages de commit. Et elle fait gagner à l’équipe 7 à 8 heures par mois.
Le problème
Chaque PR exécutait l’intégralité du pipeline CI. Linting backend, tests unitaires backend, tests unitaires frontend, tests E2E — le parcours complet. Un développeur modifie un seul fichier de test E2E, pousse sa branche, et attend 8 minutes et 18 secondes que le linting backend lui confirme que son code Python est correct.
Le code Python qu’il n’a pas touché.
C’est le comportement par défaut de la plupart des configurations CI. Un seul fichier de workflow, un seul déclencheur, tout exécuter à chaque push. C’est simple. C’est sûr. Mais c’est aussi du temps de développeur gaspillé sur des validations qui ne peuvent tout simplement pas échouer pour les modifications en cours.
La solution : 15 lignes de bash
L’idée est simple : utiliser git diff --name-only pour déterminer quels fichiers ont changé, puis ignorer conditionnellement les jobs qui ne sont pas pertinents pour ces modifications.
#!/bin/bash
# detect-changes.sh — Determine which CI jobs to run based on changed files
# Called by GitHub Actions workflow to set output variables
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
RUN_BACKEND=false
RUN_FRONTEND=false
RUN_E2E=false
for file in $CHANGED_FILES; do
case "$file" in
backend/*|api/*|*.py) RUN_BACKEND=true ;;
frontend/*|src/*|*.ts|*.tsx) RUN_FRONTEND=true ;;
tests/e2e/*|playwright/*) RUN_E2E=true ;;
.github/*|docker-compose*|Makefile)
# Infrastructure changes: run everything as a safety net
RUN_BACKEND=true; RUN_FRONTEND=true; RUN_E2E=true ;;
esac
done
echo "run-backend=$RUN_BACKEND" >> "$GITHUB_OUTPUT"
echo "run-frontend=$RUN_FRONTEND" >> "$GITHUB_OUTPUT"
echo "run-e2e=$RUN_E2E" >> "$GITHUB_OUTPUT"C’est tout. Le script lit le diff, classe chaque fichier modifié par répertoire et définit des indicateurs de sortie que les jobs en aval vérifient avant de s’exécuter.
Le workflow GitHub Actions : avant et après
Avant : tout exécuter systématiquement
# .github/workflows/ci.yml — BEFORE
name: CI
on: [push, pull_request]
jobs:
backend-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install ruff
- run: ruff check backend/
backend-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt
- run: pytest backend/tests/
frontend-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
e2e-test:
runs-on: ubuntu-latest
needs: [backend-test, frontend-test]
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright testChaque job s’exécute sur chaque PR. Une correction de faute de frappe dans un fichier de test E2E déclenche le linting backend, les tests backend, les tests frontend et les tests E2E. Temps total : 8 minutes 18 secondes.
Après : n’exécuter que ce qui est pertinent
# .github/workflows/ci.yml — AFTER
name: CI
on: [push, pull_request]
jobs:
detect-changes:
runs-on: ubuntu-latest
outputs:
run-backend: ${{ steps.changes.outputs.run-backend }}
run-frontend: ${{ steps.changes.outputs.run-frontend }}
run-e2e: ${{ steps.changes.outputs.run-e2e }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for accurate git diff
- id: changes
run: bash .github/scripts/detect-changes.sh
backend-lint:
needs: detect-changes
if: needs.detect-changes.outputs.run-backend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install ruff
- run: ruff check backend/
backend-test:
needs: detect-changes
if: needs.detect-changes.outputs.run-backend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install -r requirements.txt
- run: pytest backend/tests/
frontend-test:
needs: detect-changes
if: needs.detect-changes.outputs.run-frontend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm test
e2e-test:
needs: detect-changes
if: needs.detect-changes.outputs.run-e2e == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright testLe seul ajout est le job detect-changes (exécuté en environ 5 secondes) et les conditions if: sur chaque job en aval. Tout le reste est identique.
Les résultats
| Type de PR | Avant | Après | Gain |
|---|---|---|---|
| Modifications E2E uniquement | 8 min 18 sec | 39 sec | 95 % |
| Modifications frontend uniquement | 6 min 13 sec | 1 min 32 sec | 75 % |
| Modifications backend uniquement | 5 min 45 sec | 2 min 10 sec | 62 % |
| Modifications full-stack | 8 min 18 sec | 8 min 18 sec | 0 % (tout s’exécute) |
| Modifications CI/infra | 8 min 18 sec | 8 min 18 sec | 0 % (filet de sécurité) |
Sur une semaine type — environ 40 % de PR E2E uniquement, 30 % frontend uniquement, 20 % backend uniquement, 10 % full-stack — cela représente 7 à 8 heures par mois de temps d’attente développeur économisé.
Le filet de sécurité est important : toute modification des workflows .github/, des configurations Docker ou du Makefile déclenche le pipeline complet. On ne saute jamais la validation de l’infrastructure qui exécute vos validations.
Pourquoi ne pas utiliser une GitHub Action pour ça ?
C’est là que l’histoire devient intéressante. J’ai audité trois GitHub Actions populaires pour le filtrage CI basé sur les chemins avant d’écrire le script bash. Ce que j’ai trouvé m’a convaincu que zéro dépendance était le bon choix.
Action n°1 : communique avec une API commerciale
Une action populaire de filtrage par chemin effectue un appel API vers un service commercial à chaque exécution CI. Relisez bien : votre pipeline CI communique avec un tiers à chaque exécution. L’action fonctionne correctement quand le service est en ligne et que votre abonnement est actif. Quand ce n’est pas le cas — et j’ai trouvé des Issues GitHub d’utilisateurs le signalant — l’action peut faire échouer votre build. Votre pipeline CI a désormais une dépendance de disponibilité envers un produit SaaS que vous ne contrôlez pas.
Action n°2 : cible d’attaque sur la chaîne d’approvisionnement (CVE-2025-30066)
En mars 2025, la GitHub Action largement utilisée tj-actions/changed-files — utilisée par plus de 23 000 dépôts — a été compromise dans une attaque sur la chaîne d’approvisionnement. L’attaquant a modifié des tags de version existants pour pointer vers du code malveillant qui vidait la mémoire du runner CI, exposant les secrets du workflow, notamment les clés API, les tokens et les identifiants. La CISA a émis un avis formel (CVE-2025-30066). L’attaque a ensuite été retracée jusqu’à un token d’accès personnel compromis appartenant à un compte bot avec accès en écriture au dépôt.
Ce n’est pas un risque théorique. C’est arrivé. À une action qui fait exactement ce que mon script bash de 15 lignes fait.
Action n°3 : fonctionne bien, mais inutile
La troisième action que j’ai évaluée est bien maintenue, open-source et ne présente aucun problème de sécurité connu. Elle fonctionne correctement. Elle ajoute aussi une dépendance qui doit être verrouillée en version, surveillée pour les mises à jour et auditée périodiquement. Pour 15 lignes de bash qui font la même chose, la dépendance n’est pas justifiée.
L’argument sécurité en faveur de zéro dépendance
Chaque GitHub Action que vous ajoutez à votre workflow est du code que vous exécutez avec accès aux secrets de votre dépôt, à votre code source et à votre environnement CI. Chaque action est une décision de confiance. L’incident tj-actions/changed-files a démontré que même des actions populaires et largement utilisées peuvent être compromises.
L’approche par script bash a un profil de sécurité différent :
- Aucun appel réseau. Le script lit la sortie de
git diff. Il ne télécharge rien, ne communique avec rien et ne dépend de rien en dehors du dépôt. - Aucune exécution de code tiers. Le script est versionné dans votre dépôt et revu dans le même processus de PR que votre code applicatif.
- Pas de jeu de verrouillage de version. Pas de tag
@v4dont il faut se soucier. Aucun vecteur d’attaque sur la chaîne d’approvisionnement. Le code est le vôtre. - Auditabilité totale. N’importe qui dans l’équipe peut lire 15 lignes de bash et comprendre exactement ce que ça fait.
Parfois, la meilleure dépendance est l’absence de dépendance.
Cas limites et filets de sécurité
Quelques éléments que je gère et qu’une implémentation naïve manquerait :
Nouveaux fichiers sans catégorie claire
# If a file doesn't match any known pattern, run everything.
# Better to over-run than under-run.
*)
RUN_BACKEND=true; RUN_FRONTEND=true; RUN_E2E=true ;;Le cas par défaut déclenche une exécution complète. Un type de fichier non reconnu ne devrait jamais ignorer silencieusement la validation.
Fichiers supprimés
git diff --name-only inclut les fichiers supprimés. Un fichier backend supprimé nécessite quand même l’exécution de la suite de tests backend — la suppression pourrait casser un import.
Commits de merge
Le fetch-depth: 0 dans l’étape de checkout est essentiel. Sans l’historique git complet, le git diff par rapport à la branche de base produit des résultats inexacts. J’ai vu des configurations CI avec fetch-depth: 1 (la valeur par défaut) qui manquaient des modifications parce que le clone superficiel ne contenait pas la base de merge.
Ce que vous pouvez faire dès lundi matin
Mesurez votre référence actuelle. Quelle est la durée moyenne du CI pour votre type de PR le plus courant ? Si vous ne le savez pas, vérifiez les 20 dernières exécutions de workflow.
Classez vos jobs CI par répertoire. Quels jobs correspondent à quelles parties de la base de code ? La plupart des pipelines ont une correspondance naturelle.
Écrivez le script bash. Partez de la version ci-dessus et adaptez les patterns de répertoires à la structure de votre dépôt.
Ajoutez le job detect-changes. Intégrez-le à votre workflow avec les conditions
if:.Ajoutez le filet de sécurité. Toute modification de la configuration CI, des fichiers Docker ou des scripts d’infrastructure doit déclencher une exécution complète. Toujours.
Mesurez le résultat. Comparez les mêmes types de PR avant et après. Les chiffres plaideront mieux que n’importe quel argument.
La leçon plus large
Le pipeline CI est l’infrastructure la plus négligée dans la plupart des organisations d’ingénierie. Les équipes vont passer des semaines à optimiser des requêtes de base de données qui font gagner 200 ms par requête, puis regarder chaque développeur attendre 8 minutes de validation CI non pertinente 5 fois par jour sans se poser de questions.
La solution ici n’est pas astucieuse. Elle n’est pas nouvelle. C’est 15 lignes de bash qui posent une question simple : les modifications de cette PR affectent-elles réellement le code que ce job valide ?
Si la réponse est non, on le saute. Ce n’est pas prendre des raccourcis. C’est respecter le temps de votre équipe.
15 lignes. Zéro dépendance. 95 % plus rapide. Parfois, la solution la plus simple est celle que personne n’essaie parce qu’elle semble trop simple pour fonctionner.

