GitHub Actions : architecture CI/CD réutilisable
Quand plusieurs dépôts partagent la même stack technique, chacun maintient souvent une copie quasi-identique de ses workflows CI/CD. Une modification (nouvelle version d'un outil, changement de runner, ajout d'une étape de sécurité) doit être répercutée manuellement dans chaque dépôt. Un dépôt centralisé de workflows mutualisés résout ce problème : les dépôts consommateurs appellent les workflows du dépôt central, qui devient le seul point de maintenance.
Architecture
Le dépôt shared_workflows contient :
- des workflows réutilisables (
workflow_call) qui constituent les points d'entrée pour les dépôts consommateurs - des actions composites qui factorisent la logique commune entre ces workflows
- des appels à des workflows génériques (pre-commit, lint) partagés entre toutes les stacks
Les dépôts consommateurs ont un workflow minimal qui délègue tout au dépôt central.
Workflow réutilisable dans le dépôt central
Un workflow réutilisable se déclare avec on: workflow_call. Il expose des inputs et des secrets que les appelants doivent fournir :
# shared_workflows/.github/workflows/build.yml
name: Build and Test
on:
workflow_call:
inputs:
image-name:
description: "Docker image name"
required: true
type: string
python-version:
description: "Python version"
required: false
type: string
default: "3.12"
secrets:
registry-token:
required: true
description: "Token for container registry"
jobs:
pre-commit:
uses: org/generic_workflows/.github/workflows/pre-commit.yml@v1
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11", "3.12"]
container:
image: python:${{ matrix.python-version }}-slim
steps:
- uses: actions/checkout@v4
- run: pip install -e ".[dev]" && pytest
build:
needs: [pre-commit, test]
runs-on: ubuntu-latest # sans container : le démon Docker du runner est requis pour le build
steps:
- uses: actions/checkout@v4
- name: Build and push image
uses: org/shared_workflows/.github/actions/docker-build@v1
with:
image: ${{ inputs.image-name }}
tag: ${{ github.sha }}
registry-token: ${{ secrets.registry-token }}
Le build d'image est isolé dans un job distinct, sans container : un conteneur python:*-slim ne contient ni client Docker ni accès au démon, et une matrice produirait plusieurs images poussées sous le même tag. Les tests, eux, profitent de la matrice.
Dans un workflow réutilisable, une référence locale uses: ./.github/actions/docker-build serait résolue dans le dépôt appelant (celui que actions/checkout a récupéré), et non dans shared_workflows. L'action composite est donc référencée par son chemin complet org/shared_workflows/...@v1.
Workflow consommateur dans chaque dépôt
Le workflow de chaque dépôt consommateur devient minimal : il se contente d'appeler le workflow central :
# project_a/.github/workflows/ci.yml
name: CI
on:
push:
jobs:
build:
uses: org/shared_workflows/.github/workflows/build.yml@v1
permissions:
contents: read
packages: write
with:
image-name: ghcr.io/org/project-a
secrets:
registry-token: ${{ secrets.GITHUB_TOKEN }}
Les permissions du GITHUB_TOKEN sont fixées par l'appelant : le workflow appelé peut seulement les restreindre, jamais les étendre. Sans packages: write côté consommateur, le push vers GHCR échoue. secrets: inherit transmet l'ensemble des secrets de l'appelant, au prix d'un contrat moins explicite que la liste nominative.
La référence @v1 détermine la propagation des changements. Avec @main, toute modification de shared_workflows/build.yml s'applique immédiatement à tous les dépôts, y compris une régression. Avec un tag de version majeure (v1) déplacé à chaque release compatible, les consommateurs reçoivent les correctifs sans intervention, et une évolution incompatible passe par un nouveau tag (v2) adopté dépôt par dépôt.
Action composite dans le dépôt central
Les actions composites factorisent la logique commune entre les workflows du dépôt central lui-même :
# shared_workflows/.github/actions/docker-build/action.yml
name: "Docker Build and Push"
description: "Build a Docker image and push it to GHCR"
inputs:
image:
required: true
tag:
required: true
registry-token:
required: true
runs:
using: "composite"
steps:
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ inputs.registry-token }}
- uses: docker/build-push-action@v6
with:
push: true
tags: ${{ inputs.image }}:${{ inputs.tag }}
cache-from: type=gha
cache-to: type=gha,mode=max
Différence entre action et workflow réutilisable
Les deux mécanismes se complètent mais ont des périmètres différents :
| Action composite | Workflow réutilisable | |
|---|---|---|
| Appelé depuis | Une step (uses) | Un job (uses) |
| Environnement | Hérite du job appelant | Définit ses propres runners |
| Secrets | Via inputs | Via secrets dédié |
| Usage | Factoriser des steps | Encapsuler un pipeline complet |
Un workflow réutilisable définit on: workflow_call et ne peut pas être utilisé dans une step : c'est un job à part entière avec ses propres runners. Une action composite s'exécute dans l'environnement du job qui l'appelle et peut être insérée n'importe où dans une liste de steps. Le détail de la syntaxe des actions composites figure dans l'article action réutilisable.
Application / Projet lié
workflow_call) organisée par domaine technologique, appelée par plus de 70 dépôts pour generic_workflows et près de 90 pour zephyr_workflows, tous alignés sur la branche principale sans tag de version.GitHub ActionsDockerHelmPythonROSZephyr