Aller au contenu
CI/CD & Automatisation medium

Workflows réutilisables GitHub Actions

15 min de lecture

Les workflows réutilisables permettent de factoriser la logique CI/CD commune. Au lieu de copier-coller les mêmes jobs dans 10 repositories, vous créez un workflow central appelable par tous les autres.

  • Créer un workflow réutilisable avec le trigger workflow_call
  • Définir des inputs, des secrets et des outputs typés
  • Appeler un workflow réutilisable depuis un autre dépôt ou en local
  • Passer les secrets explicitement ou via secrets: inherit
  • Chaîner et versionner vos templates CI/CD
  • Connaître les limitations : profondeur d'appel, secrets dynamiques

Ce guide s'adresse à ceux qui maintiennent plusieurs pipelines. Si vous débutez, voyez d'abord Workflows GitHub Actions.

Problème : vous avez 50 microservices avec des pipelines quasi identiques. Chaque modification (nouvelle version de Node, ajout d'un scan de sécurité) nécessite de modifier 50 fichiers.

Solution : un workflow réutilisable dans un repository central :

org/ci-templates/
└── .github/workflows/
├── node-ci.yml # CI standard Node.js
├── python-ci.yml # CI standard Python
└── docker-build.yml # Build et push d'images

Chaque microservice appelle ces templates :

# Dans chaque repo : 5 lignes au lieu de 100
jobs:
ci:
uses: org/ci-templates/.github/workflows/node-ci.yml@v1.0.0
with:
node-version: '20'
secrets: inherit

Un workflow réutilisable est un fichier de workflow comme les autres, à une différence près : son déclencheur le rend appelable par d'autres workflows.

Un workflow réutilisable utilise le trigger workflow_call :

.github/workflows/reusable-ci.yml
name: Reusable CI
on:
workflow_call: # Ce trigger rend le workflow appelable
inputs:
node-version:
description: 'Version de Node.js'
required: false
type: string
default: '20'
secrets:
npm-token:
description: 'Token NPM pour publish'
required: false
jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
- run: npm test

Les inputs permettent de paramétrer le workflow :

on:
workflow_call:
inputs:
# Input obligatoire
environment:
description: 'Environnement de déploiement'
required: true
type: string
# Input optionnel avec valeur par défaut
node-version:
description: 'Version de Node.js'
required: false
type: string
default: '20'
# Input booléen
skip-tests:
description: 'Skip les tests'
required: false
type: boolean
default: false
# Input numérique
timeout:
description: 'Timeout en minutes'
required: false
type: number
default: 30

Types d'inputs disponibles : string, boolean, number.

Les secrets sont déclarés séparément des inputs :

on:
workflow_call:
secrets:
# Secret obligatoire
deploy-key:
description: 'Clé SSH de déploiement'
required: true
# Secret optionnel
slack-webhook:
description: 'Webhook Slack pour notifications'
required: false

Les outputs permettent de retourner des valeurs au workflow appelant :

on:
workflow_call:
outputs:
version:
description: 'Version buildée'
value: ${{ jobs.build.outputs.version }}
image-digest:
description: "Digest de l'image Docker"
value: ${{ jobs.build.outputs.digest }}
jobs:
build:
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.version.outputs.value }}
digest: ${{ steps.push.outputs.digest }}
steps:
- id: version
run: echo "value=$(cat VERSION)" >> $GITHUB_OUTPUT
- id: push
run: echo "digest=sha256:abc123" >> $GITHUB_OUTPUT

Côté appelant, un workflow réutilisable s'invoque à la place des steps: d'un job, avec le mot-clé uses:.

Le job appelant ne contient pas de steps: — il délègue entièrement le travail au workflow réutilisable.

name: CI
on: [push, pull_request]
jobs:
call-workflow:
uses: owner/repo/.github/workflows/reusable.yml@v1.0.0
with:
node-version: '20'
environment: 'production'
secrets:
deploy-key: ${{ secrets.DEPLOY_KEY }}

Plusieurs formats sont possibles :

# Même repository (chemin relatif)
uses: ./.github/workflows/reusable.yml
# Repository externe avec branche (mobile, à éviter en production)
uses: org/repo/.github/workflows/reusable.yml@main
# Repository externe avec tag
uses: org/repo/.github/workflows/reusable.yml@v1.0.0
# Repository externe avec SHA (recommandé pour la sécurité)
uses: org/repo/.github/workflows/reusable.yml@a1b2c3d4e5f6

Trois méthodes existent pour passer les secrets au workflow réutilisable.

1. Secrets explicites — vous transmettez nommément ce dont le template a besoin, et rien d'autre :

jobs:
call:
uses: org/repo/.github/workflows/reusable.yml@v1.0.0
secrets:
npm-token: ${{ secrets.NPM_TOKEN }}
deploy-key: ${{ secrets.DEPLOY_KEY }}

2. Hériter tous les secretssecrets: inherit transmet l'intégralité des secrets du dépôt parent. Pratique, mais c'est l'option la plus large :

jobs:
call:
uses: org/repo/.github/workflows/reusable.yml@v1.0.0
secrets: inherit

3. Aucun secret — si le workflow réutilisable n'en a pas besoin, n'en passez aucun :

jobs:
call:
uses: org/repo/.github/workflows/reusable.yml@v1.0.0

Le workflow appelant récupère les valeurs retournées par le workflow réutilisable via needs.<job>.outputs.

jobs:
build:
uses: org/repo/.github/workflows/build.yml@v1.0.0
deploy:
needs: build
runs-on: ubuntu-24.04
steps:
- run: |
echo "Deploying version ${{ needs.build.outputs.version }}"
echo "Image digest: ${{ needs.build.outputs.image-digest }}"

Voici un cas réel de bout en bout : un template de CI Node.js et le workflow d'un service qui l'appelle.

Ce template centralise le lint et les tests Node.js, pilotables par des inputs.

org/ci-templates/.github/workflows/node-ci.yml
name: Node.js CI
on:
workflow_call:
inputs:
node-version:
type: string
default: '20'
working-directory:
type: string
default: '.'
run-tests:
type: boolean
default: true
run-lint:
type: boolean
default: true
secrets:
npm-token:
required: false
outputs:
test-passed:
description: 'Tests passés'
value: ${{ jobs.test.outputs.passed }}
permissions:
contents: read
jobs:
lint:
if: ${{ inputs.run-lint }}
runs-on: ubuntu-24.04
defaults:
run:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
cache-dependency-path: ${{ inputs.working-directory }}/package-lock.json
- run: npm ci
- run: npm run lint
test:
if: ${{ inputs.run-tests }}
runs-on: ubuntu-24.04
outputs:
passed: ${{ steps.test.outputs.passed }}
defaults:
run:
working-directory: ${{ inputs.working-directory }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
cache-dependency-path: ${{ inputs.working-directory }}/package-lock.json
- run: npm ci
- id: test
run: |
npm test
echo "passed=true" >> $GITHUB_OUTPUT

Côté service, le pipeline tient en quelques lignes : il appelle le template puis enchaîne un déploiement conditionnel.

my-service/.github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
ci:
uses: org/ci-templates/.github/workflows/node-ci.yml@v1.0.0
with:
node-version: '20'
run-lint: true
run-tests: true
secrets: inherit
deploy:
needs: ci
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
steps:
- run: |
echo "Tests passed: ${{ needs.ci.outputs.test-passed }}"
- run: ./deploy.sh

Une fois les bases acquises, ces patterns couvrent les cas que l'on rencontre sur de vrais portefeuilles de projets.

Le workflow réutilisable peut contenir une matrix :

reusable-test.yml
on:
workflow_call:
inputs:
os:
type: string
default: '["ubuntu-24.04"]'
node:
type: string
default: '["18", "20"]'
jobs:
test:
strategy:
matrix:
os: ${{ fromJSON(inputs.os) }}
node: ${{ fromJSON(inputs.node) }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ matrix.node }}
- run: npm ci && npm test

Appel avec matrix personnalisée :

jobs:
test:
uses: org/templates/.github/workflows/reusable-test.yml@v1.0.0
with:
os: '["ubuntu-24.04", "windows-latest"]'
node: '["18", "20", "22"]'

Plusieurs workflows réutilisables s'enchaînent avec needs, exactement comme des jobs classiques.

jobs:
lint:
uses: org/templates/.github/workflows/lint.yml@v1.0.0
test:
needs: lint
uses: org/templates/.github/workflows/test.yml@v1.0.0
build:
needs: test
uses: org/templates/.github/workflows/build.yml@v1.0.0
with:
version: ${{ needs.test.outputs.version }}
deploy:
needs: build
uses: org/templates/.github/workflows/deploy.yml@v1.0.0
with:
image: ${{ needs.build.outputs.image }}
secrets:
deploy-key: ${{ secrets.DEPLOY_KEY }}

Un workflow réutilisable déclare ses propres permissions: — le workflow appelant doit lui accorder au moins ce niveau.

reusable-deploy.yml
on:
workflow_call:
inputs:
environment:
type: string
required: true
# Permissions déclarées dans le workflow réutilisable
permissions:
contents: read
id-token: write # Pour OIDC
jobs:
deploy:
runs-on: ubuntu-24.04
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Deploy
run: ./deploy.sh

Quatre règles évitent les pièges les plus coûteux quand vos templates sont partagés par de nombreux dépôts.

Un workflow réutilisable est du code tiers : il s'épingle exactement comme une action. N'appelez jamais un template en @main — une référence mobile que le mainteneur peut faire pointer vers n'importe quoi. Utilisez un tag de version ou, mieux, un SHA de commit :

# Acceptable : tag de version
uses: org/templates/.github/workflows/ci.yml@v1.2.0
# Le plus sûr : SHA de commit
uses: org/templates/.github/workflows/ci.yml@a1b2c3d4e5f6

Le principe de l'épinglage et son outillage sont détaillés dans le guide Épingler les actions par SHA.

Une description claire sur chaque input évite aux équipes consommatrices de devoir lire le code du template.

on:
workflow_call:
inputs:
environment:
description: |
Environnement cible pour le déploiement.
Valeurs acceptées : staging, production.
Défaut : staging.
type: string
default: 'staging'

Des default: bien choisis rendent le template utilisable sans configuration dans le cas le plus courant.

inputs:
node-version:
type: string
default: '20' # LTS actuel
timeout:
type: number
default: 30 # Raisonnable pour la plupart des builds

Déclarez le minimum au niveau du workflow et n'élargissez qu'au niveau du job qui en a réellement besoin.

# Dans le workflow réutilisable
permissions:
contents: read # Minimum par défaut
jobs:
publish:
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write # Uniquement si nécessaire
steps:
- run: ./publish.sh

Les workflows réutilisables ont quelques contraintes structurantes — mieux vaut les connaître avant de bâtir toute une architecture dessus.

  • Profondeur d'appel : les workflows réutilisables peuvent s'imbriquer, mais sur 4 niveaux maximum
  • Nombre d'appels : maximum 20 workflows réutilisables uniques dans l'arbre d'appel
  • Secrets : les secrets ne peuvent pas être passés dynamiquement via expressions
  • Permissions : le workflow appelant doit avoir au moins les permissions requises par le workflow réutilisable
  • Un workflow réutilisable se déclare avec le trigger workflow_call et s'appelle via uses: à la place des steps: d'un job.
  • Les inputs sont typés (string, boolean, number) ; les secrets se déclarent à part.
  • Les outputs remontent au workflow appelant, qui les lit via needs.<job>.outputs.
  • secrets: inherit transmet tous les secrets du dépôt parent — pratique, mais à éviter quand le template n'a besoin que d'un ou deux secrets.
  • Versionnez vos templates par tag semver ou par SHA — jamais @main en production.
  • Limites à connaître : imbrication sur 4 niveaux maximum, 20 workflows réutilisables uniques par arbre d'appel, pas de secret passé dynamiquement.

Pour la référence complète, consultez la documentation officielle sur les workflows réutilisables.

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