fr

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

AspectDétail
DéclencheurLe push sur le repo applicatif déclenche le workflow — c’est le repo applicatif qui appelle le workflow central
Contexteactions/checkout récupère le code du repo appelant (l’applicatif), pas du repo central
GITHUB_TOKENAutomatiquement fourni, fonctionne pour push vers ghcr.io du repo appelant
SecretsDoivent ê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é
LimitesMax 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

AspectDétail
TokenNécessite un PAT (Personal Access Token) avec scope repo, stocké comme secret dans chaque repo applicatif
PayloadMax 10 propriétés de premier niveau, 65 535 caractères
Contexte checkoutIl faut checkout explicitement le repo applicatif (pas le repo central)
AvantageToute la logique CI/CD est centralisée dans un seul repo
InconvénientPlus 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 ActionReusable Workflow
GranularitéSteps (dans un job)Workflow entier (multi-jobs)
SecretsNon accessibles directement (passés via inputs)Accessibles via secrets:
RunnersMême runner que le job appelantPeut définir ses propres runners
Cas d’usageRéutiliser un bloc de stepsRé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

FichierCentraliser ?MéthodeJustification
Workflow GHAOuiReusable workflow (workflow_call)C’est le fichier le plus long et le plus susceptible d’évoluer
Dockerfile (< 30 lignes)NonGarder dans chaque repoDocker a besoin du contexte local ; le fichier est stable et court
docker-compose.yml (< 15 lignes)NonGarder dans chaque repoTrop simple pour justifier l’abstraction
Dockerfile (complexe)OuiImage de base partagéeSi > 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

  1. Créer le repo central — le rendre public, internal (GitHub Enterprise), ou configurer l’accès explicitement s’il est privé
  2. Définir les secrets au niveau de l’organisation GitHub pour profiter de secrets: inherit
  3. 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

Docker