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 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.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn