
29 septembre 2026
CI/CD et protection de branches : les leçons d'un projet de fin de formation
En bref : seul sur mon projet de titre RNCP, j'ai mis en place une CI/CD avec GitHub Actions (lint, tests, build, déploiement) et protégé ma branche
main. Ce que j'en retiens tient en une phrase : chaque incident est une règle automatique qui attend d'être écrite.
Mon projet de titre RNCP était une PWA de calcul d'itinéraire personnalisé selon les préférences de l'utilisateur, pour une métropole de 500 000 habitants. J'étais seul sur le projet, et mon titre en dépendait. Personne ne m'imposait de CI/CD : c'était un choix personnel, et le jury l'a relevé comme un point positif.
Dans cet article, je te raconte ce que j'ai mis en place et surtout ce que les incidents m'ont appris. La plupart de mes améliorations sont venues d'un problème réel plutôt que d'un plan.
Le contexte
Le projet repose sur un front React (Vite), un back NestJS, une base PostgreSQL/PostGIS et un moteur d'itinéraires OpenTripPlanner. Le tout tourne dans Docker sur un VPS OVH, derrière Caddy.
J'étais seul, mais j'ai séparé mes rôles : PO, UX/UI, front, back, sysadmin. C'est d'ailleurs « en tant que PO » que j'ai ouvert l'issue qui a mené à la plus grosse amélioration de ma CI. On y vient.
La pipeline
Un seul workflow GitHub Actions, déclenché à chaque pull request vers main et à chaque push sur main. Il contient cinq jobs :
| Job | Ce qu'il fait | Requis pour merger | Bloque le déploiement |
|---|---|---|---|
| frontend | lint, tests (Vitest), build | ✅ | ✅ |
| backend | lint, tests (Jest), build | ✅ | ✅ |
| secrets-scan | gitleaks sur tout l'historique git | ❌ | ✅ |
| e2e-wcag | audit d'accessibilité (Playwright) | ❌ | ❌ |
| deploy | déploiement en production, uniquement sur push sur main | – | – |
Le déploiement construit le front et envoie les fichiers statiques sur le VPS avec rsync. Côté serveur, il fait ensuite un git reset --hard origin/main puis un docker compose up --build -d pour redémarrer le backend.

Voici un extrait simplifié du workflow :
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
backend:
name: Backend — lint & tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 20
cache: npm
cache-dependency-path: backend/package-lock.json
- run: npm ci
working-directory: backend
- run: npm run lint
working-directory: backend
- run: npm test
working-directory: backend
- run: npm run build # le vrai build, distinct des tests
working-directory: backend
# frontend, secrets-scan, e2e-wcag : même principe
deploy:
needs: [frontend, backend, secrets-scan]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
concurrency:
group: deploy-production
cancel-in-progress: false
Deux choix que j'assume :
e2e-wcagne bloque pas le déploiement. Le job est récent, et je veux être sûr qu'il ne bloquera jamais un déploiement légitime à tort avant de le rendre bloquant.- La couverture de tests est un simple rapport, sans seuil. Sans historique, un seuil arbitraire bloquerait la CI plutôt que de m'aider.
Protéger la branche main
Sur GitHub, main est protégée par trois règles :
- les checks frontend et backend doivent être verts ;
- le force-push est interdit ;
- la branche ne peut pas être supprimée.
Comme j'étais seul, je n'ai pas exigé d'approbation : la review reste conseillée, mais pas obligatoire.
⚠️ Une limite que je connais : les règles ne s'appliquent pas au propriétaire du dépôt, donc je pourrais techniquement les contourner. Je ne le fais pas, par principe.
Mon cycle est toujours le même : une branche par feature, une PR, une CI verte, un merge, et retour sur main. La règle technique est un filet, la discipline fait le reste.
Ce que les incidents m'ont appris
1. Le build que ma CI ne lançait jamais (8 août)
Un bug a rendu mon backend indisponible : un paquet (nodemailer) manquait dans un volume Docker resté obsolète côté dev. La cause n'avait rien à voir avec le code, mais en cherchant, j'ai découvert un trou dans ma CI. Voici ce que j'ai écrit dans l'issue :
En tant que PO, […] je constate que la CI ne fait jamais tourner le vrai build […]
— issue #104
Ma CI ne lançait que le lint et les tests. Or Jest et Vitest ont leur propre transformation TypeScript, plus tolérante : une erreur de compilation pouvait donc passer au vert et casser le déploiement. J'ai ajouté l'étape build dans les deux jobs (PR #105).
💡 Leçon : des tests verts ne prouvent pas que le build passe.
2. Un test qui avait pourri en silence (4 septembre)
J'ai ajouté un job Playwright pour automatiser mon audit d'accessibilité WCAG, jusque-là lancé à la main. Le message de mon commit résume le premier vrai run :
Premier run réel de la suite WCAG en CI, 8/10 tests passent
Le premier échec venait d'un détail de données de test : le géocodeur renvoie « Universite » sans accent. Le second était plus instructif : un test visait un bouton qui n'existait plus, remplacé par d'autres éléments d'interface au fil des évolutions. Personne ne l'avait remarqué, car il n'était jamais lancé automatiquement.
Je l'ai marqué test.fixme, ouvert une issue (#264) et je l'ai réécrit dans une PR suivante.

Les premiers runs de ce job et de gitleaks ont aussi demandé plusieurs ajustements (permissions Docker, faux positifs). Une CI se débogue comme du code.
💡 Leçon : un test qui n'est pas lancé automatiquement finit par ne plus rien tester.
3. Deux problèmes de déploiement
Les déploiements en collision. Après six merges Dependabot enchaînés, six déploiements se sont lancés presque en même temps sur le même VPS, et cinq ont échoué sur un conflit de nom de conteneur. J'ai ajouté un groupe de concurrence : les déploiements font la queue, et aucun n'est interrompu en cours de route.
Le .env qui dérive. Des clés oubliées dans le .env du serveur (GEOLOCATION_ENCRYPTION_KEY, les VAPID_*) cassaient des fonctionnalités en silence. Un script compare maintenant les noms des clés attendues avec celles présentes sur le VPS (jamais les valeurs) et lève un avertissement à chaque déploiement.
4. Une mise à jour bloquée avant la production
Dependabot a proposé de mettre à jour 13 paquets NestJS d'un coup. L'installation a échoué en quelques secondes :
npm error code ERESOLVE
npm error While resolving: @nestjs/schematics@12.0.3
npm error Could not resolve dependency:
npm error peer typescript@">=6.0.0" from @nestjs/schematics@12.0.3
Traduction : la nouvelle version du CLI exigeait une version de TypeScript supérieure à la mienne. La CI est passée au rouge et rien n'a été déployé. Je n'ai pas encore traité cette PR, mais elle n'a pas pu m'exploser en production.
Au quotidien, les tests unitaires (près de 250 côté backend début septembre) m'ont surtout servi de filet à chaque intégration de feature, pour repérer les régressions.
Ce que je referais autrement
- Mettre le build en CI dès le premier jour. C'est ce que je fais maintenant sur mes projets.
- Lancer automatiquement tout ce qui compte. Un contrôle manuel finit toujours par être oublié.
- Introduire un nouveau contrôle en mode informatif, puis le rendre bloquant quand il a fait ses preuves.
- Transformer chaque incident en règle automatique. Chacun des problèmes ci-dessus est devenu une vérification de la pipeline.
Pour conclure
Je n'ai pas construit cette pipeline en une fois. Elle s'est enrichie à chaque incident, et c'est ce qui m'a le plus appris.
Si tu débutes, mon conseil est simple : mets lint, tests et build en CI dès le début, et considère chaque rouge comme une information gratuite plutôt qu'une contrariété.