Skip to Content

Chapitre 13.3 — GitHub Actions

⏱️ TL;DR — GitHub Actions exécute des workflows — des fichiers YAML dans .github/workflows/ — déclenchés par des événements git. Le vocabulaire tient en cinq mots : un workflow réagit à un déclencheur (on:), contient des jobs, chaque job tourne sur une machine (runs-on:) et enchaîne des steps (étapes). On construit un workflow deploy.yml qui, à chaque push sur main, récupère le code (actions/checkout), installe Node (actions/setup-node), lint + teste + build dans un runner propre, puis déploie sur le VPS par SSH — soit en exécutant le deploy.sh distant (via appleboy/ssh-action), soit en poussant les fichiers par rsync. La clé SSH de déploiement est stockée en secret GitHub, jamais dans le code. Bâtir dans la CI décharge le serveur de prod et garantit qu’on ne déploie qu’un artefact déjà testé.

🎯 Objectifs

  • Comprendre le vocabulaire : workflow, on:, jobs, steps, runs-on, actions réutilisables.
  • Écrire un workflow qui lint, teste et build à chaque push, dans un environnement propre.
  • Déployer sur un VPS depuis la CI, par SSH ou par rsync, en réutilisant deploy.sh.
  • Stocker une clé SSH de déploiement en secret GitHub et l’utiliser sans la révéler.
  • Accélérer avec le cache des dépendances, et comprendre pourquoi builder dans la CI vaut mieux que sur le serveur.

Le vocabulaire de GitHub Actions

Un workflow est un fichier YAML rangé dans .github/workflows/ de ton dépôt. GitHub le lit et l’exécute quand un événement survient. Cinq notions suffisent à tout comprendre :

  • on: — le déclencheur. Sur quel événement le workflow tourne-t-il ? Un push sur main, une pull request, un tag, une exécution manuelle, un horaire (cron)…
  • jobs — un workflow contient un ou plusieurs jobs. Chaque job est une unité indépendante qui tourne sur sa propre machine. Par défaut, les jobs s’exécutent en parallèle ; on peut les enchaîner avec needs:.
  • runs-on: — la machine (le runner) sur laquelle le job tourne. Le plus souvent ubuntu-latest : une VM Ubuntu neuve, fournie par GitHub, détruite à la fin.
  • steps — la liste ordonnée d’étapes d’un job. Une étape lance soit une commande shell (run:), soit une action réutilisable (uses:).
  • Les actions (uses:) — des briques toutes prêtes publiées par la communauté : actions/checkout (récupère ton code), actions/setup-node (installe Node), appleboy/ssh-action (exécute des commandes SSH), etc.

Point crucial : chaque job démarre sur une machine vierge. Le runner ne connaît rien de ton dépôt tant que tu n’as pas fait actions/checkout. C’est justement ce qui rend la CI fiable : elle part toujours d’un environnement propre, sans traîner l’état d’une exécution précédente.

💡 Réflexe — Un workflow qui échoue, ça se lit. Chaque exécution apparaît dans l’onglet Actions de ton dépôt GitHub, avec le détail de chaque step et sa sortie. Quand un déploiement casse, tu ne devines pas : tu ouvres l’exécution, tu repères le step rouge, tu lis son log. C’est l’historique et le tableau de bord qui manquaient au déploiement par script.

Un workflow CI : lint, test, build

Commençons par la moitié CI — celle qui ne touche pas encore à la prod, donc sans risque à mettre en place. Ce workflow se déclenche sur les push et les pull requests, et vérifie que le code est sain.

# .github/workflows/ci.yml name: CI on: push: branches: [main] pull_request: branches: [main] jobs: build-and-test: runs-on: ubuntu-latest steps: # 1. Récupérer le code du dépôt sur le runner (sinon il est vide) - uses: actions/checkout@v4 # 2. Installer Node LTS + activer le cache des dépendances npm - uses: actions/setup-node@v4 with: node-version: 20 # aligne-la sur la version de ton serveur cache: npm # met en cache ~/.npm entre les exécutions # 3. Installer les dépendances de façon reproductible - run: npm ci # 4. Vérifier le style et la qualité - run: npm run lint # 5. Lancer les tests - run: npm test # 6. Vérifier que la version de prod compile - run: npm run build

Lis-le de haut en bas : name: nomme le workflow, on: dit quand il tourne, jobs: liste les jobs (ici un seul, build-and-test), runs-on: choisit la machine, et steps: enchaîne les étapes. Les quatre run: (install, lint, test, build) sont exactement ce que tu ferais à la main — sauf que la machine le fait, à chaque push, sur un environnement neuf.

⚠️ Piège — Oublier actions/checkout. C’est la première étape, et son absence donne une erreur déroutante : « fichier introuvable », « pas de package.json »… parce que le runner est vide au démarrage. Il ne contient ton code que après le checkout. Si ton workflow ne trouve rien, vérifie que c’est bien le premier step.

Ajouter le déploiement : le workflow complet

Maintenant la partie CD. On étend le workflow : après que lint, test et build ont réussi, on déploie sur le VPS. Deux jobs, enchaînés par needs: — le déploiement ne part que si la CI est verte.

Voici le workflow deploy.yml complet et commenté. Il déploie en exécutant le deploy.sh distant via l’action appleboy/ssh-action, à laquelle on passe la clé SSH de déploiement stockée en secret :

# .github/workflows/deploy.yml name: Deploy on: push: branches: [main] # on ne déploie QUE depuis main jobs: # ---------- 1. CI : on valide le code ---------- ci: 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 test - run: npm run build # ---------- 2. CD : on déploie, seulement si la CI a réussi ---------- deploy: needs: ci # ce job attend que le job "ci" soit vert runs-on: ubuntu-latest steps: - name: Déployer sur le VPS par SSH uses: appleboy/ssh-action@v1 # vérifie la version majeure courante dans la doc de l'action with: host: ${{ secrets.SSH_HOST }} # l'IP ou le domaine du VPS (secret) username: ${{ secrets.SSH_USER }} # l'utilisateur deploy (secret) key: ${{ secrets.SSH_KEY }} # la CLÉ PRIVÉE de déploiement (secret) script: | cd /var/www/formacampus ./deploy.sh # on réutilise le script du 13.2 !

Ce qu’il faut voir :

  • Le job deploy déclare needs: ci : il ne démarre que si ci a réussi. Un test rouge bloque le déploiement — c’est le pipeline idéal du 13.1 en action.
  • On réutilise le deploy.sh du 13.2. La CI ne réinvente pas le déploiement : elle appelle le script distant, déjà testé et versionné.
  • Les valeurs sensibles (host, username, key) ne sont jamais en clair : elles viennent de ${{ secrets.XXX }}, les secrets du dépôt GitHub (détaillés plus bas et au 13.5).

Variante : déployer par rsync

Plutôt qu’exécuter un script distant, tu peux envoyer les fichiers directement. C’est pertinent quand tu builds dans la CI (voir plus loin) et que tu veux juste copier l’artefact sur le serveur. rsync sur SSH ne transfère que ce qui a changé :

deploy: needs: ci 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 build # le build est produit ICI, dans la CI - name: Installer la clé SSH de déploiement run: | mkdir -p ~/.ssh echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_deploy chmod 600 ~/.ssh/id_deploy ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts - name: Envoyer l'artefact buildé sur le VPS run: | rsync -az --delete -e "ssh -i ~/.ssh/id_deploy" \ ./build/ ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/var/www/formacampus/current/

Les deux variantes sont légitimes. ssh-action + deploy.sh garde toute la logique de déploiement sur le serveur (un seul endroit à maintenir). rsync convient quand la CI produit l’artefact et n’a qu’à le poser sur le serveur — c’est le modèle qui se marie bien avec les releases du 13.4.

📚 La doc — Ne recopie jamais une version d’action au hasard : chaque action (actions/checkout, actions/setup-node, appleboy/ssh-action…) publie sa version majeure courante et sa syntaxe exacte sur sa page du GitHub Marketplace  et dans son README. La syntaxe des with: évolue. La doc officielle GitHub Actions  fait autorité pour le reste (déclencheurs, expressions ${{ }}, matrices).

Les secrets GitHub : la clé de déploiement

La CI a besoin de se connecter à ton VPS. Elle ne peut pas taper un mot de passe interactivement, et tu ne veux surtout pas écrire la clé dans le YAML (qui est dans le dépôt, donc versionné et parfois public). La réponse : les secrets GitHub.

Un secret GitHub est une valeur chiffrée, stockée hors du dépôt (dans Settings → Secrets and variables → Actions), que le workflow lit via ${{ secrets.NOM }}. GitHub masque automatiquement ces valeurs dans les logs (elles apparaissent en ***). Marche à suivre pour la clé de déploiement :

# --- Sur ton poste : générer une PAIRE de clés DÉDIÉE au déploiement --- ssh-keygen -t ed25519 -f deploy_key -C "github-actions-deploy" -N "" # Produit deux fichiers : # deploy_key -> la clé PRIVÉE -> ira dans le secret GitHub SSH_KEY # deploy_key.pub -> la clé PUBLIQUE -> ira sur le serveur
# --- Sur le VPS : autoriser cette clé publique pour l'utilisateur deploy --- # On colle le contenu de deploy_key.pub dans le fichier des clés autorisées : cat deploy_key.pub >> /home/deploy/.ssh/authorized_keys

Ensuite, dans GitHub : Settings → Secrets and variables → Actions → New repository secret, tu crées SSH_KEY (contenu de deploy_key, la privée), SSH_HOST, SSH_USER. Le workflow les lit via ${{ secrets.SSH_KEY }} et consorts.

🔒 Sécurité — Cette clé de déploiement doit être dédiée et à privilèges minimaux : ce n’est pas ta clé personnelle. Génère une paire rien que pour la CI, autorise-la pour le seul utilisateur deploy, et le jour où tu soupçonnes une fuite, tu la révoques (retire la ligne de authorized_keys) sans impacter tes autres accès. On peut même la restreindre à une seule commande côté serveur (option command= dans authorized_keys) pour qu’elle ne puisse que déclencher le déploiement. Tout ce volet est développé au 13.5.

⚠️ PiègeCommitter la clé privée dans le dépôt « pour aller vite ». Une clé privée dans git est compromise pour toujours : même supprimée dans un commit ultérieur, elle reste dans l’historique, et si le dépôt est public, des bots la trouvent en minutes. La clé privée va uniquement dans un secret GitHub ; le YAML n’en voit qu’une référence (${{ secrets.SSH_KEY }}), jamais la valeur.

Cache et matrices : accélérer et couvrir

Deux fonctionnalités que tu croiseras, ici en survol :

  • Le cache — réinstaller les dépendances à chaque exécution est lent. actions/setup-node avec cache: npm met en cache le dossier ~/.npm (le cache de téléchargement npm) entre les exécutions, ce qui accélère npm ci. Pour des cas plus fins, l’action actions/cache permet de mettre en cache n’importe quel dossier en fonction d’une clé (souvent le hash du lock).
  • Les matrices — un job peut tourner en plusieurs variantes d’un coup. Une matrice strategy.matrix lance par exemple les tests sur Node 18, 20 et 22 en parallèle, pour vérifier la compatibilité. Utile pour une bibliothèque ; pour un déploiement (où tu cibles UNE version de Node, celle du serveur), ça n’a en général pas d’intérêt.

Builder dans la CI vs sur le serveur

Le vrai gain d’architecture est là. Rappelle-toi la limite du 13.2 : builder sur le VPS de prod consomme ses ressources et peut le faire ramer. En bâtissant dans la CI :

  • le serveur de prod est déchargé — il ne fait plus que recevoir un artefact déjà buildé et basculer dessus ; sa RAM et son CPU restent pour servir le trafic ;
  • l’artefact déployé est exactement celui qui a passé les tests — pas de « et si le build sur le serveur donnait un résultat différent ? » ;
  • le build échoue dans la CI, avant de toucher à la prod : un build cassé n’atteint jamais le serveur.

C’est le basculement conceptuel de la partie : on ne « déploie » plus du code source à builder sur place, on déploie un artefact fini, testé, immuable. Le serveur ne compile plus, il exécute.

🧭 Sur FormaCampus — L’équipe crée .github/workflows/deploy.yml. À chaque merge sur main : GitHub installe, lint, teste et build le front Next.js sur un runner Ubuntu jetable, puis — seulement si tout est vert — se connecte au VPS avec la clé de déploiement dédiée (en secret) et lance le deploy.sh écrit au 13.2. Le VPS ne builde plus rien : il reçoit un artefact validé et bascule dessus. Fini les ralentissements pendant les livraisons, fini les « ça marchait chez moi ». Chaque déploiement est visible dans l’onglet Actions, avec son commit et ses logs. Reste à rendre la bascule sans coupure et réversible : c’est le 13.4.

Ce qu’il faut retenir

GitHub Actions exécute des workflows YAML déclenchés par on:, faits de jobs (runs-on:) et de steps (run: ou uses:). Un workflow CI installe, lint, teste et build à chaque push, sur un runner propre — commence par lui, il est sans risque. On ajoute la CD avec un job deploy en needs: ci, qui se connecte au VPS (via appleboy/ssh-action + deploy.sh, ou par rsync), en utilisant une clé SSH dédiée stockée en secret GitHub. Builder dans la CI décharge le serveur et garantit qu’on ne déploie qu’un artefact testé et immuable.

✏️ Exercices

Exercice 1 — Lis le déclencheur. Un workflow commence par on: avec push: sur branches: [main] et un job deploy qui a needs: ci. Un développeur pousse sur une branche feature/login. Le déploiement part-il ? Et s’il ouvre une pull request vers main ? Explique.

✅ Solution

Un push sur feature/login ne déclenche pas ce workflow : on: push est restreint à branches: [main]. Le déploiement ne part donc pas. Ouvrir une pull request vers main ne le déclenche pas non plus tel quel (il faudrait un pull_request: dans on:), et surtout une PR ne fait pas de push sur main : le code n’arrive sur main qu’au merge, qui, lui, est un push sur main — c’est que le workflow (CI puis, si verte, deploy) s’exécute. Le needs: ci garantit en plus que deploy ne part que si le job ci a réussi.

Exercice 2 — Où va la clé ? Tu dois donner à GitHub Actions l’accès SSH à ton VPS. Explique, en trois points, où va la clé privée, où va la clé publique, et pourquoi on ne met jamais la clé privée dans le fichier deploy.yml.

✅ Solution

(1) La clé privée va dans un secret GitHub (Settings → Secrets → Actions, ex. SSH_KEY), chiffré et masqué dans les logs ; le workflow n’y accède que via ${{ secrets.SSH_KEY }}. (2) La clé publique va sur le VPS, dans ~/.ssh/authorized_keys de l’utilisateur deploy, pour autoriser cette clé. (3) On ne met jamais la clé privée dans deploy.yml parce que ce fichier est dans le dépôt, donc versionné (et potentiellement public) : une clé privée commitée reste dans l’historique git pour toujours et est considérée comme compromise. On génère de plus une clé dédiée au déploiement, révocable sans toucher aux autres accès.

🧠 Quiz de révision

1. Que représentent on:, jobs, runs-on: et steps dans un workflow ?

on: = le déclencheur (quel événement lance le workflow, ex. push sur main). jobs = les unités de travail indépendantes, chacune sur sa machine. runs-on: = la machine (le runner, ex. ubuntu-latest) sur laquelle un job tourne. steps = la liste ordonnée d’étapes d’un job (commandes run: ou actions uses:).

2. Pourquoi actions/checkout est-il presque toujours le premier step ?

Parce que le runner démarre vide : il ne contient pas ton code. actions/checkout récupère le dépôt sur la machine. Sans lui, les steps suivants ne trouvent ni package.json ni les fichiers du projet, d’où des erreurs « fichier introuvable ». C’est la première brique de presque tous les workflows.

3. À quoi sert needs: entre deux jobs ?

needs: crée une dépendance d’ordre : le job qui le déclare attend qu’un autre job réussisse avant de démarrer. Ex. deploy avec needs: ci ne se lance que si le job ci (lint/test/build) est vert. C’est ce qui empêche de déployer du code qui n’a pas passé les tests.

4. Comment le workflow accède-t-il à la clé SSH sans l’écrire en clair ?

Via les secrets GitHub : la clé privée est stockée chiffrée dans Settings → Secrets → Actions, et le workflow la lit avec ${{ secrets.SSH_KEY }}. GitHub masque la valeur dans les logs (***). Le fichier YAML ne contient qu’une référence au secret, jamais la valeur elle-même.

5. Quel est l’avantage principal de builder dans la CI plutôt que sur le serveur de prod ?

Le serveur de prod est déchargé (il ne compile plus, il reçoit un artefact déjà buildé et bascule dessus, gardant CPU/RAM pour le trafic) et l’on ne déploie qu’un artefact déjà testé et immuable. Un build qui échoue le fait dans la CI, avant de toucher à la prod, qui n’est donc jamais mise en péril par une compilation ratée.


Chapitre suivant : 13.4 — Stratégies de release — déployer sans coupure et pouvoir revenir en arrière : les releases horodatées, le symlink current, le rollback instantané, et les migrations de base.

Last updated on