Construire un Pipeline CI/CD Complet avec GitHub Actions pour Next.js

Équipe Noqta
Par Équipe Noqta ·

Chargement du lecteur de synthèse vocale...

À chaque push, vous croisez les doigts en espérant que rien ne casse en production. C'est un pari. Le CI/CD élimine ce pari.

Dans ce tutoriel, vous allez construire un pipeline GitHub Actions complet pour une application Next.js. À la fin, chaque push déclenchera automatiquement le linting, les tests unitaires, les tests E2E avec Playwright, et le déploiement sur Vercel — uniquement si tout passe.

Plus de QA manuel. Plus de « ça marche sur ma machine ». Juste de la confiance à chaque commit.

Ce que vous allez construire

Un pipeline CI/CD multi-étapes qui :

  1. Vérifie votre code avec ESLint à chaque push
  2. Valide les types avec le compilateur TypeScript
  3. Exécute les tests unitaires avec Vitest
  4. Lance les tests E2E avec Playwright
  5. Déploie sur Vercel uniquement quand tout passe
  6. Met en cache les dépendances pour des builds rapides
  7. Poste des commentaires de statut sur les Pull Requests

Prérequis

Avant de commencer, assurez-vous d'avoir :

  • Un projet Next.js 14+ (App Router recommandé)
  • Un dépôt GitHub pour votre projet
  • Un compte Vercel connecté au projet
  • Node.js 20+ installé localement
  • Une connaissance de base de la syntaxe YAML

💡 Pas encore de projet Next.js ? Lancez npx create-next-app@latest my-app --typescript --tailwind --eslint --app pour en créer un.

Étape 1 : Préparer la structure du projet

D'abord, installons les bons outils de test.

# Installer les dépendances de test
npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/jest-dom
 
# Installer Playwright pour les tests E2E
npm install -D @playwright/test
npx playwright install --with-deps chromium

Créez le fichier de configuration Vitest :

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'
import path from 'path'
 
export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
    setupFiles: ['./tests/setup.ts'],
    include: ['**/*.test.{ts,tsx}'],
    coverage: {
      reporter: ['text', 'json-summary', 'html'],
      exclude: ['node_modules/', '.next/', 'tests/e2e/'],
    },
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './'),
    },
  },
})

Créez le fichier de setup des tests :

// tests/setup.ts
import '@testing-library/jest-dom'

Et la configuration Playwright :

// playwright.config.ts
import { defineConfig, devices } from '@playwright/test'
 
export default defineConfig({
  testDir: './tests/e2e',
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: process.env.CI ? 'github' : 'html',
  use: {
    baseURL: 'http://localhost:3000',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],
  webServer: {
    command: 'npm run build && npm run start',
    url: 'http://localhost:3000',
    reuseExistingServer: !process.env.CI,
    timeout: 120_000,
  },
})

Étape 2 : Écrire des tests de démonstration

Avant de câbler le CI/CD, créons les tests que le pipeline va exécuter.

Test unitaire

// tests/unit/home.test.tsx
import { render, screen } from '@testing-library/react'
import { describe, it, expect } from 'vitest'
import Home from '@/app/page'
 
describe('Page d\'accueil', () => {
  it('affiche le titre principal', () => {
    render(<Home />)
    const heading = screen.getByRole('heading', { level: 1 })
    expect(heading).toBeInTheDocument()
  })
 
  it('contient un lien de démarrage', () => {
    render(<Home />)
    const link = screen.getByRole('link', { name: /get started/i })
    expect(link).toBeInTheDocument()
  })
})

Test E2E

// tests/e2e/navigation.spec.ts
import { test, expect } from '@playwright/test'
 
test.describe('Navigation', () => {
  test('la page d\'accueil se charge correctement', async ({ page }) => {
    await page.goto('/')
    await expect(page).toHaveTitle(/Next.js/)
    await expect(page.locator('h1')).toBeVisible()
  })
 
  test('la page respecte la hiérarchie des titres', async ({ page }) => {
    await page.goto('/')
    const headings = await page.locator('h1, h2, h3').all()
    expect(headings.length).toBeGreaterThan(0)
  })
})

Mettre à jour les scripts package.json

{
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "type-check": "tsc --noEmit",
    "test": "vitest run",
    "test:watch": "vitest",
    "test:coverage": "vitest run --coverage",
    "test:e2e": "playwright test",
    "test:e2e:ui": "playwright test --ui"
  }
}

Étape 3 : Créer le workflow GitHub Actions

C'est ici que la magie opère. Créez le fichier de workflow :

# .github/workflows/ci.yml
name: CI/CD Pipeline
 
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]
 
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
 
env:
  NODE_VERSION: '20'
  PNPM_VERSION: '9'
 
jobs:
  # ─── Étape 1 : Qualité du code ──────────────────────
  lint:
    name: 🧹 Lint & Vérification des types
    runs-on: ubuntu-latest
    steps:
      - name: Récupérer le code
        uses: actions/checkout@v4
 
      - name: Configurer pnpm
        uses: pnpm/action-setup@v4
        with:
          version: ${{ env.PNPM_VERSION }}
 
      - name: Configurer Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'
 
      - name: Installer les dépendances
        run: pnpm install --frozen-lockfile
 
      - name: Lancer ESLint
        run: pnpm lint
 
      - name: Vérification des types TypeScript
        run: pnpm type-check
 
  # ─── Étape 2 : Tests unitaires ──────────────────────
  unit-tests:
    name: 🧪 Tests unitaires
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - name: Récupérer le code
        uses: actions/checkout@v4
 
      - name: Configurer pnpm
        uses: pnpm/action-setup@v4
        with:
          version: ${{ env.PNPM_VERSION }}
 
      - name: Configurer Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'
 
      - name: Installer les dépendances
        run: pnpm install --frozen-lockfile
 
      - name: Lancer les tests avec couverture
        run: pnpm test:coverage
 
      - name: Uploader le rapport de couverture
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: coverage-report
          path: coverage/
          retention-days: 7
 
  # ─── Étape 3 : Tests E2E ────────────────────────────
  e2e-tests:
    name: 🎭 Tests E2E
    runs-on: ubuntu-latest
    needs: lint
    steps:
      - name: Récupérer le code
        uses: actions/checkout@v4
 
      - name: Configurer pnpm
        uses: pnpm/action-setup@v4
        with:
          version: ${{ env.PNPM_VERSION }}
 
      - name: Configurer Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'
 
      - name: Installer les dépendances
        run: pnpm install --frozen-lockfile
 
      - name: Installer les navigateurs Playwright
        run: pnpm exec playwright install --with-deps chromium
 
      - name: Construire l'application
        run: pnpm build
 
      - name: Lancer les tests E2E
        run: pnpm test:e2e
 
      - name: Uploader le rapport Playwright
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 7
 
      - name: Uploader les captures d'écran
        uses: actions/upload-artifact@v4
        if: failure()
        with:
          name: test-screenshots
          path: test-results/
          retention-days: 7
 
  # ─── Étape 4 : Déploiement ──────────────────────────
  deploy:
    name: 🚀 Déployer sur Vercel
    runs-on: ubuntu-latest
    needs: [unit-tests, e2e-tests]
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment:
      name: production
      url: ${{ steps.deploy.outputs.url }}
    steps:
      - name: Récupérer le code
        uses: actions/checkout@v4
 
      - name: Configurer pnpm
        uses: pnpm/action-setup@v4
        with:
          version: ${{ env.PNPM_VERSION }}
 
      - name: Configurer Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'
 
      - name: Installer les dépendances
        run: pnpm install --frozen-lockfile
 
      - name: Installer Vercel CLI
        run: pnpm add -g vercel
 
      - name: Récupérer l'environnement Vercel
        run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
 
      - name: Construire avec Vercel
        run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
 
      - name: Déployer sur Vercel
        id: deploy
        run: |
          url=$(vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }})
          echo "url=$url" >> $GITHUB_OUTPUT

Voici ce que fait chaque étape :

ÉtapeDéclencheurFonctionBloque le déploiement ?
LintChaque push & PRESLint + vérification TypeScriptOui
Tests unitairesAprès le lintVitest + rapport de couvertureOui
Tests E2EAprès le lint (en parallèle)Tests navigateur PlaywrightOui
DéploiementAprès tous les tests, main uniquementDéploiement production Vercel

🚀 Besoin d'aide pour configurer le CI/CD de votre projet ? Noqta construit des applications web de qualité production avec des pipelines de test et déploiement automatisés intégrés dès le départ.

Étape 4 : Configurer les secrets GitHub

Votre workflow a besoin d'accéder à Vercel. Voici comment configurer les secrets :

  1. Allez dans votre dépôt GitHub → SettingsSecrets and variablesActions
  2. Ajoutez ces secrets :
SecretComment l'obtenir
VERCEL_TOKENDashboard Vercel → Créer un token
VERCEL_ORG_IDLancez vercel link localement → vérifiez .vercel/project.json
VERCEL_PROJECT_IDMême fichier que ci-dessus
# Méthode rapide pour obtenir les identifiants Vercel
vercel link
cat .vercel/project.json

Vous verrez quelque chose comme :

{
  "orgId": "team_xxxxxxxxxxxx",
  "projectId": "prj_xxxxxxxxxxxx"
}

Copiez ces valeurs dans vos secrets GitHub.

Étape 5 : Ajouter des commentaires de statut sur les PR

Rendez votre pipeline convivial en publiant les résultats directement sur les Pull Requests :

  # ─── Commentaire PR avec résultats ───────────────────
  pr-comment:
    name: 📝 Statut PR
    runs-on: ubuntu-latest
    needs: [unit-tests, e2e-tests]
    if: github.event_name == 'pull_request'
    permissions:
      pull-requests: write
    steps:
      - name: Télécharger le rapport de couverture
        uses: actions/download-artifact@v4
        with:
          name: coverage-report
          path: coverage/
 
      - name: Commenter sur la PR
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            let coverageSummary = 'Rapport de couverture non disponible';
 
            try {
              const coverage = JSON.parse(
                fs.readFileSync('coverage/coverage-summary.json', 'utf8')
              );
              const total = coverage.total;
              coverageSummary = `
              | Métrique | Couverture |
              |----------|-----------|
              | Instructions | ${total.statements.pct}% |
              | Branches | ${total.branches.pct}% |
              | Fonctions | ${total.functions.pct}% |
              | Lignes | ${total.lines.pct}% |`;
            } catch (e) {
              console.log('Impossible de lire la couverture:', e.message);
            }
 
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: `## ✅ Pipeline CI réussi
 
            ### Couverture des tests
            ${coverageSummary}
 
            ### Résumé du pipeline
            - 🧹 Lint & Types : ✅
            - 🧪 Tests unitaires : ✅
            - 🎭 Tests E2E : ✅
 
            > _Automatisé par GitHub Actions_`
            });

Étape 6 : Ajouter des règles de protection de branche

Le pipeline est inutile si les développeurs peuvent le contourner. Verrouillez-le :

  1. Allez dans SettingsBranchesAdd rule
  2. Pattern de nom de branche : main
  3. Activez :
    • ✅ Exiger une Pull Request avant la fusion
    • ✅ Exiger que les vérifications de statut passent avant la fusion
    • ✅ Exiger que les branches soient à jour avant la fusion
  4. Ajoutez les vérifications requises :
    • 🧹 Lint & Vérification des types
    • 🧪 Tests unitaires
    • 🎭 Tests E2E

Plus personne ne peut pusher directement sur main sans passer toutes les vérifications.

Étape 7 : Optimiser avec le cache

Le workflow utilise déjà le cache pnpm via actions/setup-node. Mais pour les navigateurs Playwright et les builds Next.js, ajoutez un cache explicite :

      # Ajoutez au job e2e-tests avant "Installer les navigateurs Playwright"
      - name: Mettre en cache les navigateurs Playwright
        uses: actions/cache@v4
        id: playwright-cache
        with:
          path: ~/.cache/ms-playwright
          key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}
 
      - name: Installer les navigateurs Playwright
        if: steps.playwright-cache.outputs.cache-hit != 'true'
        run: pnpm exec playwright install --with-deps chromium
 
      # Ajoutez à tout job qui lance `next build`
      - name: Mettre en cache le build Next.js
        uses: actions/cache@v4
        with:
          path: .next/cache
          key: nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('**/*.ts', '**/*.tsx') }}
          restore-keys: |
            nextjs-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml') }}-

Avec le cache, votre pipeline passe de ~4 minutes à moins de 2 minutes sur les exécutions suivantes.

Étape 8 : Déploiements par environnement

Pour un vrai projet, vous voulez des déploiements de prévisualisation sur les PRs et des déploiements de production sur main :

  # ─── Déploiement Preview (PRs) ──────────────────────
  deploy-preview:
    name: 🔍 Déploiement Preview
    runs-on: ubuntu-latest
    needs: [unit-tests, e2e-tests]
    if: github.event_name == 'pull_request'
    environment:
      name: preview
      url: ${{ steps.deploy.outputs.url }}
    steps:
      - name: Récupérer le code
        uses: actions/checkout@v4
 
      - name: Configurer pnpm
        uses: pnpm/action-setup@v4
        with:
          version: ${{ env.PNPM_VERSION }}
 
      - name: Configurer Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: 'pnpm'
 
      - name: Installer les dépendances
        run: pnpm install --frozen-lockfile
 
      - name: Installer Vercel CLI
        run: pnpm add -g vercel
 
      - name: Déploiement Preview
        id: deploy
        run: |
          vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
          vercel build --token=${{ secrets.VERCEL_TOKEN }}
          url=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})
          echo "url=$url" >> $GITHUB_OUTPUT
 
      - name: Commenter l'URL preview sur la PR
        uses: actions/github-script@v7
        with:
          script: |
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: `🔍 **Preview déployé :** ${{ steps.deploy.outputs.url }}`
            });

Étape 9 : Surveiller la santé du pipeline

Ajoutez un badge de statut à votre README :

[![CI/CD Pipeline](https://github.com/YOUR_ORG/YOUR_REPO/actions/workflows/ci.yml/badge.svg)](https://github.com/YOUR_ORG/YOUR_REPO/actions/workflows/ci.yml)

Et configurez les notifications en cas d'échec :

  # ─── Notification en cas d'échec ─────────────────────
  notify:
    name: 🔔 Notification
    runs-on: ubuntu-latest
    needs: [lint, unit-tests, e2e-tests, deploy]
    if: failure()
    steps:
      - name: Envoyer notification Slack
        uses: slackapi/slack-github-action@v2
        with:
          webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
          webhook-type: incoming-webhook
          payload: |
            {
              "text": "❌ Pipeline CI/CD échoué",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "❌ *Pipeline échoué* sur `${{ github.ref_name }}`\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Voir l'exécution>"
                  }
                }
              ]
            }

💡 Vous voulez un pipeline de test et déploiement solide sans le construire vous-même ? L'équipe QA de Noqta met en place des pipelines CI/CD de qualité production avec monitoring, pour que votre équipe déploie en confiance.

Erreurs courantes et solutions

1. « Espace disque insuffisant » sur les runners GitHub

Les builds Next.js peuvent être volumineux. Libérez de l'espace :

      - name: Libérer de l'espace disque
        run: |
          sudo rm -rf /usr/share/dotnet
          sudo rm -rf /usr/local/lib/android

2. Tests E2E qui expirent

Augmentez le timeout Playwright et ajoutez des tentatives :

// playwright.config.ts
export default defineConfig({
  timeout: 60_000,
  expect: { timeout: 10_000 },
  retries: process.env.CI ? 2 : 0,
})

3. Tests instables qui bloquent les déploiements

Utilisez toPass de Playwright pour les vérifications éventuellement cohérentes :

await expect(async () => {
  const response = await page.request.get('/api/health')
  expect(response.status()).toBe(200)
}).toPass({ timeout: 30_000 })

4. Installation lente des dépendances

Utilisez toujours --frozen-lockfile et le cache pnpm :

      - name: Configurer pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 9
      - name: Configurer Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'pnpm'

Schéma complet du pipeline

Push / PR
    │
    ▼
┌──────────────────┐
│  🧹 Lint &       │
│  Vérif. types    │
└────────┬─────────┘
         │
    ┌────┴────┐
    ▼         ▼
┌─────────┐ ┌─────────┐
│🧪 Unit. │ │🎭 E2E   │
└───┬─────┘ └───┬─────┘
    │           │
    └─────┬─────┘
          │
          ▼
    ┌──────────┐        ┌───────────┐
    │  PR ?    │──oui──▶│🔍 Preview │
    └────┬─────┘        └───────────┘
         │non (main)
         ▼
    ┌───────────┐
    │🚀 Déploi- │
    │ement prod │
    └───────────┘

Résumé

Voici ce que vous avez construit :

ComposantOutilObjectif
LintingESLint + TypeScriptDétecter les erreurs avant l'exécution
Tests unitairesVitest + Testing LibraryTester les composants isolément
Tests E2EPlaywrightTester les parcours utilisateur réels
CI/CDGitHub ActionsTout automatiser
DéploiementVercel CLIDéploiements sans interruption
CacheGitHub Actions CachePipelines 50%+ plus rapides
Protection de brancheParamètres GitHubImposer les contrôles qualité

Le pipeline complet s'exécute en moins de 3 minutes avec le cache. Chaque PR obtient un déploiement de prévisualisation. Chaque merge sur main déclenche le déploiement en production — mais uniquement après que tous les tests passent.

Arrêtez de déployer manuellement. Arrêtez de prier pour que votre code fonctionne. Automatisez.

Prochaines étapes

  • Ajoutez des tests de régression visuelle avec les captures Playwright
  • Configurez les migrations de base de données dans le pipeline pour les apps full-stack
  • Intégrez Lighthouse CI pour le monitoring de performance à chaque PR
  • Implémentez des feature flags pour les déploiements progressifs

Le meilleur pipeline CI/CD est celui que vous configurez une fois et en qui vous avez confiance pour toujours. Maintenant, vous l'avez.


Vous voulez lire plus de tutoriels? Découvrez notre dernier tutoriel sur Construire des API REST prêtes pour la production avec FastAPI, PostgreSQL et Docker.

Discutez de votre projet avec nous

Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.

Trouvons les meilleures solutions pour vos besoins.

Articles connexes