Changer le thème

CI/CD Next.js : guide pratique avec GitHub Actions pour tests et déploiement

Easton editorial illustration: rendering-mode selector

Les logs défilent dans le terminal, les doigts tapent mécaniquement git pull && npm install && npm run build && pm2 restart. Troisième déploiement aujourd’hui — un petit correctif le matin, une optimisation d’API à midi, quelques retouches CSS maintenant. Trois serveurs, la même routine sur chacun.

Après le dernier, il est déjà 19 h.

Sur le chemin du retour, je me suis dit qu’il devait y avoir mieux. Je ne voulais plus répéter ces commandes sur le serveur à chaque modification, ni craindre qu’un serveur oublié laisse la prod dans un état incohérent. Les tests ? Souvent sautés — on voulait déployer vite, puis corriger en urgence.

Puis CI/CD et GitHub Actions ont changé le flux. Un push sur GitHub, et tests, build et déploiement tournent seuls. Le temps d’un café, la nouvelle version est en ligne.

Si le déploiement manuel vous épuise aussi, voici comment j’ai monté l’automatisation Next.js avec GitHub Actions — de la config de base au cas complet, pièges inclus.

Pourquoi adopter le CI/CD

Les douleurs du déploiement manuel

Au début de mes projets Next.js, le rituel était : coder en local, tester un peu, SSH sur le serveur, git pull, npm install, npm run build, pm2 restart. Une quinzaine de minutes quand tout allait bien.

Souvent, non.

Une fois, sur trois serveurs, les deux premiers étaient à jour, le troisième non — SSH coupé sans que je le voie. Le lendemain, des utilisateurs voyaient tantôt la nouvelle version, tantôt l’ancienne : le load balancer envoyait encore du trafic sur la machine non mise à jour. Une autre fois, j’ai déployé sans tests ; un bug critique en prod, rollback en urgence.

Pire encore : build ID différent par serveur. Chaque npm run build génère un nouvel ID ; derrière un load balancer, Next.js détecte le changement et force un hard refresh. L’expérience utilisateur en souffre.

Ce que le CI/CD change

En bref, il automatise ce qui est répétitif et fragile.

CI (intégration continue) : à chaque push, tests automatiques — unitaires, types, lint. Échec ? Pas de déploiement, il faut corriger.

CD (déploiement continu) : tests OK → build puis déploiement, sans intervention manuelle.

Aujourd’hui : code local → push GitHub → tests → build → déploiement. Environ cinq minutes, sans surveiller le terminal.

Chaque run laisse des logs : commit, résultats des tests, cible du déploiement. Le diagnostic est bien plus simple qu’avant.

Bases de GitHub Actions

Comment ça s’organise

Workflow, job, step — au début c’est dense, mais la logique est simple :

Workflow : le pipeline complet (ex. test + build + deploy), défini en YAML dans .github/workflows.

Job : tâche indépendante (ex. job « test », job « deploy »), en parallèle ou en chaîne.

Step : action concrète dans un job (checkout, install, test…).

Déclencheurs flexibles : push sur main, Pull Request, cron, etc.

Premier workflow

Créez .github/workflows/ci-cd.yml :

name: CI/CD Pipeline

# Déclenché à chaque push sur main
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

À chaque push sur main : checkout, Node 18, npm ci, build. Après le commit, l’onglet Actions du dépôt affiche l’exécution — la première coche verte fait plaisir.

Secrets pour les données sensibles

IP serveur, clés SSH, tokens API : jamais en clair dans le YAML. Utilisez Settings → Secrets and variables → Actions → New repository secret (ex. SERVER_HOST), puis ${{ secrets.SERVER_HOST }} dans le workflow. Les logs masquent les valeurs.

Pipeline de tests automatiques

Configuration

Tout ce qui peut l’être doit l’être : ESLint, TypeScript, Jest — la majorité des régressions est interceptée avant la prod.

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'  # accélère les runs suivants

      - name: Install dependencies
        run: npm ci

      - name: Lint check
        run: npm run lint

      - name: Type check
        run: npm run type-check

      - name: Run tests
        run: npm run test -- --coverage

cache: 'npm' évite de retélécharger toutes les dépendances à chaque run.

Accélérer les tests

Au début, ~10 minutes par run. Après optimisation, ~3 minutes.

Cache Next.js :

- name: Cache Next.js build
  uses: actions/cache@v3
  with:
    path: |
      ~/.npm
      .next/cache
    key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}

Jobs parallèles si les vérifications sont indépendantes :

jobs:
  lint:
    runs-on: ubuntu-latest
    steps: [...]

  test:
    runs-on: ubuntu-latest
    steps: [...]

  type-check:
    runs-on: ubuntu-latest
    steps: [...]

La durée totale = le job le plus lent, pas la somme.

Pièges rencontrés

Tests OK en local, KO sur Actions : sur une PR, checkout fusionne par défaut la branche cible — un commit temporaire absent en local peut tout casser. Solution :

- uses: actions/checkout@v4
  with:
    ref: ${{ github.head_ref }}

E2E lents : augmentez le timeout du job :

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15  # défaut : 6 min

Déploiement automatique

Vercel : le plus simple

Projet sur Vercel + repo GitHub connecté = déploiement à chaque push, presque sans config.

Pour n deployer qu’après les tests :

jobs:
  deploy:
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v4

      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'

Token Vercel + org/project ID dans les Secrets. Bonus : preview par PR avec URL de prévisualisation.

Serveur auto-hébergé

Plus de contrôle, un peu plus de travail : SSH depuis Actions.

ssh-keygen -t ed25519 -C "github-actions"

Clé publique dans ~/.ssh/authorized_keys, privée dans GitHub Secrets (SSH_PRIVATE_KEY).

jobs:
  deploy:
    runs-on: ubuntu-latest
    needs: test
    if: github.ref == 'refs/heads/main'

    steps:
      - name: Deploy to server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/my-nextjs-app
            git pull origin main
            npm install
            npm run build
            pm2 restart nextjs-app

Build ID identique sur plusieurs serveurs

Un seul build, puis distribution des artefacts :

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
      - run: npm ci
      - run: npm run build

      - name: Upload build artifacts
        uses: actions/upload-artifact@v3
        with:
          name: next-build
          path: |
            .next
            public

  deploy:
    runs-on: ubuntu-latest
    needs: build
    strategy:
      matrix:
        server: [server1, server2, server3]

    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v3
        with:
          name: next-build

      - name: Deploy to ${{ matrix.server }}
        uses: appleboy/scp-action@master
        with:
          host: ${{ secrets[format('{0}_HOST', matrix.server)] }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          source: ".next,public"
          target: "/var/www/my-nextjs-app"

Optimisation et bonnes pratiques

Quand le build échoue

Sans alerte, un push « réussi » peut masquer un déploiement raté. Slack par exemple :

      - name: Notify on failure
        if: failure()
        uses: 8398a7/action-slack@v3
        with:
          status: ${{ job.status }}
          text: 'Échec du déploiement — à vérifier'
          webhook_url: ${{ secrets.SLACK_WEBHOOK }}
          channel: '#deploy-notifications'

Branches : staging et production

develop → environnement de test, main → production :

on:
  push:
    branches:
      - main
      - develop

jobs:
  deploy-staging:
    if: github.ref == 'refs/heads/develop'
    runs-on: ubuntu-latest
    steps:
      - [...]

  deploy-production:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - [...]

Approbation manuelle pour la prod :

jobs:
  deploy-production:
    runs-on: ubuntu-latest
    environment:
      name: production
    steps: [...]

Rollback

Tag par déploiement, workflow manuel workflow_dispatch :

on:
  workflow_dispatch:
    inputs:
      tag:
        description: 'Tag de version pour le rollback'
        required: true

jobs:
  rollback:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.inputs.tag }}

      - name: Deploy
        [...]

Variables d’environnement

Secrets GitHub + injection au build :

- name: Build
  run: npm run build
  env:
    NEXT_PUBLIC_API_URL: ${{ secrets.API_URL }}
    DATABASE_URL: ${{ secrets.DATABASE_URL }}

Exemple complet

Configuration test + build + déploiement staging/prod (extrait représentatif — voir le dépôt pour la version complète) :

name: Next.js CI/CD

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
      - run: npm run type-check
      - run: npm run test -- --coverage

  build:
    runs-on: ubuntu-latest
    needs: test
    if: github.event_name == 'push'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '18'
      - uses: actions/cache@v3
        with:
          path: |
            ~/.npm
            .next/cache
          key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}
      - run: npm ci
      - run: npm run build
        env:
          NEXT_PUBLIC_API_URL: ${{ secrets.API_URL }}
      - uses: actions/upload-artifact@v3
        with:
          name: next-build
          path: |
            .next
            public
            package.json

  deploy-staging:
    needs: build
    if: github.ref == 'refs/heads/develop'
    # ...

  deploy-production:
    needs: build
    if: github.ref == 'refs/heads/main'
    environment:
      name: production
    # ...

Les PR ne font que les tests ; develop et main déclenchent build + déploiement selon la branche. Secrets typiques : API_URL, STAGING_HOST, PROD_HOST, SERVER_USER, SSH_PRIVATE_KEY, SLACK_WEBHOOK.

Conclusion

Passer du manuel à l’automatisé change vraiment le quotidien : plus de SSH répétitif, moins de tests oubliés, moins d’attente devant le terminal.

La mise en place GitHub Actions demande un peu de temps au départ, mais le retour est durable. Commencez par tests + build, ajoutez déploiement, notifications et rollback ensuite.

Un push, un café, la prod à jour — difficile de revenir en arrière. Créez votre premier fichier sous .github/workflows et regardez Actions tourner : vous comprendrez tout de suite.

Configuration complète CI/CD Next.js

De la création du workflow GitHub Actions aux étapes test, build et déploiement

⏱️ Estimated time: 2 hr

  1. 1

    Step 1: Créer le workflow GitHub Actions

    Créer .github/workflows/deploy.yml :
    ```yaml
    name: Deploy

    on:
    push:
    branches: [main]

    jobs:
    build-and-deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    - uses: actions/setup-node@v3
    with:
    node-version: '18'
    - run: npm install
    - run: npm run build
    - run: npm test
    ```

    Points clés :
    • Déclencheur : push sur main
    • Environnement : ubuntu-latest
    • Étapes : checkout → setup-node → install → build → test
  2. 2

    Step 2: Configurer les tests

    Ajouter les tests :
    ```yaml
    - name: Run tests
    run: npm test

    - name: Type check
    run: npm run type-check

    - name: Lint
    run: npm run lint
    ```

    Points clés :
    • Échec des tests = pas de déploiement
    • Types et lint pour la qualité
    • Plus de tests oubliés avant la prod
  3. 3

    Step 3: Configurer le build

    Build :
    ```yaml
    - name: Build
    run: npm run build
    env:
    NEXT_PUBLIC_API_URL: ${{ secrets.NEXT_PUBLIC_API_URL }}
    ```

    Cache :
    ```yaml
    - name: Cache dependencies
    uses: actions/cache@v3
    with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    ```

    Points clés : variables d'environnement, cache, vérifier les artefacts
  4. 4

    Step 4: Configurer le déploiement

    Déploiement SSH :
    ```yaml
    - name: Deploy to server
    uses: appleboy/ssh-action@master
    with:
    host: ${{ secrets.HOST }}
    username: ${{ secrets.USERNAME }}
    key: ${{ secrets.SSH_KEY }}
    script: |
    cd /path/to/app
    git pull
    npm install
    npm run build
    pm2 restart app
    ```

    Multi-serveurs : build unique dans Actions, puis distribution des artefacts — build ID cohérent, pas de hard refresh côté utilisateur.

FAQ

Pourquoi adopter le CI/CD ?
Douleurs du manuel : répétition sur chaque serveur, redémarrage oublié, tests sautés, build ID différents → hard refresh. Le CI/CD automatise tests, build et déploiement à chaque push, réduit les erreurs et unifie le build ID. Exemples réels : serveur non mis à jour derrière le load balancer, déploiement sans tests avec bug en prod.
Comment configurer GitHub Actions ?
Créer .github/workflows/deploy.yml avec on.push sur main, jobs sur ubuntu-latest, étapes checkout → setup-node → install → build → test. Configurer les Secrets (HOST, USERNAME, SSH_KEY, etc.) dans Settings → Secrets. Commencer simple, puis enrichir.
Comment éviter des build ID différents sur plusieurs serveurs ?
Ne pas builder sur chaque serveur. Builder une fois dans GitHub Actions, uploader les artefacts (upload-artifact), les télécharger et déployer sur chaque machine (scp/ssh). Build ID identique, pas de hard refresh intempestif.
Comment configurer les étapes de test ?
Ajouter npm test, npm run type-check et npm run lint dans le job test. Échec = blocage du déploiement. Augmenter la couverture progressivement.
Comment notifier l'équipe après un déploiement ?
Slack : action 8398a7/action-slack@v3 avec webhook_url en Secret. Email possible avec dawidd6/action-send-mail@v3. Notifier succès et échec, avec version et horodatage si possible.
Quelles bonnes pratiques CI/CD ?
Approche progressive : 1) tests + build, 2) déploiement, 3) notifications et rollback. Suivre les logs, optimiser la durée de build, viser un bon taux de tests. Le CI/CD est un processus continu, pas un one-shot.

7 min de lecture · Publié le: 20 déc. 2025 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog