CI/CD GitHub Actions pour SaaS Next.js : pipeline complet en 1 fichier
Construire un pipeline CI/CD pro pour un SaaS Next.js — typecheck, lint, tests Vitest, Playwright, Prisma migrate, deploy Vercel. Un seul .yml, ~120 lignes.
Pourquoi GitHub Actions plutôt que les alternatives
CircleCI, GitLab CI, Buildkite... Tous fonctionnent. Mais pour un SaaS Next.js déployé sur Vercel, GitHub Actions a 3 avantages décisifs :
- Intégration native GitHub → status checks dans les PRs, pas de webhook à configurer
- Free tier généreux → 2000 minutes/mois sur repos privés, illimité sur public
- Marketplace riche → 90% des actions dont vous avez besoin existent déjà
Pour un SaaS early-stage, GH Actions couvre 100% des besoins CI/CD sans payer un cent.
L'objectif du pipeline
Quand un dev push une branche ou ouvre une PR, on veut :
- ✅ Typecheck — aucune erreur TypeScript
- ✅ Lint — code propre selon ESLint
- ✅ Format check — Prettier respecté
- ✅ Tests unitaires — Vitest 100% pass
- ✅ Tests sécurité — IDOR, RBAC, signatures HMAC
- ✅ Prisma validate — schéma cohérent
- ✅ Build — Next.js compile sans erreur
- ✅ E2E — Playwright sur les flows critiques (sur PRs main only)
- ✅ Deploy — Vercel auto sur merge main
Le tout en parallèle quand possible, < 5 minutes au total.
Le pipeline complet
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
env:
NODE_VERSION: "20"
PNPM_VERSION: "9"
jobs:
install:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Generate Prisma client
run: pnpm prisma generate
# Cache node_modules + generated/ pour les jobs suivants
- uses: actions/cache/save@v4
with:
path: |
node_modules
apps/web/generated
key: deps-${{ github.sha }}
typecheck:
needs: install
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
- uses: actions/cache/restore@v4
with:
path: |
node_modules
apps/web/generated
key: deps-${{ github.sha }}
- run: pnpm typecheck
lint:
needs: install
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
- uses: actions/cache/restore@v4
with:
path: |
node_modules
apps/web/generated
key: deps-${{ github.sha }}
- run: pnpm lint
- run: pnpm format:check
test:
needs: install
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
- uses: actions/cache/restore@v4
with:
path: |
node_modules
apps/web/generated
key: deps-${{ github.sha }}
- run: pnpm test
- run: pnpm test:security
build:
needs: [typecheck, lint, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
- uses: actions/cache/restore@v4
with:
path: |
node_modules
apps/web/generated
key: deps-${{ github.sha }}
- run: pnpm build
env:
# Variables d'environnement minimales pour build
DATABASE_URL: "postgresql://fake:fake@localhost:5432/fake"
NEXTAUTH_SECRET: "fake-secret-for-build-only"
NEXTAUTH_URL: "http://localhost:3000"
SKIP_ENV_VALIDATION: "1"Le pattern critique : factoriser le setup
Vous voyez la duplication ? Chaque job répète :
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
- uses: actions/cache/restore@v4C'est volontaire et c'est nécessaire. Chaque job tourne sur sa propre VM dans GitHub Actions — pas de partage de state entre jobs. Le cache que install a écrit est lu par les autres jobs.
Pour éviter la duplication, utilisez une composite action locale :
# .github/actions/setup/action.yml
name: Setup
description: Setup Node, pnpm, install deps
runs:
using: composite
steps:
- uses: pnpm/action-setup@v4
with:
version: "9"
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: pnpm
- uses: actions/cache/restore@v4
with:
path: |
node_modules
apps/web/generated
key: deps-${{ github.sha }}Puis dans chaque job :
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- run: pnpm typecheck3 lignes au lieu de 12 par job. La maintenance est centralisée.
Tests E2E Playwright sur PRs
Les tests Playwright sont lents (3-5min). Vous ne voulez pas les lancer sur chaque commit, mais sur les PRs vers main :
e2e:
if: github.event_name == 'pull_request'
needs: build
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: heartco_test
ports: [5432:5432]
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup
- name: Install Playwright browsers
run: pnpm exec playwright install --with-deps chromium
- name: Run migrations on test DB
run: pnpm prisma migrate deploy
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/heartco_test
- name: Seed test data
run: pnpm tsx scripts/seed-e2e.ts
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/heartco_test
- name: Run Playwright tests
run: pnpm exec playwright test
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/heartco_test
NEXTAUTH_SECRET: test-secret
NEXTAUTH_URL: http://localhost:3000
- uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 7Vercel deploy — automatique mais sécurisé
Vercel détecte les push GitHub et déploie automatiquement. Vous n'avez rien à câbler dans le workflow.
Par contre, configurez Vercel pour ne déployer en production que quand CI passe :
// vercel.json
{
"git": {
"deploymentEnabled": {
"main": true
}
},
"github": {
"enabled": true,
"silent": false,
"autoJobCancelation": true
}
}Dans Vercel → Settings → Git :
- ✅ "Require status checks before deploying" → cochez
typecheck,test,build - ✅ "Production Branch" →
mainuniquement
Résultat : un push qui casse le typecheck n'atteint jamais la production.
Variables d'environnement et secrets
Trois niveaux :
- Public dans le repo (env vars du workflow) → versions de Node, pnpm
- Secrets GitHub (Settings → Secrets → Actions) → tokens API, DB URL test
- Secrets Vercel (Vercel Dashboard → env vars) → production secrets
- run: pnpm test:integration
env:
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
STRIPE_TEST_KEY: ${{ secrets.STRIPE_TEST_KEY }}Performance — Faire passer le pipeline sous 3 minutes
Astuces qui ont marché pour nous :
- Cache des deps (déjà fait dans le pipeline) → -45s par job
- Turbo cache distant →
pnpm turbo build --remote-cacheavec Vercel Remote Cache → -30s - Parallélisation maximale → typecheck/lint/test en parallèle, pas en série
- Skip des jobs non concernés → utilisez
paths-filter:
jobs:
changes:
runs-on: ubuntu-latest
outputs:
web: ${{ steps.filter.outputs.web }}
marketing: ${{ steps.filter.outputs.marketing }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
web:
- 'apps/web/**'
- 'packages/**'
marketing:
- 'apps/marketing/**'
test-web:
needs: changes
if: needs.changes.outputs.web == 'true'
# ... ne tourne que si apps/web/ a changé- Fail fast → ajoutez
fail-fast: falseà false dans la matrice si vous voulez voir tous les échecs, ou laissez le défaut pour économiser des minutes.
Erreurs courantes
❌ Mettre npm install à la place de pnpm install --frozen-lockfile → CI plus lente et non déterministe.
❌ Oublier --frozen-lockfile → la CI installe des versions différentes du dev. Bug en prod garanti.
❌ Lancer les tests avec une vraie DB en parallèle → race conditions inter-jobs. Utilisez des bases isolées (services postgres dédiés par job).
❌ Pas de timeout → un test bloqué peut consommer 6h de CI. Ajoutez timeout-minutes: 10 à chaque job.
❌ Workflows qui se déclenchent en boucle → si un workflow push sur le repo, il peut redéclencher un autre workflow. Ajoutez if: github.actor != 'github-actions[bot]' pour briser le cycle.
Monitoring et alertes
Pour être notifié quand le CI casse sur main :
- name: Notify Slack on failure
if: failure() && github.ref == 'refs/heads/main'
uses: slackapi/slack-github-action@v1
with:
webhook: ${{ secrets.SLACK_WEBHOOK }}
payload: |
{
"text": "🚨 CI failed on main",
"blocks": [{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*CI failed:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|run #${{ github.run_number }}>"
}
}]
}Conclusion
Un bon pipeline CI/CD n'est pas une science compliquée — c'est de l'hygiène : ne pas merger ce qui casse, déployer ce qui passe, alerter ce qui foire. 120 lignes de YAML couvrent tous ces besoins pour un SaaS Next.js.
HeartCo Starter inclut ce pipeline par défaut, avec composite actions, cache Turbo, et hooks Vercel. Vous clonez, vous push, et vous avez la CI verte (ou rouge, mais alors c'est qu'il y a une vraie raison).
Articles connexes
Quel boilerplate SaaS Next.js choisir en 2026 ? (comparatif complet)
ShipFast, MakerKit, SupaStarter, HeartCo... Le marché des boilerplates SaaS Next.js explose. Voici comment choisir en fonction de votre projet, votre stack et votre budget.
LireDéployer un SaaS Next.js en production sur Vercel : checklist complète
De la configuration des variables d'environnement au monitoring post-deploy — tout ce qu'il faut vérifier avant de mettre votre SaaS en production.
LireNext.js 15 + tRPC + Prisma : le trio gagnant pour votre SaaS
Setup pas à pas du trio technique le plus productif pour un SaaS en 2026 : App Router, type safety end-to-end, et ORM moderne.
Lire