Nehos Groupe

Ce qu'il faut retenir

Un pipeline CI/CD GitHub Actions pour Next.js B2B se décompose en quatre phases : vérification statique (lint, type-check), tests unitaires et E2E Playwright avec sharding, déploiement conditionnel (Vercel preview ou production, OVH/AWS via Docker), puis monitoring post-déploiement avec rollback automatique.

Le cache GitHub Actions combiné au remote caching Turborepo permet de réduire les temps de pipeline de 70 à 85% sur les projets de taille significative — la différence entre 14 minutes et 2 minutes 30 pour une PR classique.

La sécurité dans le pipeline ne se limite pas à stocker les secrets dans GitHub Secrets : SAST automatisé (CodeQL ou Semgrep), SBOM généré à chaque build, Dependabot configuré par ecosystem, et rotation des tokens de déploiement sont des pratiques non négociables sur un projet B2B en production.

CI/CD GitHub Actions Next.js 2026 : pipeline de déploiement complet

Par Chokri Siala, CTO Nehos — lint, type-check, Playwright E2E shardé, déploiement Vercel et OVH, secrets SAST, cache Turborepo. Le pipeline production-ready que nous appliquons sur tous nos projets B2B.

Adapté à toute taille de structure

Artisan
Startup
PME / TPE
ETI
Grand Groupe
C
Chokri Siala
··next-headless

#Pourquoi un pipeline CI/CD est indispensable pour les projets Next.js B2B

En 2026, livrer un projet Next.js sans pipeline CI/CD est aussi risqué que de déployer en production sans tests. Pourtant, dans les projets B2B que nous auditons chez Nehos, un tiers arrivent encore avec des déploiements manuels : git pull && npm run build && pm2 restart sur le serveur. La dette technique qui s'accumule autour de cette pratique est considérable.

Les enjeux d'un pipeline CI/CD pour Next.js en contexte B2B vont bien au-delà de l'automatisation du déploiement. Ils touchent à la qualité du code (lint, type-check, tests automatisés à chaque PR), à la sécurité (détection des vulnérabilités avant production), à la vélocité de l'équipe (feedback en 3 minutes plutôt que 45), et à la confiance des parties prenantes (chaque déploiement est tracé, réversible, documenté).

Les chiffres parlent d'eux-mêmes sur nos projets B2B Next.js :

  • Réduction des bugs en production : -62% sur les 3 mois suivant la mise en place d'un pipeline complet
  • Temps de détection d'une régression : de 2-3 jours (détection manuelle) à moins de 8 minutes (pipeline automatisé)
  • Fréquence de déploiement : ×4 après mise en place du CI/CD (le frein psychologique du déploiement manuel disparaît)
  • Temps de rollback : de 25-40 minutes (rollback manuel) à moins de 90 secondes (rollback automatisé)

Ce guide présente le pipeline exact que nous déployons sur les projets Next.js B2B premium chez Nehos — avec les fichiers YAML complets, les justifications de chaque choix, et les pièges à éviter.


#GitHub Actions : concepts clés, workflows, runners, secrets

Avant de présenter le pipeline, un rappel des concepts fondamentaux de GitHub Actions que nous utilisons systématiquement.

#Workflows, jobs et steps

Un workflow est un fichier YAML dans .github/workflows/. Il contient un ou plusieurs jobs, qui s'exécutent en parallèle par défaut (ou en séquence si needs: est déclaré). Chaque job contient des steps : commandes shell ou actions réutilisables.

Nous structurons systématiquement nos workflows en trois fichiers séparés :

  • .github/workflows/ci.yml : lint, type-check, tests — déclenché sur chaque PR et push sur main
  • .github/workflows/deploy-preview.yml : déploiement preview — déclenché sur chaque PR
  • .github/workflows/deploy-production.yml : déploiement production — déclenché uniquement sur merge dans main

Cette séparation permet de voir en un coup d'œil ce qui a échoué, et de ré-exécuter uniquement la phase concernée sans relancer tout le pipeline.

#Runners : ubuntu-latest vs self-hosted

Pour la majorité des projets B2B Next.js, les runners hébergés GitHub (ubuntu-latest) sont suffisants. Ils coûtent 0,008 $ par minute pour les dépôts privés. Sur un projet avec 20 PRs/semaine et un pipeline de 6 minutes, le coût mensuel est inférieur à à partir de 960 €.

Les runners self-hosted deviennent pertinents dans deux cas : contraintes de souveraineté (le code ne doit pas quitter l'infrastructure de l'entreprise) ou besoins de performances exceptionnelles (builds très longs sur des monorepos de grande taille). Pour les projets sur cloud souverain OVH, nous configurons des runners auto-hébergés sur des instances OVH.

#Secrets et variables d'environnement

GitHub Actions distingue trois niveaux de secrets :

  • Repository secrets : accessibles à tous les workflows du repo
  • Environment secrets : accessibles uniquement aux workflows associés à un environment (staging, production)
  • Organization secrets : partagés entre plusieurs repos de l'organisation

Bonne pratique absolue : les tokens de déploiement production (VERCEL_TOKEN, KUBECONFIG, OVH_APPLICATION_KEY) doivent être des environment secrets avec protection de branche — ils ne sont accessibles qu'après approbation manuelle d'un reviewer désigné.

# Exemple de protection d'environment dans le workflow
jobs:
  deploy-production:
    runs-on: ubuntu-latest
    environment: production  # Déclenche la règle d'approbation
    steps:
      - name: Deploy to Vercel Production
        env:
          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}  # Secret d'environment
        run: vercel --prod --token $VERCEL_TOKEN

#Pipeline de base : lint, type-check, tests unitaires, build Next.js

Voici le workflow CI complet que nous utilisons comme base sur tous nos projets Next.js B2B.

# .github/workflows/ci.yml
name: CI

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

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  ci:
    name: Lint, Type-check, Tests, Build
    runs-on: ubuntu-latest
    timeout-minutes: 20

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Nécessaire pour Turborepo affected

      - name: Setup pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Lint
        run: pnpm turbo lint --filter='...[origin/main]'
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

      - name: Type-check
        run: pnpm turbo typecheck --filter='...[origin/main]'
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

      - name: Unit tests
        run: pnpm turbo test --filter='...[origin/main]'
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

      - name: Build
        run: pnpm turbo build --filter='...[origin/main]'
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}
          NEXT_PUBLIC_API_URL: ${{ vars.NEXT_PUBLIC_API_URL_STAGING }}

      - name: Upload coverage
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: coverage-reports
          path: '**/coverage/**'
          retention-days: 7

#La directive cancel-in-progress: true

La directive concurrency avec cancel-in-progress: true annule automatiquement le run précédent quand un nouveau push arrive sur la même branche. Sur une PR active avec plusieurs commits rapprochés, cela évite de faire tourner 5 pipelines en parallèle et de saturer les minutes GitHub Actions.

#Le --filter='...[origin/main]'

Combinée avec le remote caching Turborepo, la syntaxe --filter='...[origin/main]' ne traite que les packages modifiés depuis main et leurs dépendants. Sur un monorepo avec 3 apps et 8 packages, une PR qui touche uniquement l'app marketing ne relancera pas le build et les tests de portail-client et back-office. Ce seul paramètre explique une large part des gains de performance CI mesurés sur nos projets Turborepo monorepo.


#Tests E2E avec Playwright en CI : sharding, cache, rapports

Playwright est l'outil de référence pour les tests E2E en 2026 — son support natif des navigateurs modernes, la génération de code, et surtout le sharding natif en font le choix évident pour les pipelines CI à fort volume de tests. Nous avons détaillé les raisons de ce choix dans notre comparatif Playwright vs Cypress.

#Sharding : paralléliser les tests sur plusieurs runners

Le sharding Playwright divise la suite de tests en N fragments exécutés en parallèle sur N runners GitHub Actions. Sur 200 tests E2E qui prenaient 22 minutes en séquence, le sharding sur 4 runners les ramène à 6 minutes.

# .github/workflows/e2e.yml
name: E2E Tests

on:
  pull_request:
    branches: [main]

jobs:
  e2e:
    name: Playwright E2E (shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
    runs-on: ubuntu-latest
    timeout-minutes: 30

    strategy:
      fail-fast: false
      matrix:
        shardIndex: [1, 2, 3, 4]
        shardTotal: [4]

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Install Playwright browsers
        run: pnpm exec playwright install --with-deps chromium

      - name: Build app for E2E
        run: pnpm turbo build --filter=marketing
        env:
          NEXT_PUBLIC_API_URL: ${{ vars.NEXT_PUBLIC_API_URL_STAGING }}

      - name: Run Playwright tests (shard)
        run: pnpm exec playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
        env:
          BASE_URL: http://localhost:3000
          CI: true

      - name: Upload blob report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: blob-report-${{ matrix.shardIndex }}
          path: playwright-report/
          retention-days: 1

  merge-reports:
    name: Merge Playwright Reports
    runs-on: ubuntu-latest
    needs: e2e
    if: always()

    steps:
      - name: Checkout
        uses: actions/checkout@v4

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

      - name: Download blob reports
        uses: actions/download-artifact@v4
        with:
          path: all-blob-reports
          pattern: blob-report-*
          merge-multiple: true

      - name: Merge reports
        run: npx playwright merge-reports --reporter=html ./all-blob-reports

      - name: Upload HTML report
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 14

#Cache des navigateurs Playwright

L'installation des navigateurs Playwright prend 1 à 2 minutes. Pour l'éviter à chaque run, on cache le répertoire des navigateurs :

- name: Cache Playwright browsers
  uses: actions/cache@v4
  with:
    path: ~/.cache/ms-playwright
    key: playwright-${{ runner.os }}-${{ hashFiles('**/package-lock.json', '**/pnpm-lock.yaml') }}
    restore-keys: |
      playwright-${{ runner.os }}-

Ce cache économise 90 à 120 secondes par run — sur 60 PRs par mois, c'est 1 à 2 heures de CI économisées. La qualité du code et les standards automatisés passent par ce genre d'optimisation incrémentale.


#Déploiement sur Vercel via GitHub Actions : preview + production

Vercel est la plateforme de déploiement de référence pour Next.js, et son intégration GitHub Actions est mature. Nous distinguons deux workflows : preview (pour les PRs) et production (pour les merges sur main).

#Déploiement preview sur chaque PR

# .github/workflows/deploy-preview.yml
name: Deploy Preview

on:
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  deploy-preview:
    name: Deploy to Vercel Preview
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9

      - name: Install Vercel CLI
        run: npm install --global vercel@latest

      - name: Pull Vercel environment
        run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}

      - name: Build project
        run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

      - name: Deploy to Vercel Preview
        id: deploy
        run: |
          DEPLOY_URL=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})
          echo "deploy_url=$DEPLOY_URL" >> $GITHUB_OUTPUT

      - name: Comment PR with preview URL
        uses: actions/github-script@v7
        with:
          script: |
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `**Preview prête** : ${{ steps.deploy.outputs.deploy_url }}\n\nDéployée depuis le commit \`${{ github.sha }}\`.`
            })

#Déploiement production avec approbation obligatoire

# .github/workflows/deploy-production.yml
name: Deploy Production

on:
  push:
    branches: [main]

jobs:
  deploy-production:
    name: Deploy to Vercel Production
    runs-on: ubuntu-latest
    environment: production

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9

      - name: Install Vercel CLI
        run: npm install --global vercel@latest

      - name: Pull Vercel environment
        run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}

      - name: Build project
        run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
        env:
          TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
          TURBO_TEAM: ${{ vars.TURBO_TEAM }}

      - name: Deploy to Vercel Production
        id: deploy
        run: |
          DEPLOY_URL=$(vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }})
          echo "deploy_url=$DEPLOY_URL" >> $GITHUB_OUTPUT

      - name: Notify Slack
        uses: slackapi/slack-github-action@v2
        with:
          payload: |
            {"text": "Production déployée : ${{ steps.deploy.outputs.deploy_url }} (commit ${{ github.sha }})"}
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

L'environment: production dans le job déclenche la règle de protection configurée dans les paramètres GitHub : au moins un reviewer désigné doit approuver manuellement avant que le déploiement production ne démarre. Cette mécanique est le filet de sécurité minimal pour tout projet B2B en production.


→ Vous évaluez vos options ? Utilisez notre estimateur de budget en ligne pour obtenir une fourchette en 2 minutes, ou consultez nos tarifs détaillés.

#Déploiement OVHcloud/AWS : artefacts Docker, Kubernetes manifests

Pour les projets qui ne peuvent pas ou ne veulent pas utiliser Vercel — souvent pour des raisons de souveraineté, de coût à grande échelle, ou d'exigences infrastructure spécifiques — nous déployons sur microservices Kubernetes hébergés sur OVHcloud ou AWS EKS.

#Build et push de l'image Docker

# Extrait de .github/workflows/deploy-production.yml (variante Kubernetes)
jobs:
  build-and-push:
    name: Build Docker image and push
    runs-on: ubuntu-latest
    environment: production

    outputs:
      image_tag: ${{ steps.meta.outputs.version }}

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to OVH registry
        uses: docker/login-action@v3
        with:
          registry: ${{ vars.OVH_REGISTRY_URL }}
          username: ${{ secrets.OVH_REGISTRY_USER }}
          password: ${{ secrets.OVH_REGISTRY_PASSWORD }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ vars.OVH_REGISTRY_URL }}/nehos/app-marketing
          tags: |
            type=sha,prefix=,suffix=,format=short
            type=raw,value=latest,enable={{is_default_branch}}

      - name: Build and push
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./apps/marketing/Dockerfile
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            NEXT_PUBLIC_API_URL=${{ vars.NEXT_PUBLIC_API_URL_PROD }}

#Dockerfile optimisé pour Next.js

Le Dockerfile multi-stage qui réduit l'image finale de 1,2 Go à environ 180 Mo :

# Dockerfile (multi-stage)
FROM node:22-alpine AS base
RUN corepack enable

FROM base AS deps
WORKDIR /app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY apps/marketing/package.json ./apps/marketing/
COPY packages/ ./packages/
RUN pnpm install --frozen-lockfile

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN pnpm turbo build --filter=marketing

FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder --chown=nextjs:nodejs /app/apps/marketing/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/apps/marketing/.next/static ./apps/marketing/.next/static
COPY --from=builder --chown=nextjs:nodejs /app/apps/marketing/public ./apps/marketing/public
USER nextjs
EXPOSE 3000
CMD ["node", "apps/marketing/server.js"]

#Déploiement sur Kubernetes avec rollout

  deploy-kubernetes:
    name: Deploy to Kubernetes (OVH)
    runs-on: ubuntu-latest
    needs: build-and-push
    environment: production

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure kubectl
        uses: azure/k8s-set-context@v4
        with:
          method: kubeconfig
          kubeconfig: ${{ secrets.KUBECONFIG_OVH_PROD }}

      - name: Update image tag in manifest
        run: |
          sed -i "s|IMAGE_TAG|${{ needs.build-and-push.outputs.image_tag }}|g" \
            k8s/marketing-deployment.yaml

      - name: Apply Kubernetes manifests
        run: kubectl apply -f k8s/marketing-deployment.yaml

      - name: Wait for rollout
        run: kubectl rollout status deployment/marketing --timeout=300s

#Sécurité dans le pipeline : secrets management, SAST, SBOM, Dependabot

La sécurité d'un pipeline CI/CD est un sujet à part entière — et souvent le parent pauvre des projets. Nous appliquons systématiquement les recommandations OWASP DevSecOps sur tous nos pipelines.

#SAST avec CodeQL

CodeQL est l'outil d'analyse statique de sécurité natif de GitHub. Il analyse le code TypeScript/JavaScript à la recherche de vulnérabilités connues (injections, XSS, mauvaise gestion des secrets) à chaque PR.

# .github/workflows/security.yml
name: Security Analysis

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 2 * * 1'  # Chaque lundi à 2h

jobs:
  codeql:
    name: CodeQL Analysis
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: read
      security-events: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Initialize CodeQL
        uses: github/codeql-action/init@v3
        with:
          languages: javascript-typescript
          queries: security-and-quality

      - name: Autobuild
        uses: github/codeql-action/autobuild@v3

      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v3
        with:
          category: /language:javascript-typescript

#Génération du SBOM à chaque build

Le Software Bill of Materials (SBOM) est un inventaire de toutes les dépendances de l'application. Il est requis par le DORA Act et les nouvelles exigences de conformité pour les applications B2B traitant des données sensibles.

      - name: Generate SBOM
        uses: anchore/sbom-action@v0
        with:
          path: .
          format: spdx-json
          output-file: sbom.spdx.json

      - name: Upload SBOM
        uses: actions/upload-artifact@v4
        with:
          name: sbom-${{ github.sha }}
          path: sbom.spdx.json
          retention-days: 90

#Dependabot : mises à jour automatiques par ecosystem

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: npm
    directory: /
    schedule:
      interval: weekly
      day: monday
      time: '09:00'
    timezone: Europe/Paris
    groups:
      nextjs-ecosystem:
        patterns: ["next", "react", "react-dom", "@types/react*"]
      testing:
        patterns: ["@playwright/test", "vitest", "@testing-library*"]
    open-pull-requests-limit: 10

  - package-ecosystem: docker
    directory: /
    schedule:
      interval: weekly

#Rotation automatique des tokens

Les tokens de déploiement (Vercel, Docker registry, Kubernetes) doivent être rotés régulièrement. Nous configurons des alertes GitHub Actions qui déclenchent une notification Slack 7 jours avant l'expiration d'un secret (pour les secrets avec date d'expiration configurée). Pour les tokens sans expiration, une rotation trimestrielle est planifiée dans le calendrier d'équipe — et documentée dans le runbook du projet.


#Cache et optimisation : GitHub Actions cache, remote caching Turborepo

Un pipeline CI/CD lent est un pipeline que les développeurs évitent ou ignorent. L'optimisation du cache est la première étape pour rendre le CI agréable à utiliser. La combinaison Turborepo remote caching + cache GitHub Actions peut réduire les temps de pipeline de 70 à 85%.

#Stratégie de cache multicouche

# Couche 1 : dépendances pnpm
- name: Setup Node.js avec cache pnpm
  uses: actions/setup-node@v4
  with:
    node-version: 22
    cache: pnpm  # Cache automatique via setup-node

# Couche 2 : navigateurs Playwright
- name: Cache Playwright browsers
  uses: actions/cache@v4
  with:
    path: ~/.cache/ms-playwright
    key: playwright-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
    restore-keys: playwright-${{ runner.os }}-

# Couche 3 : cache Docker layers (via buildx)
- name: Build and push (avec cache GHA)
  uses: docker/build-push-action@v6
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max

# Couche 4 : remote caching Turborepo
# Activé via TURBO_TOKEN + TURBO_TEAM sur toutes les commandes turbo

#Remote caching Turborepo : configuration complète

Le remote caching Turborepo stocke les artefacts de build (résultats de lint, type-check, test, build) sur des serveurs distants. Quand un autre développeur ou un autre runner CI exécute la même tâche sur le même code, Turborepo restaure le résultat depuis le cache au lieu de réexécuter la tâche.

# Configuration initiale (une seule fois)
npx turbo login
npx turbo link

# Variables d'environnement CI nécessaires
# TURBO_TOKEN : token d'accès Vercel ou self-hosted cache
# TURBO_TEAM  : identifiant de l'équipe (ex: "nehos")
# TURBO_API   : URL du cache si self-hosted (sinon Vercel par défaut)

#Self-hosted cache avec OVH Object Storage

Pour les projets avec exigences de souveraineté, nous déployons turborepo-remote-cache sur OVH :

# Configuration turbo.json pour self-hosted cache
# Dans la commande CI :
# TURBO_API=https://cache.votre-domaine.com
# TURBO_TOKEN=votre-token-self-hosted

Le serveur turborepo-remote-cache est compatible avec l'API S3 d'OVH Object Storage. La mise en place prend moins d'une heure, et les performances sont comparables au cache Vercel pour les projets hébergés en Europe.

#Mesures de performance sur nos projets B2B

Phase du pipelineSans cacheAvec cache (premier run)Avec cache (run suivant)
pnpm install3 min 20s45s (cache dépendances)45s
turbo lint2 min 10s2 min 10s (cold)8s (remote cache)
turbo typecheck1 min 50s1 min 50s (cold)6s (remote cache)
turbo build5 min 40s5 min 40s (cold)55s (remote cache)
Playwright (4 shards)22 min6 min (sharding)6 min
Total pipeline~35 min~16 min~8 min

Le gain de 35 minutes à 8 minutes n'est pas uniquement lié au cache — c'est la combinaison du sharding Playwright, du cache des dépendances, et du remote caching Turborepo. La productivité des développeurs est directement corrélée à la vitesse du feedback CI.


#Monitoring post-déploiement : alertes, rollback automatique

Un pipeline CI/CD ne s'arrête pas au déploiement. Le monitoring post-déploiement — et la capacité à revenir en arrière rapidement en cas de problème — est la dernière couche d'un pipeline mature.

#Health check post-déploiement

      - name: Health check post-deploy
        run: |
          echo "Waiting for deployment to stabilize..."
          for i in {1..12}; do
            STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://votre-app.com/api/health)
            if [ "$STATUS" = "200" ]; then
              echo "Health check OK (attempt $i)"
              exit 0
            fi
            echo "Health check failed (attempt $i, status $STATUS), retrying in 10s..."
            sleep 10
          done
          echo "Health check failed after 12 attempts"
          exit 1

#Rollback automatique sur Vercel

Vercel conserve les 10 derniers déploiements accessibles instantanément. En cas d'échec du health check, le rollback se fait via l'API Vercel :

      - name: Rollback on failure
        if: failure()
        run: |
          PREVIOUS_DEPLOYMENT=$(vercel ls --token=${{ secrets.VERCEL_TOKEN }} \
            --json | jq -r '.[1].uid')
          vercel rollback $PREVIOUS_DEPLOYMENT \
            --token=${{ secrets.VERCEL_TOKEN }}
          echo "Rolled back to deployment: $PREVIOUS_DEPLOYMENT"

#Rollback sur Kubernetes

Sur un cluster Kubernetes, le rollback utilise la commande native kubectl rollout undo :

      - name: Rollback Kubernetes deployment
        if: failure()
        run: |
          kubectl rollout undo deployment/marketing
          kubectl rollout status deployment/marketing --timeout=120s
          echo "Rollback completed"

#Alertes Slack et PagerDuty

Nous configurons systématiquement des alertes sur trois niveaux :

  1. Alerte pipeline : notification Slack sur chaque échec CI/CD (webhook Slack ou action slackapi/slack-github-action)
  2. Alerte déploiement : notification Slack + PagerDuty sur échec de déploiement production
  3. Alerte monitoring continu : via Core Web Vitals et alertes Vercel Analytics — dégradation de performance post-déploiement détectée en moins de 5 minutes
      - name: Alert on failure
        if: failure()
        uses: slackapi/slack-github-action@v2
        with:
          payload: |
            {
              "text": ":red_circle: Pipeline échoué sur `${{ github.repository }}` — branche `${{ github.ref_name }}`",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "*Échec* : <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Voir le run CI>"
                  }
                }
              ]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

#Métriques DORA dans le pipeline

Les métriques DORA (Deployment Frequency, Lead Time for Changes, Change Failure Rate, Time to Restore Service) peuvent être calculées automatiquement en exploitant les événements GitHub Actions. Nous utilisons un job dédié qui alimente un dashboard interne après chaque déploiement production réussi — donnant aux équipes une visibilité en temps réel sur leur maturité DevOps. La refonte B2B avec Nehos inclut systématiquement la mise en place de ces métriques.

Questions & Réponses

Questions fréquentes sur CI/CD GitHub Actions Next.js

Pour un dépôt privé, GitHub Actions facture 0,008 $ par minute sur runner ubuntu-latest. Sur un projet Next.js typique avec 20 PRs par semaine et un pipeline de 8 minutes optimisé (cache + Turborepo), le coût mensuel est inférieur à à partir de 1 280 €. Les dépôts publics sont gratuits. Pour les organisations avec des volumes importants (50+ PRs/semaine ou pipelines > 15 minutes), les runners self-hosted sur une VM OVH à à partir de 40 960 €/mois deviennent économiquement plus intéressants — mais ils ajoutent une charge de maintenance. Notre recommandation : commencez avec les runners hébergés GitHub, migrez vers self-hosted uniquement quand la facture GitHub Actions dépasse à partir de 800 €/mois.
La bonne pratique est d'utiliser les GitHub Environments : créez un environment 'staging' et un environment 'production' dans les paramètres du dépôt. Chaque environment a ses propres secrets (VERCEL_TOKEN, NEXT_PUBLIC_API_URL, KUBECONFIG, etc.) et ses propres règles de protection. Pour production, configurez 'Required reviewers' — un reviewer désigné doit approuver manuellement avant que le job de déploiement ne démarre. Les secrets d'environment ne sont pas accessibles aux jobs qui ne déclarent pas `environment: production` dans leur configuration. Cette séparation garantit que le token de déploiement production ne peut jamais fuiter dans un pipeline de PR.
Les deux, mais avec une granularité différente. Sur chaque PR : lancez une suite E2E réduite (smoke tests — 20 à 30 tests sur les parcours critiques) avec 2 shards maximum. Ce feedback en 5-6 minutes est actionnable rapidement. Avant le merge en production (sur push dans main) : lancez la suite E2E complète avec 4 shards. Ce pipeline peut prendre 10 à 15 minutes mais couvre l'intégralité des cas. Sur les projets Nehos, nous configurons aussi les tests E2E contre la preview Vercel (URL réelle, pas localhost) pour les PRs importantes — ce qui détecte des problèmes d'intégration que le localhost ne révèle pas.
Vercel détecte nativement les monorepos et crée un projet par app. Dans GitHub Actions, la stratégie recommandée est d'utiliser `turbo run build --filter='...[HEAD^1]'` pour identifier les apps affectées, puis de déclencher un déploiement conditionnel pour chaque app. Concrètement, vous créez une matrice de déploiement ou des jobs séparés avec une condition `if: contains(needs.detect-changes.outputs.affected, 'marketing')`. Chaque app a son propre `VERCEL_PROJECT_ID` stocké comme variable d'environment. Cette approche garantit que modifier uniquement l'app `portail-client` ne re-déploie pas le site `marketing` — et donc évite des déploiements inutiles et des risques de régression.
CodeQL (GitHub) est natif dans GitHub Actions, gratuit pour les dépôts publics et payant (ou inclus dans GitHub Advanced Security) pour les dépôts privés d'entreprise. Il est très précis sur les vulnérabilités TypeScript/JavaScript et s'intègre directement dans les PR reviews via les alertes de sécurité GitHub. Semgrep est un outil open-source avec un large catalogue de règles communautaires et une version cloud freemium. Il est plus flexible (vous pouvez écrire vos propres règles métier), légèrement plus rapide, mais avec un peu plus de faux positifs. Notre choix : CodeQL comme outil principal sur les projets avec GitHub Advanced Security (la majorité de nos clients entreprise), Semgrep en complément pour les règles personnalisées. Sur les projets sans GitHub Advanced Security, Semgrep seul est suffisant.
Plusieurs stratégies cumulables : (1) Configurez `timeout-minutes: 20` sur chaque job pour éviter les runs bloqués indéfiniment — sans timeout, un job peut tourner 6 heures et consommer toutes vos minutes CI. (2) Utilisez le remote caching Turborepo pour que les builds lents ne se produisent qu'une fois (le cache restaure ensuite). (3) Divisez les jobs longs en jobs parallèles — lint, typecheck et tests unitaires peuvent tourner en parallèle sur des jobs séparés. (4) Utilisez `cancel-in-progress: true` dans la directive `concurrency` pour annuler les runs obsolètes. (5) Si un build Next.js spécifique dépasse systématiquement 10 minutes, analysez avec `NEXT_TELEMETRY_DISABLED=1 next build --debug` pour identifier les modules lents — souvent des imports de bibliothèques volumineuses non tree-shakées.
Le remote caching Vercel chiffre les artefacts en transit (TLS) et au repos, et les artefacts ne sont accessibles qu'aux membres de l'équipe avec le bon token. Pour les projets avec des exigences de conformité strictes (données de santé, financières, données classifiées), nous déployons `turborepo-remote-cache` en self-hosted sur l'infrastructure du client — OVH, Outscale, ou dans leur datacenter. Le code source ne quitte jamais l'infrastructure : seuls les artefacts de build (fichiers `.js` compilés, résultats de lint) sont mis en cache. Une vérification importante : listez les `outputs` dans `turbo.json` pour vous assurer qu'aucun fichier contenant des secrets (`.env` générés, fichiers de config) ne fait partie des artefacts cachés.
Réserver un audit