Externaliser son CI/CD GitHub Actions pour des dizaines (ou centaines) de repos
Reusable workflows, composite actions, repository_dispatch, externalisation du Dockerfile et docker-compose : toutes les options pour maintenir un pipeline CI/CD unique partagé entre de nombreux repos.
Le problème : la duplication de pipelines
Quand on gère de nombreux repos qui partagent le même stack technique (même framework, même Dockerfile, même pipeline CI/CD), on se retrouve vite avec des fichiers identiques copiés partout. Le workflow GitHub Actions, le Dockerfile, le docker-compose — tout est dupliqué.
Tant qu’il y a 3 repos, c’est gérable. À 20 ou 200, toute modification du pipeline devient un cauchemar de maintenance. Il faut un moyen de définir le pipeline une seule fois dans un repo central et de le réutiliser depuis chaque repo applicatif.
Cet article explore les différentes solutions offertes par GitHub Actions et Docker pour résoudre ce problème.
1. Reusable Workflows (workflow_call)
C’est la solution principale pour externaliser un workflow GitHub Actions complet.
Principe
Un workflow défini dans un repo central peut être appelé depuis n’importe quel autre repo de l’organisation, comme une fonction. Le repo appelant déclenche le workflow, mais c’est le code du workflow central qui s’exécute.
Côté repo central
Le workflow central se déclare avec on: workflow_call et peut accepter des inputs et des secrets :
# org/infra/.github/workflows/build-deploy.yml
name: Build & Deploy
on:
workflow_call:
inputs:
node_version:
required: false
type: string
default: "22"
secrets:
DEPLOY_SSH_KEY:
required: true
DEPLOY_HOST:
required: true
permissions:
contents: read
packages: write
env:
REGISTRY: ghcr.io
jobs:
build:
name: Build & push image
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.ref == 'refs/heads/main' }}
tags: ${{ env.REGISTRY }}/${{ github.repository }}:latest
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
name: Deploy
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
steps:
- uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
docker compose pull && docker compose up -d --force-recreate
Côté repo applicatif
Le workflow du repo applicatif se réduit à quelques lignes — un simple appel au workflow central :
# .github/workflows/ci.yml
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-deploy:
uses: org/infra/.github/workflows/build-deploy.yml@main
secrets: inherit
Points clés
| Aspect | Détail |
|---|---|
| Déclencheur | Le push sur le repo applicatif déclenche le workflow — c’est le repo applicatif qui appelle le workflow central |
| Contexte | actions/checkout récupère le code du repo appelant (l’applicatif), pas du repo central |
GITHUB_TOKEN | Automatiquement fourni, fonctionne pour push vers ghcr.io du repo appelant |
| Secrets | Doivent être passés explicitement, ou via secrets: inherit si configurés au niveau de l’organisation |
| Visibilité | Le repo central doit être public, internal (GitHub Enterprise), ou le workflow doit être explicitement partagé |
| Limites | Max 4 niveaux d’imbrication, max 20 workflows uniques par fichier (source) |
Simplification des secrets avec secrets: inherit
Si les secrets sont définis au niveau de l’organisation GitHub, un simple secrets: inherit suffit — plus besoin de les lister un par un :
jobs:
build-deploy:
uses: org/infra/.github/workflows/build-deploy.yml@main
secrets: inherit # Passe automatiquement tous les secrets de l'org
C’est particulièrement utile quand on gère des dizaines de repos : on définit les secrets une fois au niveau org, et tous les repos y accèdent sans configuration supplémentaire.
Sources :
2. Déclenchement cross-repo (repository_dispatch)
Alternative si on veut que le repo central exécute le build (au lieu du repo applicatif).
Principe
Un push sur le repo applicatif envoie un événement HTTP au repo central via l’API GitHub. Le repo central reçoit l’événement et déclenche son workflow avec le contexte du repo source.
Côté repo applicatif
Un workflow minimaliste envoie un repository_dispatch au repo central :
# .github/workflows/notify-infra.yml
name: Notify infra repo
on:
push:
branches: [main]
jobs:
dispatch:
runs-on: ubuntu-latest
steps:
- name: Trigger build on infra repo
run: |
curl -X POST \
-H "Authorization: token ${{ secrets.INFRA_DISPATCH_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
https://api.github.com/repos/org/infra/dispatches \
-d '{
"event_type": "site-updated",
"client_payload": {
"repo": "${{ github.repository }}",
"ref": "${{ github.sha }}"
}
}'
Côté repo central
Le workflow écoute l’événement repository_dispatch et checkout le repo source :
# .github/workflows/build-site.yml
name: Build site from dispatch
on:
repository_dispatch:
types: [site-updated]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout source repo
uses: actions/checkout@v4
with:
repository: ${{ github.event.client_payload.repo }}
ref: ${{ github.event.client_payload.ref }}
# ... suite du build et déploiement
Points clés
| Aspect | Détail |
|---|---|
| Token | Nécessite un PAT (Personal Access Token) avec scope repo, stocké comme secret dans chaque repo applicatif |
| Payload | Max 10 propriétés de premier niveau, 65 535 caractères |
| Contexte checkout | Il faut checkout explicitement le repo applicatif (pas le repo central) |
| Avantage | Toute la logique CI/CD est centralisée dans un seul repo |
| Inconvénient | Plus complexe, nécessite un PAT, et les status checks apparaissent sur le repo central (pas sur le repo applicatif) |
Source : Events that trigger workflows —
repository_dispatch
3. Composite Actions
Pour packager des steps réutilisables — plus granulaire que les reusable workflows.
Principe
Une composite action regroupe plusieurs steps dans une action réutilisable. Contrairement au reusable workflow qui remplace un workflow entier (multi-jobs), la composite action remplace des steps au sein d’un job existant.
Exemple : une action de build Docker réutilisable
# org/infra/actions/docker-build/action.yml
name: "Build Docker Image"
description: "Builds and pushes a Docker image with BuildKit cache"
inputs:
image_name:
description: "Full image name (registry/org/repo)"
required: true
push:
description: "Whether to push the image"
required: false
default: "false"
runs:
using: "composite"
steps:
- uses: docker/setup-buildx-action@v3
shell: bash
- uses: docker/metadata-action@v5
id: meta
with:
images: ${{ inputs.image_name }}
tags: |
type=sha,prefix=
type=raw,value=latest,enable={{is_default_branch}}
shell: bash
- uses: docker/build-push-action@v6
with:
context: .
push: ${{ inputs.push }}
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
shell: bash
Utilisation dans un workflow
steps:
- uses: org/infra/actions/docker-build@main
with:
image_name: ghcr.io/org/my-app
push: "true"
Composite Actions vs Reusable Workflows
| Composite Action | Reusable Workflow | |
|---|---|---|
| Granularité | Steps (dans un job) | Workflow entier (multi-jobs) |
| Secrets | Non accessibles directement (passés via inputs) | Accessibles via secrets: |
| Runners | Même runner que le job appelant | Peut définir ses propres runners |
| Cas d’usage | Réutiliser un bloc de steps | Réutiliser un pipeline complet |
En résumé : pour un pipeline complet multi-jobs → reusable workflow. Pour des blocs de steps réutilisables → composite action. Les deux approches sont complémentaires.
Source : Creating a composite action — documentation officielle
4. Externalisation du Dockerfile et docker-compose
Le workflow GitHub Actions se centralise bien. Mais qu’en est-il du Dockerfile et du docker-compose ?
Option A : Dockerfile via une URL Git distante
Docker supporte nativement le build depuis une URL Git :
docker build https://github.com/org/infra.git#main:docker/my-app
Le problème : cela clone le repo distant comme contexte de build. On perd l’accès aux fichiers locaux du projet (package.json, src/, etc.). Cette option ne convient donc pas quand le Dockerfile doit copier des fichiers du projet.
Source : Docker build context
Option B : Image de base partagée
On peut créer une image de base dans le repo central, publiée sur un registre, et l’utiliser comme FROM dans chaque repo :
# Repo central : image de base avec les dépendances communes
FROM node:22-slim AS base
WORKDIR /app
# Setup commun (tini, user non-root, etc.)
# Repo applicatif : étend l'image de base
FROM ghcr.io/org/base-image:latest AS builder
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
C’est utile quand le Dockerfile est complexe et contient beaucoup de logique commune. Pour un Dockerfile simple (< 30 lignes), le gain est marginal.
Source : Docker base images
Option C : Garder le Dockerfile dans chaque repo
Si le Dockerfile est court, générique, et ne change jamais d’un repo à l’autre — le garder dupliqué est parfaitement acceptable. Docker a besoin du contexte local pour le build (COPY, ADD), et un Dockerfile de 20-30 lignes ne pose aucun problème de maintenance.
C’est souvent la meilleure option pour des projets simples.
Option D : Docker Compose include / extends
Docker Compose permet l’inclusion et l’héritage de configurations :
# docker-compose.yml du repo applicatif
include:
- path: ../infra/docker-compose.base.yml # via submodule ou copie
services:
app:
extends:
file: common-services.yml
service: webapp
environment:
- APP_SPECIFIC=value
Comme pour le Dockerfile, si le docker-compose.yml fait une dizaine de lignes et est entièrement piloté par des variables d’environnement, le centraliser ajoute de la complexité sans gain réel.
Sources :
5. Quelle stratégie adopter ?
Matrice de décision
| Fichier | Centraliser ? | Méthode | Justification |
|---|---|---|---|
| Workflow GHA | Oui | Reusable workflow (workflow_call) | C’est le fichier le plus long et le plus susceptible d’évoluer |
| Dockerfile (< 30 lignes) | Non | Garder dans chaque repo | Docker a besoin du contexte local ; le fichier est stable et court |
| docker-compose.yml (< 15 lignes) | Non | Garder dans chaque repo | Trop simple pour justifier l’abstraction |
| Dockerfile (complexe) | Oui | Image de base partagée | Si > 50 lignes avec logique commune significative |
Architecture cible typique
org/infra (repo central)
└── .github/workflows/
└── build-deploy.yml ← Reusable workflow (workflow_call)
org/my-app (× N repos)
├── .github/workflows/
│ └── ci.yml ← ~15 lignes : appelle le workflow central
├── app.config.json ← Configuration spécifique au projet
├── Dockerfile ← Identique partout, court et stable
├── docker-compose.yml ← Identique partout, piloté par env vars
├── package.json
└── src/
Avantages de cette approche
- Un seul endroit pour modifier le pipeline CI/CD → propagation immédiate à tous les repos
- Aucun token supplémentaire nécessaire (contrairement à
repository_dispatch) - Les status checks apparaissent sur le repo applicatif (visible dans les PRs et commits)
- Le Dockerfile et docker-compose restent dans chaque repo car ils sont petits, stables, et nécessaires au build local
Prérequis
- Créer le repo central — le rendre public, internal (GitHub Enterprise), ou configurer l’accès explicitement s’il est privé
- Définir les secrets au niveau de l’organisation GitHub pour profiter de
secrets: inherit - Migrer les workflows dans chaque repo vers un appel au workflow central
Accès depuis un repo central privé
Si le repo central doit rester privé, aller dans :
Settings > Actions > General > Access et cocher “Accessible from repositories in the organization”.
Source : Access to reusable workflows
Sources
GitHub Actions
- Reusing workflows — Documentation complète sur
workflow_call - Events that trigger workflows —
repository_dispatch— Déclenchement cross-repo via API - Creating a composite action — Actions composites réutilisables
- How to start using reusable workflows — GitHub Blog — Blog GitHub avec exemples
Docker
- Docker build context — Contextes de build distants (Git URL)
- Base images — Images de base partagées
- Multi-stage builds — Builds multi-étapes
- Multiple Compose files — Include et merge de fichiers Compose
- Compose extends — Héritage de services Compose