Aller au contenu
CI/CD & Automatisation medium

Actions composites GitHub Actions

20 min de lecture

Les actions composites permettent de regrouper plusieurs steps en une action réutilisable. Contrairement aux workflows réutilisables qui opèrent au niveau des jobs, les actions composites s'intègrent comme un step dans n'importe quel workflow.

  • Distinguer action composite et workflow réutilisable, et choisir le bon
  • Créer un fichier action.yml avec runs.using: composite
  • Définir des inputs et des outputs typés et documentés
  • Appeler une action composite locale ou hébergée dans un autre dépôt
  • Sécuriser les steps : épinglage SHA, secrets passés par env:
  • Publier et versionner une action sur le GitHub Marketplace

Ce guide s'adresse à ceux qui répètent les mêmes séquences de steps dans plusieurs workflows. Si vous débutez, voyez d'abord Workflows GitHub Actions.

Les deux mécanismes factorisent du code CI/CD, mais à des granularités différentes. Une action composite est un step : elle s'insère dans un job existant, aux côtés d'autres steps. Un workflow réutilisable est un job complet : il s'appelle à la place des steps:. Le tableau ci-dessous résume quand chacun s'impose.

CritèreActions compositesWorkflows réutilisables
NiveauStepJob
Fichieraction.yml.github/workflows/*.yml
Appeluses: dans un stepuses: au niveau job
OutputsOutputs de stepOutputs de job
SecretsVia ${{ secrets.* }} dans le workflowPassés explicitement
MatrixNonOui
ParallélismeNon (séquentiel)Oui (jobs parallèles)

Utilisez les actions composites pour : des séquences de steps réutilisables. Utilisez les workflows réutilisables pour : des pipelines complets avec jobs.

Une action composite vit dans son propre fichier action.yml, à la racine d'un dossier dédié. Ce fichier décrit ce que l'action attend (inputs), ce qu'elle renvoie (outputs) et la séquence de steps qu'elle exécute (runs).

Une action composite tient dans un seul fichier action.yml. Il déclare les métadonnées, les inputs, les outputs et la séquence de steps sous la clé runs. Voici une action complète qui installe Node.js et lance les tests :

my-action/action.yml
name: 'Setup and Test'
description: "Configure l'environnement et lance les tests"
author: 'Stéphane Robert'
inputs:
node-version:
description: 'Version de Node.js'
required: false
default: '20'
outputs:
test-result:
description: 'Résultat des tests'
value: ${{ steps.test.outputs.result }}
runs:
using: 'composite' # Indique que c'est une action composite
steps:
- name: Checkout du dépôt
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Installer Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- name: Installer les dépendances
run: npm ci
shell: bash
- name: Lancer les tests
id: test
run: |
npm test
echo "result=success" >> "$GITHUB_OUTPUT"
shell: bash

Quatre clés structurent tout fichier action.yml, plus une contrainte propre aux actions composites : chaque step run doit déclarer son shell.

PropriétéDescription
nameNom de l'action
descriptionDescription courte
runs.usingDoit être composite
runs.stepsListe des steps
shellObligatoire pour chaque step run

Les inputs sont les paramètres que le workflow appelant transmet à l'action. Chacun se déclare avec une description, un caractère obligatoire ou non (required) et, le cas échéant, une valeur par défaut.

inputs:
# Input obligatoire
environment:
description: 'Environnement cible'
required: true
# Input optionnel avec défaut
node-version:
description: 'Version de Node.js'
required: false
default: '20'
# Input booléen (passé comme string)
skip-cache:
description: 'Désactiver le cache'
required: false
default: 'false'

Accès aux inputs : ${{ inputs.input-name }}

Les outputs exposent au workflow appelant des valeurs calculées pendant l'exécution. Chaque output référence la sortie d'un step interne via son id — le step doit donc obligatoirement porter un id.

outputs:
version:
description: 'Version détectée'
value: ${{ steps.detect.outputs.version }}
artifact-path:
description: "Chemin de l'artifact"
value: ${{ steps.build.outputs.path }}
runs:
using: 'composite'
steps:
- name: Détecter la version
id: detect
run: echo "version=$(cat VERSION)" >> "$GITHUB_OUTPUT"
shell: bash
- name: Construire le projet
id: build
run: |
npm run build
echo "path=./dist" >> "$GITHUB_OUTPUT"
shell: bash

Accès aux outputs (dans le workflow appelant) — la valeur transite par un bloc env: avant d'être utilisée dans le run: :

- uses: ./my-action
id: setup
- name: Afficher la version détectée
env:
DETECTED_VERSION: ${{ steps.setup.outputs.version }}
run: echo "Version : $DETECTED_VERSION"

Une action composite s'invoque comme n'importe quelle action : le mot-clé uses: dans un step. Ce qui change, c'est d'où vient l'action — un dossier du dépôt courant ou un dépôt externe.

Une action stockée dans le dépôt courant s'appelle par un chemin relatif commençant par ./. Son code est versionné avec le workflow, au même commit : inutile de l'épingler par SHA, c'est votre propre code.

name: CI
on:
push:
branches: [main]
permissions: {}
jobs:
build:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Checkout du dépôt
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
# Action dans le même repo
- name: Lancer l'action composite locale
uses: ./.github/actions/my-action
with:
node-version: '20'

Structure du repo :

  • Répertoiremy-repo/
    • Répertoire.github/
      • Répertoireactions/
        • Répertoiremy-action/
          • action.yml
      • Répertoireworkflows/
        • ci.yml
    • Répertoiresrc/

Une action hébergée dans un autre dépôt s'appelle avec la notation propriétaire/dépôt. C'est du code tiers : il s'épingle par SHA de commit, exactement comme une action du Marketplace.

- name: Installer Node.js via une action externe
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: '20'

Une action externe se référence par un tag, une branche ou un SHA de commit. Une seule de ces formes est sûre : le SHA. Un tag (@v4) comme une branche (@main) sont des références mobiles — le mainteneur peut les déplacer vers n'importe quel code, y compris malveillant, sans que votre workflow change d'une ligne.

# ❌ Référence mobile : le mainteneur peut la déplacer
- uses: actions/setup-node@v4
# ✅ SHA de commit épinglé, version en commentaire
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0

L'épinglage et son outillage (Dependabot, pinact) sont détaillés dans Épingler les actions par SHA.

Trois actions composites tirées de cas réels : préparer un environnement Node.js, lancer une passe de sécurité, déployer sur Kubernetes. Chacune illustre un usage différent des inputs et des outputs.

Cette action factorise l'installation de Node.js et la mise en cache des dépendances npm — la séquence que l'on recopie dans presque tous les pipelines JavaScript.

.github/actions/setup-node/action.yml
name: 'Setup Node.js with Cache'
description: 'Configure Node.js avec cache npm optimisé'
inputs:
node-version:
description: 'Version de Node.js'
default: '20'
working-directory:
description: 'Répertoire de travail'
default: '.'
outputs:
cache-hit:
description: 'Cache hit'
value: ${{ steps.cache.outputs.cache-hit }}
runs:
using: 'composite'
steps:
- name: Installer Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: ${{ inputs.node-version }}
- name: Récupérer le répertoire de cache npm
id: npm-cache-dir
run: echo "dir=$(npm config get cache)" >> "$GITHUB_OUTPUT"
shell: bash
- name: Mettre npm en cache
id: cache
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
with:
path: ${{ steps.npm-cache-dir.outputs.dir }}
key: npm-${{ runner.os }}-${{ hashFiles(format('{0}/package-lock.json', inputs.working-directory)) }}
restore-keys: npm-${{ runner.os }}-
- name: Installer les dépendances
working-directory: ${{ inputs.working-directory }}
run: npm ci
shell: bash

Cette action regroupe deux scannersTrivy pour les vulnérabilités, Gitleaks pour les secrets — derrière un seul uses:. L'input scan-type permet d'activer la passe lente uniquement quand c'est utile.

.github/actions/security-scan/action.yml
name: 'Security Scan'
description: 'Lance les scans de sécurité (Trivy + Gitleaks)'
inputs:
scan-type:
description: 'Type de scan (full, quick)'
default: 'quick'
fail-on-high:
description: 'Échec si vulnérabilités high+'
default: 'true'
outputs:
vulnerabilities-found:
description: 'Vulnérabilités trouvées'
value: ${{ steps.result.outputs.found }}
runs:
using: 'composite'
steps:
- name: Scanner les vulnérabilités avec Trivy
id: trivy
uses: aquasecurity/trivy-action@915b19bbe73b92a6cf82a1bc12b087c9a19a5fe2 # v0.28.0
with:
scan-type: 'fs'
scan-ref: '.'
severity: ${{ inputs.fail-on-high == 'true' && 'HIGH,CRITICAL' || 'CRITICAL' }}
exit-code: ${{ inputs.fail-on-high == 'true' && '1' || '0' }}
continue-on-error: true
- name: Évaluer le résultat de Trivy
id: result
env:
TRIVY_OUTCOME: ${{ steps.trivy.outcome }}
run: |
if [ "$TRIVY_OUTCOME" = "failure" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "found=false" >> "$GITHUB_OUTPUT"
fi
shell: bash
- name: Détecter les secrets avec Gitleaks
uses: gitleaks/gitleaks-action@ff98106e4c7b2bc287b24eaf42907196329070c7 # v2.3.9
if: inputs.scan-type == 'full'
env:
GITHUB_TOKEN: ${{ github.token }}

Cette action déploie une image sur Kubernetes. Elle manipule un secret (le kubeconfig) et des paramètres variables — l'occasion de montrer comment passer ces valeurs sans les exposer.

.github/actions/deploy/action.yml
name: 'Deploy to Kubernetes'
description: "Déploie l'application sur Kubernetes"
inputs:
environment:
description: 'Environnement (staging, production)'
required: true
image:
description: 'Image Docker à déployer'
required: true
kubeconfig:
description: 'Kubeconfig encodé en base64'
required: true
outputs:
deployment-url:
description: 'URL du déploiement'
value: ${{ steps.deploy.outputs.url }}
runs:
using: 'composite'
steps:
- name: Installer kubectl
uses: azure/setup-kubectl@3e0aec4d80787158d308d7b364cb1b702e7feb7f # v4.0.0
- name: Configurer le kubeconfig
env:
KUBECONFIG_B64: ${{ inputs.kubeconfig }}
run: |
mkdir -p ~/.kube
printf '%s' "$KUBECONFIG_B64" | base64 -d > ~/.kube/config
chmod 600 ~/.kube/config
shell: bash
- name: Déployer sur Kubernetes
id: deploy
env:
DEPLOY_IMAGE: ${{ inputs.image }}
DEPLOY_ENV: ${{ inputs.environment }}
run: |
kubectl set image deployment/app "app=$DEPLOY_IMAGE" -n "$DEPLOY_ENV"
kubectl rollout status deployment/app -n "$DEPLOY_ENV"
URL=$(kubectl get ingress -n "$DEPLOY_ENV" \
-o jsonpath='{.items[0].spec.rules[0].host}')
echo "url=https://$URL" >> "$GITHUB_OUTPUT"
shell: bash

Au-delà de deux ou trois actions, leur rangement compte. Deux approches dominent : un dépôt dédié qui regroupe toutes les actions de l'organisation, ou des actions locales au dépôt qu'elles servent.

Un dépôt unique — souvent nommé actions — héberge toutes les actions partagées de l'organisation. Chaque action occupe un sous-dossier avec son action.yml.

  • Répertoireactions/
    • Répertoiresetup-node/
      • action.yml
    • Répertoiresecurity-scan/
      • action.yml
    • Répertoiredeploy/
      • action.yml
    • Répertoirenotify/
      • action.yml

Une action située dans un sous-dossier s'adresse par org/dépôt/sous-dossier. Même au sein de votre organisation, épinglez chaque appel par SHA : un dépôt interne peut être compromis comme un autre.

jobs:
ci:
runs-on: ubuntu-24.04
steps:
- name: Préparer Node.js
uses: org/actions/setup-node@2b9f4c7e1a8d3f6b0c5e9a2d7f4b1c8e3a6d0f9b # v1.4.0
- name: Scanner la sécurité
uses: org/actions/security-scan@6e3a1f8c4b7d2a9e0c5f8b3d6a1e4c7f9b2d5a0e # v1.4.0

Quand une action ne sert qu'à un seul projet, inutile de la sortir : placez-la dans .github/actions/ du dépôt. Elle est versionnée avec le code et s'appelle par chemin relatif.

  • Répertoiremy-app/
    • Répertoire.github/
      • Répertoireactions/
        • Répertoiresetup/
          • action.yml
        • Répertoiredeploy/
          • action.yml
      • Répertoireworkflows/
        • ci.yml
    • Répertoiresrc/

Une action composite utile à d'autres équipes peut être publiée sur le GitHub Marketplace, l'annuaire public des actions. La publication impose quelques métadonnées et une discipline de versionnement.

La mise en ligne se fait depuis l'interface GitHub, une fois l'action prête.

  1. Créez un repository public pour l'action.

  2. Ajoutez un bloc branding dans action.yml :

    name: 'My Action'
    description: "Description de l'action"
    branding:
    icon: 'check-circle'
    color: 'green'
    # inputs, outputs et runs : voir les sections précédentes
  3. Créez une release avec un tag semver (v1.0.0).

  4. Publiez sur le Marketplace depuis la page Releases.

Maintenez deux niveaux de tags : un tag exact et immuable par release (v1.2.3), et un tag majeur mobile (v1) repointé à chaque release compatible. Les consommateurs choisissent ainsi entre stabilité absolue et mises à jour automatiques.

Fenêtre de terminal
# Tag de version spécifique
git tag v1.2.3
git push origin v1.2.3
# Tag majeur (pointe vers la dernière v1.x.x)
git tag -f v1
git push -f origin v1

Côté consommateur, on n'utilise ni l'un ni l'autre directement : on épingle le SHA du commit derrière le tag visé, version en commentaire (voir Épingler les actions appelées).

Quelques réflexes rendent vos actions composites fiables et faciles à consommer pour les autres équipes.

Un output d'action référence la sortie d'un step interne : ce step doit porter un id, sinon la valeur est introuvable.

- name: Construire le projet
id: build # Nécessaire pour référencer les outputs
run: echo "version=1.0" >> "$GITHUB_OUTPUT"
shell: bash
outputs:
version:
value: ${{ steps.build.outputs.version }}

Une description précise évite aux équipes consommatrices d'aller lire le code de l'action pour comprendre ce qu'elles peuvent passer.

inputs:
environment:
description: |
L'environnement cible pour le déploiement.
Valeurs supportées : staging, production.
Les permissions requises varient selon l'environnement.
required: true

continue-on-error sur un step critique permet d'enchaîner une étape de repli plutôt que d'interrompre brutalement le job.

- name: Étape critique
id: critical
run: ./critical-script.sh
shell: bash
continue-on-error: true
- name: Gérer l'échec
if: steps.critical.outcome == 'failure'
run: ./fallback.sh
shell: bash
  • Une action composite est un step réutilisable décrit dans un action.yml avec runs.using: composite.
  • Chaque step run: d'une action composite doit déclarer un shell: ; les steps uses: n'en prennent pas.
  • Les inputs sont toujours des strings ; les outputs référencent la sortie d'un step interne identifié par un id.
  • Une action locale s'appelle par chemin relatif (./...) ; une action externe est du code tiers épinglé par SHA.
  • Secrets et paramètres passent par env:, jamais interpolés en ${{ }} dans un bloc run: — sinon injection de commande et fuite dans les logs.
  • Côté publication : un action.yml avec branding, un tag exact immuable doublé d'un tag majeur mobile ; côté consommation, on épingle le SHA.

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

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