Tu veux automatiser ton workflow de livraison logicielle avec GitHub Actions ? Pas juste un hello world qui lance un echo, mais un vrai pipeline CI/CD — du lint au déploiement Kubernetes. Ce guide te prend par la main et t’emmène de zéro à un pipeline de production complet.
On va construire chaque étape, bloc par bloc, avec des workflows YAML fonctionnels que tu peux copier et adapter à tes projets. Pas de théorie creuse : les mains dans le cambouis.
Pourquoi GitHub Actions pour ton pipeline CI/CD
#GitHub Actions s’est imposé comme la plateforme CI/CD de référence pour les équipes DevOps. Intégré nativement à GitHub, il élimine le besoin d’un outil externe comme Jenkins ou CircleCI. Voici pourquoi il fait sens :
- Zéro infrastructure à gérer (ou presque — on parlera des self-hosted runners)
- Marketplace riche avec des milliers d’actions prêtes à l’emploi
- Matrix builds pour tester sur plusieurs versions en parallèle
- Environments avec gates d’approbation pour les déploiements sensibles
- Gratuit pour les repos publics, généreux pour les repos privés
Mais la vraie force de GitHub Actions, c’est sa flexibilité. Tu peux modéliser n’importe quel workflow, du plus simple au plus complexe, en YAML déclaratif.
Architecture du pipeline qu’on va construire
#Avant de plonger dans le code, voici la vue d’ensemble de notre github actions pipeline CI/CD :
1
2
3
| Push/PR → Lint → Tests (matrix) → Build Docker → Push Registry → Deploy K8s
↓
Staging → Prod
|
Chaque étape est un job distinct avec ses dépendances. Si le lint échoue, on ne lance pas les tests. Si les tests échouent, on ne build pas l’image Docker. C’est le principe du fail fast.
Étape 1 : structure du projet
#On part d’une application Node.js classique, mais les principes s’appliquent à n’importe quel langage. Voici la structure :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| mon-app/
├── .github/
│ └── workflows/
│ └── ci-cd.yml
├── src/
│ └── index.js
├── tests/
│ └── index.test.js
├── Dockerfile
├── k8s/
│ ├── deployment.yml
│ └── service.yml
├── package.json
└── .eslintrc.json
|
Les workflows GitHub Actions vivent dans .github/workflows/. Chaque fichier YAML dans ce dossier est un workflow indépendant.
Étape 2 : le workflow de base — lint et tests
#Commençons par la fondation. Ce premier workflow se déclenche sur chaque push et pull request :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: "20"
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout du code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- name: Installation des dépendances
run: npm ci
- name: Exécution du linter
run: npm run lint
test:
name: Tests
needs: lint
runs-on: ubuntu-latest
steps:
- name: Checkout du code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- name: Installation des dépendances
run: npm ci
- name: Exécution des tests
run: npm test
- name: Upload des résultats de couverture
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
retention-days: 7
|
Quelques points importants :
npm ci plutôt que npm install — plus rapide et déterministe en CIcache: "npm" dans setup-node — met en cache le répertoire npm pour accélérer les builds suivantsneeds: lint — le job test attend que le lint passe avant de démarrer- Les artefacts de couverture sont uploadés pour consultation ultérieure
Étape 3 : matrix builds — tester sur plusieurs versions
#En production, ton application tourne peut-être sur différentes versions de Node.js, ou tu veux tester sur plusieurs OS. C’est là que les matrix builds entrent en jeu :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| test-matrix:
name: Tests (${{ matrix.node-version }} / ${{ matrix.os }})
needs: lint
runs-on: ${{ matrix.os }}
strategy:
matrix:
node-version: [18, 20, 22]
os: [ubuntu-latest, ubuntu-22.04]
fail-fast: false
steps:
- name: Checkout du code
uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- name: Installation des dépendances
run: npm ci
- name: Exécution des tests
run: npm test
|
La stratégie matrix génère automatiquement 6 jobs (3 versions × 2 OS) qui tournent en parallèle. L’option fail-fast: false permet de voir tous les résultats même si un job échoue — utile pour identifier exactement quelles combinaisons posent problème.
Étape 4 : build et push de l’image Docker
#Une fois les tests passés, on construit l’image Docker et on la pousse vers un registry. On utilise GitHub Container Registry (ghcr.io), mais la même logique s’applique avec Docker Hub, AWS ECR ou Harbor :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| build-and-push:
name: Build & Push Docker
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
image-digest: ${{ steps.build.outputs.digest }}
steps:
- name: Checkout du code
uses: actions/checkout@v4
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login au registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extraction des métadonnées Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=
type=ref,event=branch
type=semver,pattern={{version}}
- name: Build et push de l'image
id: build
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
|
Détaillons les choix techniques :
- Docker Buildx pour le build multi-plateforme (amd64 + arm64)
cache-from: type=gha — le cache de layers Docker est stocké dans le cache GitHub Actions, ce qui accélère considérablement les builds suivantsmetadata-action génère automatiquement des tags intelligents (SHA du commit, nom de branche, version semver)if: github.event_name == 'push' && github.ref == 'refs/heads/main' — on ne build l’image que sur les pushes vers main, pas sur les PR- Les outputs permettent de transmettre le tag et le digest de l’image aux jobs suivants
Étape 5 : gestion des secrets
#Les secrets sont le nerf de la guerre en CI/CD. GitHub Actions offre plusieurs niveaux de gestion :
Secrets au niveau du repository
#Configurés dans Settings → Secrets and variables → Actions :
1
2
3
4
5
6
7
8
| - name: Deploy avec clé SSH
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
KUBECONFIG_DATA: ${{ secrets.KUBECONFIG_DATA }}
run: |
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
|
Secrets au niveau des environments
#Les environments permettent d’avoir des secrets différents pour staging et production :
1
2
3
4
5
6
7
8
9
| deploy-staging:
name: Deploy Staging
environment: staging
# Les secrets de l'environment "staging" sont automatiquement injectés
steps:
- name: Connexion au cluster
env:
KUBECONFIG: ${{ secrets.KUBECONFIG }} # Différent par environment
run: kubectl get nodes
|
Bonnes pratiques pour les secrets
#- Ne jamais afficher un secret dans les logs (GitHub les masque automatiquement, mais méfie-toi des encodages)
- Utiliser
GITHUB_TOKEN plutôt qu’un PAT quand c’est possible — il est éphémère et scopé au workflow - Rotation régulière des secrets, surtout les clés de déploiement
- Préférer les OIDC tokens pour les providers cloud (AWS, GCP, Azure) plutôt que des credentials statiques
Étape 6 : déploiement sur Kubernetes
#Voici la partie la plus intéressante du pipeline — le déploiement. On utilise les environments GitHub pour séparer staging et production, avec une gate d’approbation manuelle pour la prod :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
| deploy-staging:
name: Deploy Staging
needs: build-and-push
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.mon-app.ch
steps:
- name: Checkout du code
uses: actions/checkout@v4
- name: Setup kubectl
uses: azure/setup-kubectl@v4
with:
version: "v1.31.0"
- name: Configuration du kubeconfig
run: |
mkdir -p $HOME/.kube
echo "${{ secrets.KUBECONFIG }}" | base64 -d > $HOME/.kube/config
- name: Déploiement sur staging
run: |
export IMAGE_TAG="${{ needs.build-and-push.outputs.image-tag }}"
envsubst < k8s/deployment.yml | kubectl apply -f - -n staging
kubectl apply -f k8s/service.yml -n staging
kubectl rollout status deployment/mon-app -n staging --timeout=300s
- name: Smoke test
run: |
sleep 10
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://staging.mon-app.ch/health)
if [ "$STATUS" != "200" ]; then
echo "Smoke test échoué: HTTP $STATUS"
exit 1
fi
deploy-production:
name: Deploy Production
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://mon-app.ch
steps:
- name: Checkout du code
uses: actions/checkout@v4
- name: Setup kubectl
uses: azure/setup-kubectl@v4
with:
version: "v1.31.0"
- name: Configuration du kubeconfig
run: |
mkdir -p $HOME/.kube
echo "${{ secrets.KUBECONFIG }}" | base64 -d > $HOME/.kube/config
- name: Déploiement sur production
run: |
export IMAGE_TAG="${{ needs.build-and-push.outputs.image-tag }}"
envsubst < k8s/deployment.yml | kubectl apply -f - -n production
kubectl apply -f k8s/service.yml -n production
kubectl rollout status deployment/mon-app -n production --timeout=300s
- name: Vérification post-déploiement
run: |
sleep 15
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://mon-app.ch/health)
if [ "$STATUS" != "200" ]; then
echo "Rollback automatique..."
kubectl rollout undo deployment/mon-app -n production
exit 1
fi
echo "Déploiement production réussi ✓"
|
Points clés de cette configuration :
- L’environment
production doit être configuré dans GitHub avec des required reviewers — un humain doit approuver avant le déploiement - Le smoke test sur staging valide que l’application répond avant de passer en prod
- Le rollback automatique en production si le health check échoue après déploiement
envsubst injecte dynamiquement le tag de l’image dans le manifest Kubernetes
Étape 7 : optimiser avec le caching
#Le caching est crucial pour réduire les temps de build. GitHub Actions offre plusieurs mécanismes :
Cache natif avec actions/cache
# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| - name: Cache des dépendances npm
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Cache des layers Docker
uses: actions/cache@v4
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
|
Cache intégré aux actions
#Beaucoup d’actions officielles gèrent le cache automatiquement — c’est le cas de setup-node avec l’option cache: "npm" qu’on a utilisée plus haut. Préfère toujours le cache intégré quand il existe.
Impact réel du caching
#Sur un projet Node.js typique, le caching réduit le temps d’installation des dépendances de 60-90 secondes à 5-10 secondes. Pour les builds Docker avec le cache GHA, les rebuilds partiels passent de minutes à secondes. Ça s’accumule vite quand tu as 20+ pushes par jour.
Étape 8 : self-hosted runners
#Les runners GitHub hébergés sont pratiques, mais parfois tu as besoin de plus — accès à un réseau privé, hardware spécifique, ou simplement réduire les coûts :
Quand utiliser un self-hosted runner
#- Accès réseau privé — déploiement vers un cluster K8s interne
- Hardware spécifique — GPU, ARM natif, stockage rapide
- Conformité — données qui ne doivent pas quitter ton infrastructure
- Coûts — au-delà de 2000 minutes/mois, l’auto-hébergement devient rentable
Configuration d’un runner
#1
2
3
4
5
6
7
8
| # Sur ta machine ou VM
mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64-2.321.0.tar.gz -L \
https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-linux-x64-2.321.0.tar.gz
tar xzf actions-runner-linux-x64-2.321.0.tar.gz
./config.sh --url https://github.com/ton-org/ton-repo --token TON_TOKEN
./svc.sh install
./svc.sh start
|
Utilisation dans un workflow
#1
2
3
4
5
6
7
| deploy-internal:
name: Deploy interne
runs-on: [self-hosted, linux, x64]
steps:
- name: Deploy sur K8s interne
run: |
kubectl apply -f k8s/ -n production
|
Le label self-hosted cible tes runners. Tu peux ajouter des labels custom (gpu, high-memory, staging-zone) pour router les jobs vers le bon runner.
Sécurité des self-hosted runners
#- Jamais sur un repo public — n’importe qui pourrait exécuter du code sur ta machine via une PR
- Utiliser des conteneurs éphémères (le runner tourne dans un container qui est détruit après chaque job)
- Mettre à jour régulièrement le runner
- Isoler le réseau : le runner ne doit accéder qu’aux ressources nécessaires
Le workflow complet assemblé
#Voici le pipeline complet, toutes les étapes réunies dans un seul fichier :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
| name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
NODE_VERSION: "20"
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
permissions:
contents: read
packages: write
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- run: npm ci
- run: npm run lint
test:
name: Tests
needs: lint
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: "npm"
- run: npm ci
- run: npm test
build-and-push:
name: Build & Push Docker
needs: test
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
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 }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=
type=ref,event=branch
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy-staging:
name: Deploy Staging
needs: build-and-push
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.mon-app.ch
steps:
- uses: actions/checkout@v4
- uses: azure/setup-kubectl@v4
- run: |
mkdir -p $HOME/.kube
echo "${{ secrets.KUBECONFIG }}" | base64 -d > $HOME/.kube/config
- run: |
export IMAGE_TAG="${{ needs.build-and-push.outputs.image-tag }}"
envsubst < k8s/deployment.yml | kubectl apply -f - -n staging
kubectl rollout status deployment/mon-app -n staging --timeout=300s
deploy-production:
name: Deploy Production
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://mon-app.ch
steps:
- uses: actions/checkout@v4
- uses: azure/setup-kubectl@v4
- run: |
mkdir -p $HOME/.kube
echo "${{ secrets.KUBECONFIG }}" | base64 -d > $HOME/.kube/config
- run: |
export IMAGE_TAG="${{ needs.build-and-push.outputs.image-tag }}"
envsubst < k8s/deployment.yml | kubectl apply -f - -n production
kubectl rollout status deployment/mon-app -n production --timeout=300s
|
Bonnes pratiques pour un pipeline CI/CD robuste
#Après avoir monté des dizaines de pipelines GitHub Actions, voici les leçons apprises :
Versionner les actions
#Toujours utiliser un tag précis (@v4) ou mieux, un SHA de commit (@abc123) pour les actions tierces. Un @main peut casser ton pipeline sans prévenir.
Limiter les permissions
#Utiliser le principe du moindre privilège avec le bloc permissions. Par défaut, GITHUB_TOKEN a des permissions larges — restreins-les :
1
2
3
4
| permissions:
contents: read
packages: write
# Rien d'autre
|
Paralléliser intelligemment
#Les jobs sans dépendance entre eux doivent tourner en parallèle. Le lint et les tests de sécurité (SAST, dependency scan) peuvent tourner en même temps.
Notifications en cas d’échec
#Ajoute un job de notification pour être alerté des échecs :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| notify:
name: Notification
needs: [deploy-production]
if: failure()
runs-on: ubuntu-latest
steps:
- name: Notification Slack
uses: slackapi/slack-github-action@v2.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{
"text": "❌ Pipeline échoué sur ${{ github.repository }} — ${{ github.event.head_commit.message }}"
}
|
Timeouts explicites
#Définis toujours un timeout sur tes jobs pour éviter les runners bloqués qui consomment tes minutes :
1
2
| test:
timeout-minutes: 15
|
Aller plus loin
#Ce pipeline couvre 90 % des cas d’usage, mais tu peux l’étendre :
- Scans de sécurité — intègre Trivy pour scanner tes images Docker et Snyk pour les dépendances
- Tests d’intégration — lance un
docker compose avec ta base de données dans le workflow via les services GitHub Actions - GitOps — au lieu de
kubectl apply, mets à jour un repo ArgoCD ou Flux qui se charge du déploiement - Release automation — utilise
semantic-release pour versionner automatiquement à partir des commits conventionnels - Reusable workflows — factorise les patterns communs dans des workflows réutilisables partagés entre tes repos
Conclusion
#Construire un github actions pipeline CI/CD complet demande un peu de travail initial, mais le retour sur investissement est immédiat. Chaque push est automatiquement vérifié, testé, packagé et déployé — sans intervention manuelle.
L’approche progressive qu’on a suivie — lint, tests, build Docker, push registry, déploiement K8s — te permet de démarrer petit et d’ajouter des étapes au fur et à mesure. Commence par le lint et les tests. Une fois que c’est solide, ajoute le build Docker. Puis le déploiement staging. Et enfin la production avec gate d’approbation.
Le plus important : ton pipeline est du code. Il vit dans ton repo, il est versionné, reviewable et reproductible. C’est ça, l’infrastructure as code appliquée à la CI/CD.
Maintenant, à toi de jouer. Fork ce workflow, adapte-le à ton stack, et pousse ton premier commit. Le pipeline fera le reste. 🚀