Aller au contenu
CI/CD & Automatisation medium

Concurrency GitHub Actions : éviter les runs parallèles

10 min de lecture

Par défaut, GitHub Actions exécute chaque déclenchement indépendamment. Si vous poussez 5 commits rapidement, vous obtenez 5 workflows en parallèle. C'est du gaspillage de ressources et, pire, une source de conflits de déploiement. Le bloc concurrency règle les deux.

  • Annuler automatiquement les runs obsolètes avec cancel-in-progress
  • Définir un groupe de concurrence par branche, par PR ou par environnement
  • Sérialiser les déploiements pour éviter deux déploiements simultanés
  • Protéger la branche main des annulations intempestives
  • Éviter les pièges : groupe trop large, trop spécifique, ou dangereux

Sans garde-fou, chaque push lance son propre workflow. Sur une branche active, les premiers runs sont déjà obsolètes avant même de finir : seul le dernier commit compte.

Commit 1 → Workflow 1 (en cours)
Commit 2 → Workflow 2 (en cours) ← Inutile, commit 2 sera écrasé
Commit 3 → Workflow 3 (en cours) ← Inutile aussi
Commit 4 → Workflow 4 (en cours) ← Inutile aussi
Commit 5 → Workflow 5 (en cours) ← Seul celui-ci compte vraiment

Solution : annuler automatiquement les runs obsolètes.

Le bloc concurrency se déclare au niveau du workflow (ou d'un job). Deux propriétés suffisent : un groupe et la décision d'annuler ou non.

name: CI
on: [push, pull_request]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-24.04
steps: [...]
PropriétéDescription
groupIdentifiant du groupe de concurrence
cancel-in-progressAnnuler les runs en cours du même groupe

Tout se joue dans la composition du groupe. Deux runs qui partagent un groupe identique entrent en concurrence ; sinon ils s'ignorent. Voici les quatre découpages les plus utiles.

# Annule les runs précédents sur la même branche
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

Un push sur main annule les runs précédents sur main, mais pas ceux sur develop.

# Annule les runs précédents sur la même PR
concurrency:
group: pr-${{ github.event.pull_request.number }}
cancel-in-progress: true
# Un seul run de ce workflow à la fois (global)
concurrency:
group: deploy-production
cancel-in-progress: false # Attendre au lieu d'annuler
# Un seul déploiement par environnement
concurrency:
group: deploy-${{ github.event.inputs.environment || 'staging' }}
cancel-in-progress: false

Annuler les runs obsolètes pour économiser les ressources :

name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions: {}
jobs:
build:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- run: npm ci && npm test

Ne pas annuler, mais attendre que le déploiement précédent soit terminé :

name: Deploy
on:
push:
branches: [main]
concurrency:
group: deploy-production
cancel-in-progress: false # Attendre, ne pas annuler
permissions: {}
jobs:
deploy:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- run: ./deploy.sh
name: CI and Deploy
on:
push:
branches: [main]
# Concurrence globale pour ce workflow
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# Aucun droit par défaut
permissions: {}
jobs:
test:
runs-on: ubuntu-24.04
steps:
- run: npm test
deploy:
needs: test
runs-on: ubuntu-24.04
# Concurrence spécifique pour le déploiement
concurrency:
group: deploy-production
cancel-in-progress: false
steps:
- run: ./deploy.sh

Ne pas annuler les runs sur la branche principale :

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# Annuler uniquement sur les PRs
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

La valeur de cancel-in-progress change radicalement ce qui arrive aux runs déjà lancés. Comparons les deux modes.

Workflow 1 (en cours) → ANNULÉ
Workflow 2 (en cours) → ANNULÉ
Workflow 3 (nouveau) → S'EXÉCUTE
Workflow 1 (en cours) → SE TERMINE
Workflow 2 (en attente) → ATTEND
Workflow 3 (en attente) → ATTEND

Les workflows en attente s'exécutent dans l'ordre.

Vous pouvez aussi définir la concurrence au niveau d'un job spécifique :

jobs:
test:
runs-on: ubuntu-24.04
# Pas de restriction de concurrence pour les tests
steps: [...]
deploy:
needs: test
runs-on: ubuntu-24.04
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false
steps: [...]

Dans l'onglet Actions, les runs annulés affichent le statut "Cancelled" avec le message "This run was cancelled because a newer run was started".

- name: Debug concurrency
env:
# Les valeurs de contexte passent par env:, jamais dans run: en clair
WORKFLOW: ${{ github.workflow }}
REF: ${{ github.ref }}
RUN_ID: ${{ github.run_id }}
RUN_ATTEMPT: ${{ github.run_attempt }}
run: |
echo "Workflow: $WORKFLOW"
echo "Ref: $REF"
echo "Run ID: $RUN_ID"
echo "Run attempt: $RUN_ATTEMPT"

Trois réglages de groupe se retournent contre vous. Voici comment les reconnaître et les corriger.

# ❌ Tous les workflows sont dans le même groupe !
concurrency:
group: ci
cancel-in-progress: true
# ✅ Groupe par workflow ET branche
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
# ❌ Chaque run a son propre groupe (inutile)
concurrency:
group: ${{ github.run_id }}
# ✅ Groupe par branche
concurrency:
group: ${{ github.ref }}
# ❌ Dangereux : peut annuler un déploiement en cours
concurrency:
group: deploy
cancel-in-progress: true
# ✅ Attendre le déploiement précédent
concurrency:
group: deploy
cancel-in-progress: false
  • concurrency regroupe les runs et annule ou met en file ceux du même groupe.
  • Le groupe canonique est ${{ github.workflow }}-${{ github.ref }} : un groupe par workflow et par branche.
  • cancel-in-progress: true pour la CI (économiser des minutes), false pour les déploiements (ne jamais couper un déploiement en cours).
  • Un groupe trop large annule des runs sans rapport ; un groupe trop spécifique (github.run_id) n'annule jamais rien.
  • Protégez main en conditionnant l'annulation : cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}.

Pour la référence complète, consultez la documentation officielle sur la concurrency.

Ce site vous est utile ?

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

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn