Aller au contenu principal

GitHub Actions : architecture CI/CD réutilisable

· 5 minutes de lecture

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 compositeWorkflow réutilisable
Appelé depuisUne step (uses)Un job (uses)
EnvironnementHérite du job appelantDéfinit ses propres runners
SecretsVia inputsVia secrets dédié
UsageFactoriser des stepsEncapsuler 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é​

Mis en pratique dans le projet2024 → aujourd'huiCI/CD - Workflows GitHub Actions mutualisésBibliothèque de workflows réutilisables (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