À la une · Fiche technique

Comment optimiser vos workflows GitHub Actions ?

Vos workflows GitHub Actions sont lents ? Découvrez comment les accélérer avec le cache des dépendances, les matrices de test en parallèle, le concurrency pour annuler les runs obsolètes, les jobs conditionnels et timeouts. Des pipelines plus rapides et surtout moins gourmands.

Intermédiaire
Un tapis roulant d'usine accéléré avec des rouages bien huilés, pixel art

Au début, on est souvent content que notre workflow tourne. Puis vient le moment où on attend 6 minutes à chaque push pour un simple npm test et là, on commence à trouver le temps long et plus personne n’aime la CI.

Un workflow lent, ce n’est pas seulement un confort en moins. C’est un feedback plus lent (vous attendez avant de savoir si votre code passe) et plus de minutes consommées sur votre quota GitHub Actions.

Dans cette fiche, on va voir les leviers les plus efficaces pour accélérer vos workflows GitHub Actions sans rien sacrifier. C’est un peu l’équivalent CI/CD de la fiche Comment optimiser une image Docker ? : on cherche à faire pareil, mais en moins de temps.


Levier n°1 - Mettre en cache les dépendances

C’est de loin le gain le plus rentable. À chaque run, réinstaller toutes vos dépendances depuis zéro, c’est du temps perdu et de l’argent jeté par les fenêtres.. Le cache permet de les réutiliser d’un run à l’autre.


La bonne nouvelle, c’est que les actions setup-* intègrent déjà un cache. Pour Node.js, il suffit d’une ligne :

- name: Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: "npm"

Le cache: "npm" va automatiquement mettre en cache votre dossier de dépendances et le restaurer au run suivant. Simple et redoutablement efficace.


Pour des cas plus spécifiques, il existe l’action générique actions/cache, où vous définissez vous-même quoi mettre en cache et avec quelle clé :

- name: Cache des dépendances
  uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-${{ hashFiles('package-lock.json') }}

La clé basée sur hashFiles(...) garantit qu’on recharge le cache uniquement tant que le package-lock.json n’a pas changé.


Levier n°2 - Paralléliser avec une matrice

Vous devez tester votre code sur plusieurs versions de Node ? Ne les enchaînez pas les unes après les autres, lancez-les plutôt en parallèle avec une matrice.

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci
      - run: npm test

GitHub va créer trois jobs en parallèle, un par version. Vous testez du coup trois configurations dans le temps d’une seule. C’est aussi valable pour tester plusieurs OS, plusieurs versions de Python, etc. C’est un peu l’idéal pour les libs.


Levier n°3 - Annuler les runs obsolètes avec concurrency

Voici un gaspillage classique : vous poussez trois commits d’affilée sur une PR, et GitHub lance trois workflows complets alors que seul le dernier vous intéresse.


Le mot-clé concurrency règle ça en annulant automatiquement les runs en cours devenus inutiles :

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

Traduction : « pour un même workflow sur une même branche, garde seulement le run le plus récent et annule les autres ». Vous économisez des minutes sans rien perdre d’utile.


Levier n°4 - Ne lancer que ce qui est nécessaire

Pourquoi relancer toute la CI si vous n’avez modifié qu’un fichier de documentation ? Deux outils pour ça.


Le filtre paths au niveau du déclencheur, pour ne partir que si certains fichiers changent :

on:
  push:
    branches: [main]
    paths:
      - "src/**"
      - "package.json"

Et la condition if au niveau d’un job ou d’un step, pour l’exécuter seulement dans certains cas :

deploy:
  if: github.ref == 'refs/heads/main'
  runs-on: ubuntu-latest
  steps:
    - run: ./deploy.sh

Ici, le déploiement ne se lance que sur la branche main, jamais sur une PR.


Levier n°5 - Poser des garde-fous

Ces deux réglages simples peuvent vous éviter des mauvaises surprises :

  • timeout-minutes empêche un job bloqué de tourner (et de consommer vos minutes) pendant des heures :
jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 10
  • fail-fast: false dans une matrice permet, au contraire, de ne pas tout arrêter dès qu’un job échoue — pratique quand vous voulez voir le résultat de toutes les versions, pas seulement la première qui casse :
strategy:
  fail-fast: false
  matrix:
    node: [18, 20, 22]

Bonus - Le cache pour vos images Docker

Si vous déployez une image Docker depuis GitHub Actions, sachez que docker/build-push-action sait aussi utiliser un cache pour ne pas reconstruire toutes les couches à chaque fois :

- uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: moncompte/mon-api:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max

Combiné à un bon multi-stage build, c’est le combo gagnant pour des builds d’images vraiment rapides en CI.


Et voilà, vos workflows devraient déjà tourner nettement plus vite ! Pour résumer les réflexes : on met les dépendances en cache, on parallélise avec une matrice, on annule les runs obsolètes, on ne lance que le nécessaire, et on pose des garde-fous.

D’ici là, je vous invite :

Ressources

Partager

Codez bien !