À 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 :
- Vérifie votre code avec ESLint à chaque push
- Valide les types avec le compilateur TypeScript
- Exécute les tests unitaires avec Vitest
- Lance les tests E2E avec Playwright
- Déploie sur Vercel uniquement quand tout passe
- Met en cache les dépendances pour des builds rapides
- 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 --apppour 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 chromiumCré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_OUTPUTVoici ce que fait chaque étape :
| Étape | Déclencheur | Fonction | Bloque le déploiement ? |
|---|---|---|---|
| Lint | Chaque push & PR | ESLint + vérification TypeScript | Oui |
| Tests unitaires | Après le lint | Vitest + rapport de couverture | Oui |
| Tests E2E | Après le lint (en parallèle) | Tests navigateur Playwright | Oui |
| Déploiement | Après tous les tests, main uniquement | Dé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 :
- Allez dans votre dépôt GitHub → Settings → Secrets and variables → Actions
- Ajoutez ces secrets :
| Secret | Comment l'obtenir |
|---|---|
VERCEL_TOKEN | Dashboard Vercel → Créer un token |
VERCEL_ORG_ID | Lancez vercel link localement → vérifiez .vercel/project.json |
VERCEL_PROJECT_ID | Même fichier que ci-dessus |
# Méthode rapide pour obtenir les identifiants Vercel
vercel link
cat .vercel/project.jsonVous 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 :
- Allez dans Settings → Branches → Add rule
- Pattern de nom de branche :
main - 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
- 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 :
[](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/android2. 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 :
| Composant | Outil | Objectif |
|---|---|---|
| Linting | ESLint + TypeScript | Détecter les erreurs avant l'exécution |
| Tests unitaires | Vitest + Testing Library | Tester les composants isolément |
| Tests E2E | Playwright | Tester les parcours utilisateur réels |
| CI/CD | GitHub Actions | Tout automatiser |
| Déploiement | Vercel CLI | Déploiements sans interruption |
| Cache | GitHub Actions Cache | Pipelines 50%+ plus rapides |
| Protection de branche | Paramètres GitHub | Imposer 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.