03 — GitHub Actions en profondeur#

GitHub Actions est la plateforme CI/CD native de GitHub, disponible depuis 2019. Elle s’intègre directement dans le dépôt, sans infrastructure à gérer par défaut, et bénéficie d’un écosystème d’actions réutilisables (GitHub Marketplace) qui en fait l’une des plateformes les plus adoptées dans l’industrie.

Anatomie d’un workflow#

Un workflow GitHub Actions est un fichier YAML stocké dans .github/workflows/. Il est composé de plusieurs éléments hiérarchiques.

# Anatomie complète d'un workflow GitHub Actions (non exécutable)
name: CI Pipeline                  # Nom affiché dans l'interface GitHub

on:                                # Déclencheurs (triggers)
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 2 * * 1'           # Chaque lundi à 2h UTC
  workflow_dispatch:               # Déclenchement manuel

env:                               # Variables d'environnement globales
  PYTHON_VERSION: '3.12'
  REGISTRY: ghcr.io

jobs:
  build:                           # Identifiant du job
    name: Build and Test           # Nom affiché
    runs-on: ubuntu-latest         # Runner (OS)
    timeout-minutes: 15            # Timeout de sécurité

    steps:
      - name: Checkout du code
        uses: actions/checkout@v4  # Action du marketplace

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}

      - name: Installer les dépendances
        run: pip install -r requirements.txt

      - name: Lancer les tests
        run: pytest --tb=short

Runners#

Les runners sont les machines qui exécutent les jobs. GitHub propose des runners hébergés (ubuntu-latest, windows-latest, macos-latest) et permet d’utiliser des runners auto-hébergés (self-hosted).

Les runners ubuntu-latest correspondent à Ubuntu 22.04 LTS (ou 24.04 selon la période). Ils incluent des dizaines d’outils préinstallés (Docker, Node.js, Python, Java, etc.).

Triggers#

Les triggers les plus courants :

  • push : sur chaque push, filtrable par branche et chemin.

  • pull_request : sur les événements PR (opened, synchronize, closed).

  • schedule : cron expression UTC.

  • workflow_dispatch : déclenchement manuel avec paramètres optionnels.

  • workflow_call : appel depuis un autre workflow (workflows réutilisables).

  • release : sur création/publication d’une release GitHub.

Contextes et expressions#

GitHub Actions expose des contextes (objets JSON) accessibles via la syntaxe ${{ expression }}.

# Exemples de contextes et expressions (non exécutable)
steps:
  - name: Afficher les informations de contexte
    run: |
      echo "SHA: ${{ github.sha }}"
      echo "Branche: ${{ github.ref_name }}"
      echo "Acteur: ${{ github.actor }}"
      echo "Event: ${{ github.event_name }}"
      echo "Repo: ${{ github.repository }}"

  - name: Utiliser un secret
    run: docker login -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} ghcr.io

  - name: Step conditionnel
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    run: echo "Déploiement en production"

Contextes disponibles#

  • github : informations sur l’événement déclencheur, le dépôt, l’acteur.

  • env : variables d’environnement définies dans le workflow.

  • secrets : secrets chiffrés définis dans les paramètres du dépôt ou de l’organisation.

  • steps : sorties (outputs) des steps précédents du même job.

  • jobs : sorties des jobs dans un workflow réutilisable.

  • runner : informations sur le runner en cours d’exécution.

  • matrix : valeurs de la matrice de build courante.

Conditions if:#

Les conditions permettent d’exécuter ou de passer un step/job selon des critères :

# Conditions avancées (non exécutable)
- name: Deploy en staging
  if: github.ref == 'refs/heads/develop' && github.event_name == 'push'
  run: ./deploy.sh staging

- name: Deploy en production
  if: startsWith(github.ref, 'refs/tags/v')
  run: ./deploy.sh production

- name: Toujours notifier, même en échec
  if: always()
  run: ./notify.sh ${{ job.status }}

Matrices de build#

La directive strategy.matrix permet de définir plusieurs configurations de job qui s’exécutent en parallèle.

# Matrice de build avec include/exclude (non exécutable)
jobs:
  test:
    strategy:
      matrix:
        python-version: ['3.10', '3.11', '3.12']
        os: [ubuntu-latest, windows-latest, macos-latest]
        exclude:
          # macOS onéreux — limiter aux versions clés
          - os: macos-latest
            python-version: '3.10'
        include:
          # Cas spécial : Python 3.12 sur macOS M1
          - os: macos-latest
            python-version: '3.12'
            experimental: true
      fail-fast: false          # Ne pas annuler les autres si un échoue
      max-parallel: 6           # Limiter le nombre de jobs simultanés
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}

Actions marketplace et actions locales#

Actions du marketplace#

Les actions sont des composants réutilisables référencés par uses: owner/repo@version. Il est impératif de pinner les versions à un SHA de commit ou à une version majeure précise pour éviter les compromissions de la supply chain.

# Bonne pratique : pinner les actions (non exécutable)
- uses: actions/checkout@v4                    # Tag de version
- uses: actions/checkout@11bd71901bbe5b1630ceea7369b5d248a4e24e9  # SHA (plus sûr)

Actions locales#

Une action locale est définie dans le dépôt lui-même, référencée par son chemin :

# Référencer une action locale (non exécutable)
- uses: ./.github/actions/setup-environment
  with:
    environment: staging

Une action locale est un répertoire contenant un fichier action.yml définissant les inputs, outputs et le comportement (JavaScript, Docker, ou composite).

Artefacts et cache#

Upload/Download d’artefacts#

# Partage d'artefacts entre jobs (non exécutable)
jobs:
  build:
    steps:
      - run: python setup.py bdist_wheel
      - uses: actions/upload-artifact@v4
        with:
          name: distribution
          path: dist/*.whl
          retention-days: 7

  deploy:
    needs: build
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: distribution
          path: dist/
      - run: twine upload dist/*.whl

Cache des dépendances#

# Cache avancé avec restore-keys (non exécutable)
- uses: actions/cache@v4
  id: cache-pip
  with:
    path: ~/.cache/pip
    key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
    restore-keys: |
      ${{ runner.os }}-pip-

- name: Installer uniquement si cache miss
  if: steps.cache-pip.outputs.cache-hit != 'true'
  run: pip install -r requirements.txt

Workflows réutilisables#

Les workflows réutilisables (workflow_call) permettent de centraliser un pipeline et de le partager entre plusieurs dépôts ou entre plusieurs workflows du même dépôt.

# Workflow réutilisable — .github/workflows/reusable-ci.yml (non exécutable)
on:
  workflow_call:
    inputs:
      python-version:
        type: string
        default: '3.12'
        description: Version de Python à utiliser
      environment:
        type: string
        required: true
    secrets:
      DEPLOY_TOKEN:
        required: true
    outputs:
      image-tag:
        description: Tag de l'image Docker construite
        value: ${{ jobs.build.outputs.image-tag }}

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ inputs.python-version }}
      - run: pytest
      - name: Build Docker image
        id: meta
        run: echo "tags=ghcr.io/org/app:${{ github.sha }}" >> $GITHUB_OUTPUT
# Appel du workflow réutilisable (non exécutable)
jobs:
  ci:
    uses: ./.github/workflows/reusable-ci.yml
    with:
      python-version: '3.12'
      environment: staging
    secrets:
      DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

Environnements, protection rules et approbation manuelle#

Les environnements GitHub permettent de modéliser les cibles de déploiement (staging, production) avec des règles de protection.

# Déploiement avec environnement protégé (non exécutable)
jobs:
  deploy-production:
    environment:
      name: production
      url: https://app.example.com
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        run: ./deploy.sh
        env:
          DEPLOY_KEY: ${{ secrets.PRODUCTION_DEPLOY_KEY }}

Règles de protection disponibles :

  • Approbateurs requis : un ou plusieurs reviewers doivent approuver avant que le job ne démarre.

  • Branches autorisées : seules certaines branches peuvent déployer dans cet environnement.

  • Délai d’attente (wait timer) : pause configurable avant le déploiement.

  • Règles de bypass pour les administrateurs.

OIDC et permissions minimales#

OIDC (OpenID Connect)#

OIDC permet à GitHub Actions d’obtenir des credentials AWS, GCP, Azure ou Vault de manière éphémère, sans stocker de credentials longue durée dans les secrets GitHub.

# Authentification AWS via OIDC (non exécutable)
permissions:
  id-token: write   # Nécessaire pour OIDC
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789:role/github-actions-role
      aws-region: eu-west-1

Permissions minimales#

Par défaut, le GITHUB_TOKEN a des permissions larges. Il faut les restreindre au minimum nécessaire :

# Permissions au niveau workflow + override par job (non exécutable)
permissions:
  contents: read       # Lecture du code source seulement

jobs:
  release:
    permissions:
      contents: write  # Nécessaire pour créer une release
      packages: write  # Nécessaire pour pousser vers GHCR

Self-hosted runners#

Les runners auto-hébergés sont nécessaires pour :

  • Accéder à des ressources réseau privées (bases de données internes, registres privés).

  • Utiliser du matériel spécifique (GPU, ARM, grandes RAM).

  • Réduire les coûts sur de gros volumes de CI.

  • Respecter des contraintes de conformité (données ne quittant pas le SI).

Considérations de sécurité :

  • Ne jamais utiliser des self-hosted runners sur des dépôts publics — un acteur malveillant pourrait soumettre une PR avec du code malicieux.

  • Utiliser des runners éphémères (JIT runners) : chaque job démarre sur une VM fraîche.

  • Isoler les runners par environnement (staging runner ≠ production runner).

  • Auditer les logs d’exécution régulièrement.

Concurrency — annuler les runs redondants#

La directive concurrency évite d’avoir plusieurs runs simultanés pour le même contexte (même branche, même PR).

# Annulation des runs redondants (non exécutable)
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true  # Annuler le run précédent si un nouveau démarre

Pour les déploiements en production, cancel-in-progress: false est préférable pour éviter d’interrompre un déploiement en cours.

Visualisations#

import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
import networkx as nx

sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

# DAG de dépendances entre jobs d'un pipeline CI complexe
G = nx.DiGraph()

jobs = {
    'checkout':       (0, 4),
    'lint':           (1, 6),
    'unit-tests':     (1, 5),
    'format-check':   (1, 4),
    'build':          (1, 3),
    'sast':           (1, 2),
    'integration':    (2, 5.5),
    'docker-build':   (2, 3),
    'e2e':            (3, 5),
    'scan-image':     (3, 3),
    'deploy-staging': (4, 4),
    'smoke-tests':    (5, 4),
    'deploy-prod':    (6, 4),
}

edges = [
    ('checkout', 'lint'),
    ('checkout', 'unit-tests'),
    ('checkout', 'format-check'),
    ('checkout', 'build'),
    ('checkout', 'sast'),
    ('unit-tests', 'integration'),
    ('build', 'integration'),
    ('build', 'docker-build'),
    ('integration', 'e2e'),
    ('docker-build', 'scan-image'),
    ('e2e', 'deploy-staging'),
    ('scan-image', 'deploy-staging'),
    ('lint', 'deploy-staging'),
    ('format-check', 'deploy-staging'),
    ('sast', 'deploy-staging'),
    ('deploy-staging', 'smoke-tests'),
    ('smoke-tests', 'deploy-prod'),
]

G.add_nodes_from(jobs.keys())
G.add_edges_from(edges)

pos = {job: coords for job, coords in jobs.items()}

node_colors = {
    'checkout':       '#90CAF9',
    'lint':           '#A5D6A7',
    'unit-tests':     '#A5D6A7',
    'format-check':   '#A5D6A7',
    'build':          '#A5D6A7',
    'sast':           '#FFCC80',
    'integration':    '#FFB74D',
    'docker-build':   '#CE93D8',
    'e2e':            '#F48FB1',
    'scan-image':     '#FFCC80',
    'deploy-staging': '#80DEEA',
    'smoke-tests':    '#80CBC4',
    'deploy-prod':    '#4DB6AC',
}

colors_list = [node_colors[n] for n in G.nodes()]

fig, ax = plt.subplots(figsize=(13, 8))
nx.draw_networkx_nodes(G, pos, node_color=colors_list, node_size=2200, ax=ax, alpha=0.9)
nx.draw_networkx_labels(G, pos, font_size=8, font_weight='bold', ax=ax)
nx.draw_networkx_edges(G, pos, ax=ax, arrows=True,
                       arrowstyle='->', arrowsize=20,
                       edge_color='#546E7A', width=1.8,
                       connectionstyle='arc3,rad=0.05')

legend_elements = [
    mpatches.Patch(color='#90CAF9', label='Source'),
    mpatches.Patch(color='#A5D6A7', label='Quality checks'),
    mpatches.Patch(color='#FFB74D', label='Integration tests'),
    mpatches.Patch(color='#CE93D8', label='Build artifact'),
    mpatches.Patch(color='#F48FB1', label='E2E tests'),
    mpatches.Patch(color='#FFCC80', label='Security scan'),
    mpatches.Patch(color='#80DEEA', label='Deploy staging'),
    mpatches.Patch(color='#4DB6AC', label='Deploy prod'),
]
ax.legend(handles=legend_elements, loc='lower right', fontsize=9, frameon=True)
ax.set_title('DAG de dépendances — pipeline CI/CD GitHub Actions', fontsize=13, fontweight='bold', pad=15)
ax.axis('off')
plt.show()
_images/c0e122010aa422bcdc03326255df349c17b6de6c56a8a242fd7663964b5da23b.png
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)

# Simulation de durée selon hit rate du cache
hit_rates = np.linspace(0, 1, 11)  # 0% à 100%

# Sans cache : toujours 18 min
# Avec cache : installation = 3 min si miss, 0.1 min si hit
install_time_no_cache = np.full_like(hit_rates, 3.0)

# Durée d'installation selon le hit rate
install_time_with_cache = 0.1 + (1 - hit_rates) * (3.0 - 0.1)

# Pipeline total = base (12 min) + installation
base_time = 12.0
total_no_cache    = base_time + 3.0              # constant
total_with_cache  = base_time + install_time_with_cache

fig, ax = plt.subplots(figsize=(9, 5))
ax.plot(hit_rates * 100, total_no_cache * np.ones_like(hit_rates),
        label='Sans cache', color='#E53935', linewidth=2.5, linestyle='--')
ax.plot(hit_rates * 100, total_with_cache,
        label='Avec cache (actions/cache)', color='#1E88E5', linewidth=2.5)
ax.fill_between(hit_rates * 100, total_with_cache, total_no_cache,
                alpha=0.15, color='#1E88E5', label='Gain de temps')

ax.set_xlabel('Taux de hit du cache (%)', fontsize=11)
ax.set_ylabel('Durée totale du pipeline (minutes)', fontsize=11)
ax.set_title('Impact du taux de hit du cache sur la durée du pipeline', fontsize=13, fontweight='bold')
ax.legend(fontsize=11)
ax.set_xlim(0, 100)
ax.set_ylim(10, 17)
ax.axhline(y=10, color='grey', linestyle=':', linewidth=1.2)

# Annoter le point d'optimum
ax.annotate(f'Hit rate 80% → {base_time + 0.1 + 0.2*(3.0-0.1):.1f} min',
            xy=(80, base_time + 0.1 + 0.2*(3.0-0.1)),
            xytext=(55, 12.5),
            arrowprops=dict(arrowstyle='->', color='#1565C0'),
            fontsize=10, color='#1565C0')
plt.show()
_images/38082755bd5f00337869fd7b8f853cddde6979ee6125a9f5faa30dcc60be9709.png

Résumé#

  1. Un workflow GitHub Actions est un fichier YAML dans .github/workflows/ composé de triggers, de jobs et de steps — chaque job s’exécute sur un runner indépendant.

  2. Les contextes (github, secrets, env, steps, matrix) sont accessibles via la syntaxe ${{ expression }} et permettent d’adapter le comportement dynamiquement.

  3. La directive strategy.matrix avec include/exclude permet de tester toutes les combinaisons runtime × OS en parallèle avec un seul fichier YAML.

  4. Il est impératif de pinner les actions du marketplace à une version ou un SHA précis pour se protéger contre les attaques de supply chain.

  5. Les artefacts (upload-artifact/download-artifact) transmettent des fichiers entre jobs ; le cache (actions/cache) évite de recréer des dépendances coûteuses entre runs.

  6. Les workflows réutilisables (workflow_call) permettent de centraliser et de partager des pipelines entre dépôts, avec des inputs, outputs et secrets formalisés.

  7. Les environnements GitHub permettent de modéliser staging et production avec des règles de protection (approbateurs requis, branches autorisées).

  8. OIDC élimine le besoin de stocker des credentials longue durée dans les secrets GitHub : les tokens sont obtenus dynamiquement et ont une durée de vie courte.

  9. La directive concurrency évite les déploiements simultanés sur la même branche en annulant les runs redondants.

  10. Les self-hosted runners ne doivent jamais être utilisés sur des dépôts publics ; préférer des runners éphémères (JIT) pour limiter la surface d’attaque.