GitHub Actions : automatiser vos pipelines CI/CD de A à Z
← Retour au blogGitHub Actions

GitHub Actions : automatiser vos pipelines CI/CD de A à Z

25 mars 202512 min de lectureGitHub ActionsCI/CDDevOps

GitHub Actions est devenu le standard pour la CI/CD. Ce guide couvre les patterns avancés : matrices de build, environnements réutilisables, secrets, déploiements multi-cloud et optimisation des coûts.

Concepts fondamentaux de GitHub Actions

GitHub Actions est un moteur d'automatisation intégré à GitHub. Il exécute des workflows définis en YAML, déclenchés par des événements du cycle de vie Git ou des planifications. Comprendre ses primitives est essentiel avant d'aller vers les patterns avancés.

  • Workflow : fichier YAML dans .github/workflows/, contient un ou plusieurs jobs. Déclenché par un événement (push, pull_request, schedule, workflow_dispatch, workflow_call)
  • Job : unité d'exécution qui tourne sur un runner. Les jobs sont parallèles par défaut ; utilisez needs pour les séquencer
  • Step : commande shell ou action réutilisable à l'intérieur d'un job. Les steps d'un même job partagent le filesystem
  • Runner : machine qui exécute le job — GitHub-hosted (ubuntu-latest, windows-latest, macos-latest) ou self-hosted
  • Action : step réutilisable empaqueté (uses: actions/checkout@v4) — 20 000+ disponibles sur GitHub Marketplace
  • Context : variables d'environnement dynamiques exposées par GitHub — github.sha, github.ref, github.event, etc.

Pipeline CI/CD complet Node.js : lint → test → build → ECR → ECS

Voici un pipeline de production complet pour une application Node.js déployée sur ECS Fargate. Il illustre les patterns essentiels : cache npm, tests parallèles, build Docker avec cache de couches, OIDC AWS et déploiement avec rollback automatique.

# .github/workflows/ci-cd.yml
name: CI/CD Production

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  AWS_REGION: eu-west-1
  ECR_REPOSITORY: mon-app
  ECS_CLUSTER: production
  ECS_SERVICE: mon-app-service
  CONTAINER_NAME: mon-app

permissions:
  id-token: write   # Requis pour OIDC
  contents: read
  pull-requests: write  # Pour commenter les PRs

jobs:
  # ─── Lint et vérifications statiques ───────────────────────
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci
      - run: npm run lint
      - run: npm run type-check

  # ─── Tests unitaires et d'intégration ──────────────────────
  test:
    runs-on: ubuntu-latest
    needs: lint

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: test
          POSTGRES_DB: testdb
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'

      - run: npm ci

      - name: Run tests with coverage
        run: npm run test:coverage
        env:
          DATABASE_URL: postgresql://postgres:test@localhost:5432/testdb

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

  # ─── Build et push Docker vers ECR ─────────────────────────
  build:
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'
    outputs:
      image: ${{ steps.build-push.outputs.image }}

    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build and push to ECR
        id: build-push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }}
            ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            NODE_ENV=production
            BUILD_SHA=${{ github.sha }}

  # ─── Déploiement ECS ────────────────────────────────────────
  deploy:
    runs-on: ubuntu-latest
    needs: build
    environment: production

    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials via OIDC
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v2

      - name: Update ECS service
        run: |
          aws ecs update-service             --cluster ${{ env.ECS_CLUSTER }}             --service ${{ env.ECS_SERVICE }}             --force-new-deployment

      - name: Wait for deployment to complete
        run: |
          aws ecs wait services-stable             --cluster ${{ env.ECS_CLUSTER }}             --services ${{ env.ECS_SERVICE }}
          echo "Deployment complete: ${{ github.sha }}"

Workflows réutilisables et composite actions : principe DRY

Les workflows réutilisables (workflow_call) permettent de centraliser la logique CI/CD partagée entre plusieurs dépôts. C'est l'équivalent des fonctions réutilisables en programmation — indispensable pour les organisations avec de nombreux projets.

# .github/workflows/reusable-deploy-ecs.yml
# Workflow réutilisable — peut être appelé depuis n'importe quel dépôt de l'org
name: Deploy to ECS (Reusable)

on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string
        description: "staging ou production"
      image-tag:
        required: true
        type: string
      ecs-cluster:
        required: true
        type: string
      ecs-service:
        required: true
        type: string
      aws-region:
        required: false
        type: string
        default: "eu-west-1"
    secrets:
      AWS_ROLE_ARN:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: ${{ inputs.aws-region }}

      - name: Deploy to ECS
        run: |
          aws ecs update-service             --cluster ${{ inputs.ecs-cluster }}             --service ${{ inputs.ecs-service }}             --force-new-deployment
          aws ecs wait services-stable             --cluster ${{ inputs.ecs-cluster }}             --services ${{ inputs.ecs-service }}

---
# Dans un autre dépôt : appel du workflow réutilisable
# .github/workflows/deploy.yml
jobs:
  deploy-staging:
    uses: mon-org/devops-workflows/.github/workflows/reusable-deploy-ecs.yml@main
    with:
      environment: staging
      image-tag: ${{ github.sha }}
      ecs-cluster: staging-cluster
      ecs-service: mon-app-staging
    secrets:
      AWS_ROLE_ARN: ${{ secrets.AWS_STAGING_ROLE_ARN }}

  deploy-production:
    needs: deploy-staging
    uses: mon-org/devops-workflows/.github/workflows/reusable-deploy-ecs.yml@main
    with:
      environment: production
      image-tag: ${{ github.sha }}
      ecs-cluster: prod-cluster
      ecs-service: mon-app-prod
    secrets:
      AWS_ROLE_ARN: ${{ secrets.AWS_PROD_ROLE_ARN }}

Matrices de build : tests parallèles multi-versions

Les matrices de build permettent de tester en parallèle sur plusieurs versions de Node.js, plusieurs systèmes d'exploitation, ou plusieurs configurations. Elles réduisent drastiquement le temps total de CI.

jobs:
  test-matrix:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        node: [18, 20, 22]
        os: [ubuntu-latest, windows-latest]
        include:
          # Ajouter une configuration spéciale pour Node 22 sur macOS
          - node: 22
            os: macos-latest
            experimental: true
        exclude:
          # Node 18 ne supporte pas certaines features Windows
          - node: 18
            os: windows-latest
      fail-fast: false  # Continuer même si une combinaison échoue

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
          cache: 'npm'

      - run: npm ci
      - run: npm test

      - name: Test résultats
        if: always()  # Toujours uploader, même en cas d'échec
        uses: actions/upload-artifact@v4
        with:
          name: test-results-node${{ matrix.node }}-${{ matrix.os }}
          path: test-results/

Stratégies de cache : optimiser le temps de CI

Le cache est le levier le plus impactant pour réduire la durée des pipelines. Un pipeline Node.js sans cache peut prendre 4 à 6 minutes ; avec un cache bien configuré, 45 secondes à 1 minute 30.

# Cache node_modules — économise 2-3 min par run
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

# Cache Docker layers avec GitHub Actions Cache Backend
# (plus efficace que Docker Hub pour les grands projets)
- uses: docker/setup-buildx-action@v3
  with:
    driver-opts: |
      image=moby/buildkit:latest

- uses: docker/build-push-action@v5
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max  # mode=max cache toutes les couches intermédiaires

# Cache Next.js build
- uses: actions/cache@v4
  with:
    path: |
      ~/.npm
      ${{ github.workspace }}/.next/cache
    key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.ts', '**/*.tsx') }}
    restore-keys: |
      ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
      ${{ runner.os }}-nextjs-

Environnements et protection rules : gouvernance des déploiements

Les environnements GitHub Actions permettent d'isoler les secrets par cible de déploiement et d'imposer des règles d'approbation avant tout déploiement en production. C'est la gate de gouvernance indispensable pour les équipes avec des contraintes de change management.

# Configuration dans GitHub UI : Settings → Environments → production
# Protection rules :
# [ ] Required reviewers : 2 membres de l'équipe SRE
# [ ] Wait timer : 5 minutes (fenêtre d'annulation)
# [ ] Deployment branches : main uniquement

jobs:
  deploy-staging:
    environment:
      name: staging
      url: https://staging.mon-app.com
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh staging

  deploy-production:
    needs: [deploy-staging, integration-tests]
    environment:
      name: production
      url: https://mon-app.com
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh production
      # Ce job attend l'approbation des reviewers configurés
      # avant d'exécuter les steps

OIDC avec AWS : zéro secret stocké dans GitHub

L'authentification OIDC élimine le besoin de stocker des credentials AWS (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY) dans les secrets GitHub. GitHub émet un token JWT signé que AWS valide directement via STS. C'est la méthode la plus sécurisée et celle que nous recommandons systématiquement.

# 1. Terraform : créer le provider OIDC et le rôle IAM
resource "aws_iam_openid_connect_provider" "github_actions" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}

resource "aws_iam_role" "github_actions_deploy" {
  name = "GitHubActionsDeployRole"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Federated = aws_iam_openid_connect_provider.github_actions.arn }
      Action    = "sts:AssumeRoleWithWebIdentity"
      Condition = {
        StringEquals = {
          "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
        }
        StringLike = {
          # Restriction au dépôt et à la branche main uniquement
          "token.actions.githubusercontent.com:sub" = "repo:mon-org/mon-repo:ref:refs/heads/main"
        }
      }
    }]
  })
}

# 2. Workflow : utilisation OIDC (aucun secret AWS stocké)
- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789:role/GitHubActionsDeployRole
    aws-region: eu-west-1
    role-session-name: GitHubActions-${{ github.run_id }}
    # Pas de aws-access-key-id ni aws-secret-access-key !

Self-hosted runners sur ECS Fargate : ephémères et rentables

Les runners GitHub-hosted coûtent 0,008 $/min. Un pipeline de 10 minutes coûte 0,08 $ — soit 80 $ pour 1 000 runs. Avec des runners self-hosted sur ECS Fargate Spot, le coût tombe à ~0,001 $/min. Pour les organisations avec un volume élevé, l'économie est significative. L'autre avantage : les runners tournent dans votre VPC et accèdent à vos ressources privées sans exposition publique.

# Architecture : Actions Runner Controller (ARC) sur EKS
# Les runners sont des pods Kubernetes éphémères — scale to zero hors charge

# runner-deployment.yaml
apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
  name: m2c-runners
  namespace: github-runners
spec:
  replicas: 0  # Scale à zéro quand aucun job en attente
  template:
    spec:
      repository: mon-org/mon-repo
      image: summerwind/actions-runner:latest
      resources:
        requests:
          cpu: "500m"
          memory: "1Gi"
        limits:
          cpu: "2"
          memory: "4Gi"
      serviceAccountName: github-runner-sa
      nodeSelector:
        node-role: runners

---
# HorizontalRunnerAutoscaler : scale en fonction de la file de jobs
apiVersion: actions.summerwind.dev/v1alpha1
kind: HorizontalRunnerAutoscaler
metadata:
  name: m2c-runners-autoscaler
spec:
  scaleTargetRef:
    name: m2c-runners
  minReplicas: 0
  maxReplicas: 10
  metrics:
    - type: TotalNumberOfQueuedAndInProgressWorkflowRuns
      repositoryNames:
        - mon-org/mon-repo

Gestion des secrets : GitHub Secrets vs AWS Secrets Manager

Les secrets GitHub sont adaptés pour les petites équipes et les secrets statiques. Pour des équipes plus grandes avec rotation des secrets, des secrets partagés entre environnements ou des secrets dynamiques, AWS Secrets Manager (accessible via OIDC) est préférable :

# Pattern recommandé : fetch des secrets AWS au runtime via OIDC
# Aucun secret sensible stocké dans GitHub

- name: Configure AWS credentials (OIDC)
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
    aws-region: eu-west-1

- name: Fetch secrets from AWS Secrets Manager
  run: |
    DB_PASSWORD=$(aws secretsmanager get-secret-value       --secret-id prod/app/db-password       --query SecretString --output text)
    API_KEY=$(aws secretsmanager get-secret-value       --secret-id prod/app/api-key       --query SecretString --output text)
    echo "DB_PASSWORD=$DB_PASSWORD" >> $GITHUB_ENV
    echo "::add-mask::$DB_PASSWORD"   # Masquer dans les logs
    echo "::add-mask::$API_KEY"

# Alternative : utiliser l'action officielle
- uses: aws-actions/aws-secretsmanager-get-secrets@v2
  with:
    secret-ids: |
      prod/app/db-password
      prod/app/api-key
    parse-json-secrets: true

Optimisation des workflows : path filters et concurrency groups

Pour les monorepos ou les projets avec plusieurs composants indépendants, déclencher tous les tests à chaque commit est inefficace. Les path filters et concurrency groups permettent d'optimiser radicalement le temps de CI.

# Path filters : ne builder que ce qui a changé
on:
  push:
    paths:
      - 'backend/**'   # Déclencher seulement si le backend a changé
      - '!docs/**'     # Ignorer les changements de doc
      - '!*.md'

# Concurrency groups : annuler les runs précédents sur la même branche
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true  # Annule le run précédent si nouvelle PR pushée

# Pour la branche main, ne pas annuler (laisser les déploiements se terminer)
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

# Conditions avancées : différencier PR vs push main
jobs:
  deploy:
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    # Ce job ne tourne que sur push main, pas sur les PRs

Monitoring et coûts : GitHub Actions dashboard et Datadog CI

GitHub Actions fournit un dashboard natif (Actions tab → tous les workflows, durées, taux de succès). Pour une observabilité avancée des pipelines CI, Datadog CI propose des métriques détaillées : durée par step, flaky tests, coût par pipeline.

# Intégration Datadog CI dans GitHub Actions
- name: Install Datadog CI
  run: npm install -g @datadog/datadog-ci

- name: Run tests with Datadog tracing
  run: npm test
  env:
    DD_CIVISIBILITY_AGENTLESS_ENABLED: true
    DD_API_KEY: ${{ secrets.DD_API_KEY }}
    DD_SITE: datadoghq.eu

# Coûts GitHub-hosted runners (2025) :
# ubuntu-latest (2 vCPU) : $0.008/min
# ubuntu-latest-4-cores   : $0.016/min
# ubuntu-latest-8-cores   : $0.032/min
# windows-latest           : $0.016/min
# macos-latest             : $0.08/min

# Pour 1 000 runs de 10 min sur ubuntu :
# GitHub-hosted : 1000 × 10 × $0.008 = $80/mois
# Self-hosted Fargate Spot : ~$8/mois (économie 90%)

Sécurisation des workflows

Les workflows GitHub Actions peuvent être vecteurs d'attaque si mal configurés. Les bonnes pratiques essentielles :

  • Épingler les actions par commit SHA : uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 plutôt que @v4 — empêche les supply chain attacks via tag hijacking
  • Permissions minimales : déclarer permissions: read-all au niveau workflow et accorder uniquement ce dont chaque job a besoin
  • PRs depuis des forks : les workflows déclenchés par pull_request depuis un fork n'ont pas accès aux secrets — c'est une protection intentionnelle à ne pas contourner
  • CODEOWNERS sur les workflows : ajouter .github/workflows/ @team-devops dans CODEOWNERS pour que toute modification de workflow passe en revue

Conclusion

GitHub Actions couvre le spectre complet de la CI/CD, de la simple automatisation de tests à des déploiements multi-environnements complexes avec gouvernance. Les workflows réutilisables éliminent la duplication entre projets, les matrices de build parallélisent les tests sur toutes les configurations cibles, et l'intégration OIDC avec AWS supprime la dernière raison de stocker des credentials cloud dans GitHub. Pour les organisations avec un volume élevé de builds, les runners self-hosted sur ECS Fargate Spot réduisent les coûts de CI de 80 à 90 %. GitHub Actions est aujourd'hui suffisamment mature pour remplacer Jenkins dans la quasi-totalité des cas d'usage — avec une courbe d'apprentissage infiniment plus courte et une intégration Git native que Jenkins ne peut pas égaler.

← Retour au blog