Créez une pipeline CI/CD complète avec GitHub Actions : lint, tests automatisés, build Docker et déploiement automatique sur push pour livrer plus vite.
Concepts fondamentaux — workflow, job, step, runner
GitHub Actions est un système CI/CD natif de GitHub. Un workflow est un fichier YAML dans .github/workflows/ qui décrit quand et comment automatiser des tâches.
| Concept | Description | Analogie |
|---|---|---|
| Workflow | Fichier YAML complet — déclenché par un événement | Script shell complet |
| Event | Ce qui déclenche le workflow (push, PR, schedule...) | Trigger / condition |
| Job | Ensemble de steps sur un même runner | Tâche parallélisable |
| Step | Commande shell ou action réutilisable | Instruction individuelle |
| Runner | Machine virtuelle qui exécute le job (ubuntu-latest, windows-latest, macos-latest) | Serveur d'exécution |
| Action | Plugin réutilisable (ex: actions/checkout) | Bibliothèque de step |
# Structure d'un workflow GitHub Actions
# Fichier : .github/workflows/ci.yml
name: CI Pipeline # Nom affiché dans l'onglet Actions
on: # Événements déclencheurs
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs: # Un ou plusieurs jobs (exécution parallèle par défaut)
build:
runs-on: ubuntu-latest # Runner : VM Ubuntu gratuite GitHub
steps: # Séquence d'étapes
- name: Checkout le code
uses: actions/checkout@v4 # Action officielle GitHub
- name: Installer Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Installer les dépendances
run: npm ci # ci = install propre depuis package-lock.json
Premier workflow — lint, test, build
Un workflow CI complet pour une application Node.js/Angular enchaîne : lint (qualité du code), tests unitaires, build de production.
# .github/workflows/ci.yml
name: CI — Lint, Test, Build
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
quality:
name: Lint + Tests + Build
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js 20
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Cache le dossier ~/.npm automatiquement
- name: Installer les dépendances
run: npm ci
- name: Lint ESLint
run: npm run lint
# Si cette étape échoue, les suivantes ne s'exécutent pas
- name: Tests unitaires avec couverture
run: npm run test -- --coverage --watchAll=false
env:
CI: true # Désactive le mode watch interactif de Jest/Karma
- name: Upload couverture vers Codecov (optionnel)
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false # Ne bloque pas le CI si Codecov est down
- name: Build production
run: npm run build -- --configuration=production
- name: Upload des artifacts de build
uses: actions/upload-artifact@v4
with:
name: build-dist
path: dist/
retention-days: 7 # Conserve les artifacts 7 jours
Secrets, variables d'environnement et sécurité
Ne jamais mettre de secrets en clair dans les fichiers YAML. GitHub chiffre les secrets et les masque automatiquement dans les logs.
Configurer les secrets dans GitHub
- Aller dans le repo → Settings → Secrets and variables → Actions
- Cliquer sur "New repository secret"
- Nommer le secret en MAJUSCULES avec underscores (ex:
DATABASE_URL) - GitHub masque automatiquement la valeur dans tous les logs
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # Environnement GitHub avec ses propres secrets
steps:
- name: Déployer sur le VPS
env:
# Secrets injectés comme variables d'environnement
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
DATABASE_URL: ${{ secrets.PRODUCTION_DATABASE_URL }}
API_KEY: ${{ secrets.EXTERNAL_API_KEY }}
run: |
# La valeur de SSH_PRIVATE_KEY est masquée dans les logs
echo "$SSH_PRIVATE_KEY" > /tmp/deploy_key
chmod 600 /tmp/deploy_key
ssh -i /tmp/deploy_key -o StrictHostKeyChecking=no \
"$DEPLOY_USER@$DEPLOY_HOST" "cd /app && ./deploy.sh"
Matrice de builds — tests multi-versions
La matrice permet d'exécuter le même job en parallèle avec différentes combinaisons de paramètres (versions Node, OS, navigateurs).
jobs:
test-matrix:
name: Tests Node ${{ matrix.node }} / ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false # Continue les autres combinaisons même si une échoue
matrix:
node: ['18', '20', '22']
os: [ubuntu-latest, windows-latest]
# Exclusions : certaines combinaisons problématiques
exclude:
- node: '18'
os: windows-latest
# Inclusions : configs supplémentaires
include:
- node: '20'
os: ubuntu-latest
coverage: true # Variable custom accessible via matrix.coverage
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: npm ci
- name: Tests
run: npm test
- name: Couverture (seulement pour la config marquée)
if: ${{ matrix.coverage }}
run: npm run test:coverage
Cache des dépendances — accélérer les builds
Sans cache, npm ci télécharge toutes les dépendances à chaque run. Avec le cache, les runs suivants utilisent le cache si package-lock.json n'a pas changé.
# Méthode 1 : Cache automatique via actions/setup-node
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Cache ~/.npm automatiquement
# Pour pnpm : cache: 'pnpm'
# Pour yarn : cache: 'yarn'
# Méthode 2 : Cache manuel avec actions/cache (plus de contrôle)
- name: Cache node_modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
# La clé change uniquement si package-lock.json change
# hashFiles() calcule un hash du fichier — cache invalidé si le lock change
# Cache Angular (cache spécifique .angular)
- name: Cache Angular build cache
uses: actions/cache@v4
with:
path: .angular/cache
key: ${{ runner.os }}-angular-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-angular-
# Le cache .angular/cache accélère le rebuild incrémental de 30-60%
Déploiement automatique sur VPS / DigitalOcean
Workflow complet CI + CD : les tests doivent passer avant le déploiement. Le job deploy dépend du job test via needs.
# .github/workflows/deploy.yml
name: CI + Deploy Production
on:
push:
branches: [main] # Déploie seulement depuis main
jobs:
test:
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 test
- run: npm run build -- --configuration=production
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
deploy:
needs: test # Attend que test réussisse
runs-on: ubuntu-latest
environment: production # Optionnel : environnement avec approbation manuelle
steps:
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Setup SSH key
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts
- name: Sync les fichiers buildés vers le serveur
run: |
rsync -avz --delete \
-e "ssh -i ~/.ssh/deploy_key" \
dist/ \
"${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/var/www/myapp/"
- name: Recharger Nginx
run: |
ssh -i ~/.ssh/deploy_key \
"${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" \
"sudo nginx -t && sudo systemctl reload nginx"
Build et push d'image Docker
# .github/workflows/docker.yml
name: Build et Push Docker Image
on:
push:
branches: [main]
tags: ['v*.*.*'] # Déclenche aussi sur les tags de version
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Docker meta (génère les tags automatiquement)
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }} # GitHub Container Registry
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=sha,prefix=sha-
- name: Login GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} # Token automatique, pas besoin de créer
- name: Build et push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha # Cache GitHub Actions
cache-to: type=gha,mode=max
Notifications et gestion des échecs
# Notification Slack sur échec uniquement
- name: Notification Slack si échec
if: failure() # Seulement si le job a échoué
uses: slackapi/slack-github-action@v1.26.0
with:
payload: |
{
"text": "❌ CI échoué sur *${{ github.repository }}*\nBranche: ${{ github.ref_name }}\nCommit: ${{ github.sha }}\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Voir le run>"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
# Conditions d'exécution — contrôle fin des steps
steps:
- name: Exécuté toujours (même si une étape précédente échoue)
if: always()
run: echo "Cleanup..."
- name: Exécuté seulement si le job réussit
if: success()
run: echo "Deploy..."
- name: Exécuté seulement si annulé
if: cancelled()
run: echo "Cancelled..."
- name: Exécuté si la PR vient d'un fork
if: github.event.pull_request.head.repo.fork == true
run: echo "Restricted CI for forks..."
Bonnes pratiques et optimisations
- Fixer les versions des actions : utiliser
@v4plutôt que@latestpour éviter les régressions silencieuses - Utiliser
npm ciplutôt quenpm install— déterministe, respecte exactement le lock file - Paralléliser les jobs indépendants — lint, test, build peuvent tourner en parallèle si les artifacts ne sont pas partagés
- Limiter les permissions avec
permissions:au niveau du job (principe du moindre privilège) - Cache les dépendances sur
package-lock.json— gain de 1 à 3 minutes par run - Utiliser
concurrencypour annuler les runs précédents quand un nouveau push arrive sur la même branche
# Annuler automatiquement 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 sur la même branche
# Permissions minimales (sécurité)
permissions:
contents: read # Lecture du code seulement
packages: write # Écriture sur ghcr.io seulement si nécessaire
pull-requests: write # Pour commenter les PRs