Aller au contenu
CI/CD & Automatisation medium

Conditions et if dans GitHub Actions

17 min de lecture

Les conditions (if) permettent de contrôler quand un job ou un step s'exécute. Vous pouvez exécuter du code uniquement sur certaines branches, après un échec, ou selon des critères métier.

  • Placer une condition if au bon niveau — job ou step — selon la granularité voulue
  • Écrire des expressions conditionnelles : comparaisons, opérateurs logiques, échappement
  • Utiliser les fonctions de statut success(), failure(), always() et cancelled()
  • Conditionner sur le context : branche, événement, acteur, pull request
  • Réagir aux outputs et aux résultats de steps et de jobs précédents
  • Éviter les trois pièges classiques des conditions if

Ce guide suppose que vous connaissez la structure d'un workflow. Sinon, commencez par les guides Workflows GitHub Actions et Contexts et expressions.

Une condition if se place soit sur un job entier, soit sur un step individuel. Le principe est identique, seule la portée change.

Placée au niveau du job, la condition décide si l'ensemble des steps s'exécute ou si le job entier est ignoré.

jobs:
deploy:
runs-on: ubuntu-24.04
# Ce job ne s'exécute que sur la branche main
if: github.ref == 'refs/heads/main'
steps:
- run: ./deploy.sh

Placée au niveau du step, la condition n'affecte que cette étape — les autres steps du job continuent normalement.

jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Run tests
run: npm test
# Ce step ne s'exécute qu'en cas d'échec du précédent
- name: Upload logs on failure
if: failure()
run: ./upload-logs.sh

Les conditions utilisent des expressions GitHub Actions avec la syntaxe ${{ }} (optionnelle dans if).

Les opérateurs de comparaison testent l'égalité, l'inégalité ou l'ordre de deux valeurs.

# Égalité
if: github.ref == 'refs/heads/main'
if: github.event_name == 'push'
if: matrix.os == 'ubuntu-24.04'
# Inégalité
if: github.ref != 'refs/heads/main'
# Comparaisons numériques
if: matrix.node >= 20
if: github.run_attempt > 1

Les opérateurs &&, || et ! combinent plusieurs conditions en une seule expression.

# ET logique
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
# OU logique
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
# Négation
if: "!contains(github.event.head_commit.message, '[skip ci]')"
# Combinaisons
if: |
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))

Ces fonctions évaluent le résultat des steps ou jobs précédents :

success() renvoie vrai tant qu'aucun step précédent n'a échoué — c'est la condition implicite de chaque step.

steps:
- run: npm test
# Équivalent à if: success()
- name: Deploy (si tests OK)
run: ./deploy.sh
# Explicite
- name: Notify success
if: success()
run: echo "All good!"

success() est le comportement par défaut : un step ne s'exécute que si tous les steps précédents ont réussi.

failure() ne s'active que si un step précédent a échoué : idéal pour remonter des artefacts de diagnostic ou alerter une équipe.

steps:
- name: Run tests
run: npm test
- name: Upload test artifacts on failure
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: test-results
path: ./test-results/
- name: Notify team on failure
if: failure()
run: |
curl -X POST "$SLACK_WEBHOOK" \
-d '{"text": "Tests failed on ${{ github.repository }}"}'
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

always() force l'exécution quoi qu'il arrive — succès, échec ou annulation. À réserver au nettoyage et à la collecte de logs.

steps:
- name: Run tests
run: npm test
# S'exécute TOUJOURS, même si un step a échoué ou le job est annulé
- name: Cleanup
if: always()
run: ./cleanup.sh
- name: Upload coverage
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: coverage
path: ./coverage/

cancelled() cible le cas où le workflow a été interrompu — manuellement ou par une nouvelle exécution qui remplace la précédente.

steps:
- name: Long running task
run: ./process.sh
- name: Notify cancellation
if: cancelled()
run: echo "Job was cancelled"

Combiner ces fonctions permet d'exprimer des cas plus fins, comme « toujours sauf si annulé ».

# S'exécute sur succès OU échec (mais pas si annulé)
if: success() || failure()
# S'exécute toujours SAUF si annulé
if: "!cancelled()"
# S'exécute uniquement si le job précédent a échoué
if: always() && needs.build.result == 'failure'

La plupart des conditions s'appuient sur le context github pour réagir à la branche, à l'événement ou à l'acteur qui a déclenché le workflow.

La propriété github.ref distingue branches et tags ; la fonction startsWith() reconnaît les familles de références.

# Branch main uniquement
if: github.ref == 'refs/heads/main'
# Branches commençant par "release/"
if: startsWith(github.ref, 'refs/heads/release/')
# Tags uniquement
if: startsWith(github.ref, 'refs/tags/')
# Tag avec pattern semver
if: startsWith(github.ref, 'refs/tags/v')

github.event_name indique quel déclencheur a lancé le workflow — utile quand un même workflow réagit à plusieurs événements.

# Push uniquement
if: github.event_name == 'push'
# Pull request uniquement
if: github.event_name == 'pull_request'
# Déclenchement manuel
if: github.event_name == 'workflow_dispatch'
# Planifié (cron)
if: github.event_name == 'schedule'
# Plusieurs événements
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'

github.actor identifie le compte à l'origine de l'exécution, ce qui permet de traiter les bots à part des contributeurs humains.

# Utilisateur spécifique
if: github.actor == 'admin-user'
# Bots (Dependabot, Renovate)
if: github.actor == 'dependabot[bot]'
if: github.actor == 'renovate[bot]'
# Exclure les bots
if: "!contains(github.actor, '[bot]')"

Les conditions sur les pull requests filtrent selon la branche cible, l'origine (fork ou non) ou les labels de la PR.

# PR vers main
if: github.event_name == 'pull_request' && github.base_ref == 'main'
# PR depuis un fork
if: github.event.pull_request.head.repo.fork == true
# PR non-draft
if: github.event.pull_request.draft == false
# PR avec label spécifique
if: contains(github.event.pull_request.labels.*.name, 'deploy')

Une condition peut aussi dépendre d'une valeur calculée plus tôt dans le workflow, via les outputs d'un step ou d'un job.

Un step écrit une valeur dans $GITHUB_OUTPUT, et un step suivant la lit dans sa condition if.

steps:
- name: Check changes
id: changes
run: |
if git diff --name-only HEAD~1 | grep -q "^src/"; then
echo "src_changed=true" >> $GITHUB_OUTPUT
else
echo "src_changed=false" >> $GITHUB_OUTPUT
fi
- name: Build
if: steps.changes.outputs.src_changed == 'true'
run: npm run build

Un job expose un output ; un job dépendant le lit via needs pour décider s'il doit s'exécuter.

jobs:
check:
runs-on: ubuntu-24.04
outputs:
should_deploy: ${{ steps.check.outputs.deploy }}
steps:
- id: check
run: |
if [ "${{ github.ref }}" == "refs/heads/main" ]; then
echo "deploy=true" >> $GITHUB_OUTPUT
else
echo "deploy=false" >> $GITHUB_OUTPUT
fi
deploy:
needs: check
if: needs.check.outputs.should_deploy == 'true'
runs-on: ubuntu-24.04
steps:
- run: ./deploy.sh

needs.<job>.result donne le verdict d'un job précédent — success, failure, skipped ou cancelled — à condition de dépendre de ce job.

jobs:
build:
runs-on: ubuntu-24.04
steps:
- run: npm run build
test:
runs-on: ubuntu-24.04
steps:
- run: npm test
notify:
needs: [build, test]
if: always()
runs-on: ubuntu-24.04
steps:
- name: Notify success
if: needs.build.result == 'success' && needs.test.result == 'success'
run: echo "All good!"
- name: Notify failure
if: needs.build.result == 'failure' || needs.test.result == 'failure'
run: echo "Something failed!"

Voici les conditions que l'on retrouve dans presque tous les pipelines réels — à copier puis adapter à votre contexte.

Ce pattern évite de relancer le pipeline quand le message de commit contient le marqueur [skip ci].

jobs:
build:
# Ne pas exécuter si le message de commit contient [skip ci]
if: "!contains(github.event.head_commit.message, '[skip ci]')"
runs-on: ubuntu-24.04
steps:
- run: npm test

Chaque environnement reçoit son propre job, déclenché par la branche correspondante.

jobs:
deploy-staging:
if: github.ref == 'refs/heads/develop'
environment: staging
runs-on: ubuntu-24.04
steps:
- run: ./deploy.sh staging
deploy-production:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production
runs-on: ubuntu-24.04
steps:
- run: ./deploy.sh production

Un même job adapte son comportement selon qu'il tourne sur un push ou sur une exécution planifiée — par exemple un scan rapide contre un scan complet.

jobs:
scan:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Scan rapide sur push
- name: Quick scan
if: github.event_name == 'push'
run: ./scan.sh --quick
# Scan complet sur schedule
- name: Full scan
if: github.event_name == 'schedule'
run: ./scan.sh --full

Cette condition empêche de relancer toute la CI à chaque mise à jour automatique de dépendances poussée par Dependabot ou Renovate.

jobs:
test:
# Ne pas déclencher les tests pour les commits de bots
if: |
github.actor != 'dependabot[bot]' &&
github.actor != 'renovate[bot]'
runs-on: ubuntu-24.04
steps:
- run: npm test

Sur une pull request, le déploiement tourne en simulation ; il ne devient réel que sur la branche principale.

jobs:
deploy:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Dry-run sur les PRs
- name: Deploy (dry-run)
if: github.event_name == 'pull_request'
run: ./deploy.sh --dry-run
# Déploiement réel sur main
- name: Deploy (real)
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: ./deploy.sh

Dans une matrice, une condition sur matrix.* réserve certains steps à un système ou à une version donnés.

jobs:
test:
strategy:
matrix:
os: [ubuntu-24.04, windows-latest, macos-latest]
node: [18, 20, 22]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Step spécifique Windows
- name: Setup (Windows)
if: matrix.os == 'windows-latest'
run: choco install nodejs
# Step spécifique aux anciennes versions
- name: Legacy compatibility check
if: matrix.node < 20
run: npm run test:legacy

Trois pièges reviennent systématiquement avec les conditions if. Les connaître évite des heures de debug sur un job qui ne se déclenche jamais — ou toujours.

L'erreur la plus fréquente : comparer github.ref à un nom de branche nu, alors que cette propriété contient le préfixe refs/heads/.

# ❌ Toujours faux : github.ref inclut "refs/heads/"
if: github.ref == 'main'
# ✅ Correct
if: github.ref == 'refs/heads/main'
if: github.ref_name == 'main'

Une expression qui commence par ! casse le parseur YAML si elle n'est pas entourée de guillemets.

# ❌ Erreur YAML
if: !contains(...)
# ✅ Correct
if: "!contains(...)"

Une condition if: false sur un job rend tous ses steps inatteignables — placez la condition au bon niveau de granularité.

# ❌ Le job est skipped, les steps ne s'exécutent jamais
jobs:
deploy:
if: false
steps:
- if: true # Jamais atteint
run: echo "Never runs"
# ✅ Condition au niveau du step pour plus de granularité
jobs:
deploy:
steps:
- if: github.ref == 'refs/heads/main'
run: ./deploy.sh
  • Une condition if se place sur un job (tout ou rien) ou sur un step (granularité fine).
  • Sans if, chaque step porte une condition success() implicite : il est ignoré dès qu'un step précédent échoue.
  • failure(), always() et cancelled() débloquent les steps même après un échec — indispensables pour le diagnostic et le nettoyage.
  • Comparez toujours github.ref à la référence complète (refs/heads/main), ou utilisez github.ref_name.
  • Une expression qui commence par ! doit être entre guillemets, sinon le YAML casse.
  • Pour réagir au verdict d'un job précédent, lisez needs.<job>.result depuis un job qui en dépend.

Pour la liste exhaustive des opérateurs et fonctions, gardez sous la main la documentation officielle des expressions.

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