Avance 13 min de lecture · 2 820 mots

Intégration CI/CD pour WordPress avec GitHub Actions

Estimated reading time: 14 minutes

Introduction : DevOps Moderne pour WordPress

En 2025, le déploiement manuel de WordPress est devenu obsolète pour les équipes professionnelles. L’intégration continue (CI) et le déploiement continu (CD) sont désormais des standards de l’industrie, garantissant qualité, fiabilité et rapidité des livraisons.

GitHub Actions offre une plateforme CI/CD native, gratuite pour les projets open source, et parfaitement intégrée à l’écosystème Git. Cet article présente une architecture complète de pipeline CI/CD pour WordPress, incluant tests automatisés, analyses de sécurité, et déploiements multi-environnements.

Architecture du Pipeline CI/CD

Vue d’Ensemble

┌─────────────┐
│   Git Push  │
└──────┬──────┘
       │
       ▼
┌────────────────────────────────────────────┐
│         GitHub Actions Workflow            │
├────────────────────────────────────────────┤
│  1. Validation & Qualité du Code          │
│     - PHP CodeSniffer (WPCS)              │
│     - PHPStan (Analyse statique)          │
│     - ESLint/Stylelint                    │
├────────────────────────────────────────────┤
│  2. Tests Automatisés                     │
│     - Unit Tests (PHPUnit)                │
│     - Integration Tests                   │
│     - E2E Tests (Playwright)              │
├────────────────────────────────────────────┤
│  3. Analyse de Sécurité                   │
│     - WPScan                              │
│     - Dependency Check                    │
│     - OWASP ZAP                           │
├────────────────────────────────────────────┤
│  4. Build & Packaging                     │
│     - Composer install --no-dev           │
│     - npm run build                       │
│     - Asset optimization                  │
├────────────────────────────────────────────┤
│  5. Deployment                            │
│     - Development (auto)                  │
│     - Staging (auto on main)              │
│     - Production (manual approval)        │
└────────────────────────────────────────────┘

Configuration du Workflow GitHub Actions

Workflow Principal : CI/CD Complet

# .github/workflows/wordpress-ci-cd.yml
name: WordPress CI/CD Pipeline

on:
  push:
    branches: [ main, develop, 'feature/' ]
  pullrequest:
    branches: [ main, develop ]
  workflowdispatch:
    inputs:
      environment:
        description: 'Target environment'
        required: true
        default: 'staging'
        type: choice
        options:
          - development
          - staging
          - production

env:
  PHPVERSION: '8.2'
  NODEVERSION: '20'
  WPVERSION: 'latest'

jobs:
  # Job 1: Code Quality & Linting
  code-quality:
    name: Code Quality Analysis
    runs-on: ubuntu-latest

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

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ env.PHPVERSION }}
          extensions: mbstring, xml, ctype, iconv, intl, pdomysql, dom, filter, gd, json
          coverage: none
          tools: composer:v2

      - name: Get Composer Cache Directory
        id: composer-cache
        run: echo "dir=$(composer config cache-files-dir)" >> $GITHUBOUTPUT

      - name: Cache Composer dependencies
        uses: actions/cache@v4
        with:
          path: ${{ steps.composer-cache.outputs.dir }}
          key: ${{ runner.os }}-composer-${{ hashFiles('/composer.lock') }}
          restore-keys: ${{ runner.os }}-composer-

      - name: Install Composer dependencies
        run: composer install --prefer-dist --no-progress --no-suggest

      - name: WordPress Coding Standards
        run: |
          composer require --dev wp-coding-standards/wpcs
          vendor/bin/phpcs --config-set installedpaths vendor/wp-coding-standards/wpcs
          vendor/bin/phpcs --standard=WordPress plugins/ themes/ --extensions=php

      - name: PHPStan Static Analysis
        run: |
          composer require --dev phpstan/phpstan
          vendor/bin/phpstan analyse plugins/ themes/ --level=5

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODEVERSION }}
          cache: 'npm'

      - name: Install Node dependencies
        run: npm ci

      - name: ESLint JavaScript
        run: npm run lint:js

      - name: Stylelint CSS
        run: npm run lint:css

  # Job 2: Unit & Integration Tests
  tests:
    name: Automated Tests
    runs-on: ubuntu-latest
    needs: code-quality

    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQLROOTPASSWORD: root
          MYSQLDATABASE: wordpresstest
        ports:
          - 3306:3306
        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3

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

      - name: Setup PHP with Xdebug
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ env.PHPVERSION }}
          extensions: mbstring, xml, ctype, iconv, intl, pdomysql, dom, filter, gd, json
          coverage: xdebug
          tools: composer:v2

      - name: Cache Composer dependencies
        uses: actions/cache@v4
        with:
          path: vendor
          key: ${{ runner.os }}-composer-${{ hashFiles('/composer.lock') }}
          restore-keys: ${{ runner.os }}-composer-

      - name: Install Composer dependencies
        run: composer install --prefer-dist --no-progress

      - name: Install WordPress Test Suite
        run: |
          bash bin/install-wp-tests.sh wordpresstest root root 127.0.0.1 ${{ env.WPVERSION }}

      - name: Run PHPUnit Tests
        run: |
          vendor/bin/phpunit --coverage-clover=coverage.xml --coverage-text

      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@v4
        with:
          file: ./coverage.xml
          flags: unittests
          name: codecov-umbrella

      - name: Integration Tests
        run: |
          vendor/bin/phpunit --testsuite=integration

  # Job 3: E2E Tests
  e2e-tests:
    name: End-to-End Tests
    runs-on: ubuntu-latest
    needs: tests

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

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODEVERSION }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Install Playwright Browsers
        run: npx playwright install --with-deps

      - name: Start WordPress with Docker
        run: |
          docker-compose -f docker-compose.test.yml up -d
          sleep 30

      - name: Run Playwright Tests
        run: npx playwright test

      - name: Upload Playwright Report
        uses: actions/upload-artifact@v4
        if: always()
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 30

      - name: Cleanup
        if: always()
        run: docker-compose -f docker-compose.test.yml down

  # Job 4: Security Scanning
  security:
    name: Security Analysis
    runs-on: ubuntu-latest
    needs: code-quality

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

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ env.PHPVERSION }}
          tools: composer:v2

      - name: Install Composer dependencies
        run: composer install --prefer-dist --no-progress

      - name: Check for known security vulnerabilities
        run: |
          composer audit

      - name: WPScan Vulnerability Database Check
        run: |
          docker run --rm 
            -v $(pwd):/app 
            wpscanteam/wpscan 
            --url http://localhost 
            --enumerate p,t,u 
            --plugins-detection mixed 
            --api-token ${{ secrets.WPSCANAPITOKEN }}

      - name: OWASP Dependency-Check
        uses: dependency-check/Dependency-CheckAction@main
        with:
          project: 'WordPress Project'
          path: '.'
          format: 'HTML'

      - name: Upload Dependency-Check Report
        uses: actions/upload-artifact@v4
        with:
          name: dependency-check-report
          path: reports/

  # Job 5: Build Assets
  build:
    name: Build & Package
    runs-on: ubuntu-latest
    needs: [tests, e2e-tests, security]
    if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop'

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

      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ env.PHPVERSION }}
          tools: composer:v2

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ env.NODEVERSION }}
          cache: 'npm'

      - name: Install production dependencies
        run: |
          composer install --no-dev --optimize-autoloader --prefer-dist --no-progress
          npm ci --production

      - name: Build frontend assets
        run: |
          npm run build

      - name: Optimize images
        run: |
          npm run optimize:images

      - name: Create deployment archive
        run: |
          mkdir -p artifacts
          tar -czf artifacts/wordpress-build-${{ github.sha }}.tar.gz 
            --exclude='.git' 
            --exclude='nodemodules' 
            --exclude='tests' 
            --exclude='.github' 
            --exclude='.md' 
            .

      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: wordpress-build
          path: artifacts/
          retention-days: 30

  # Job 6: Deploy to Development
  deploy-development:
    name: Deploy to Development
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/develop'
    environment:
      name: development
      url: https://dev.example.com

    steps:
      - name: Download build artifact
        uses: actions/download-artifact@v4
        with:
          name: wordpress-build

      - name: Deploy to Development Server
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.DEVHOST }}
          username: ${{ secrets.DEVUSER }}
          key: ${{ secrets.DEVSSHKEY }}
          port: ${{ secrets.DEVPORT }}
          script: |
            set -e
            cd /var/www/development

            # Backup actuel
            tar -czf backups/backup-$(date +%Y%m%d-%H%M%S).tar.gz wp-content/plugins wp-content/themes

            # Maintenance mode
            wp maintenance-mode activate

            # Déploiement
            tar -xzf ~/wordpress-build-${{ github.sha }}.tar.gz -C /tmp/wordpress-deploy
            rsync -av --delete /tmp/wordpress-deploy/plugins/ wp-content/plugins/
            rsync -av --delete /tmp/wordpress-deploy/themes/ wp-content/themes/

            # Database migrations
            wp plugin activate custom-plugin

            # Clear cache
            wp cache flush
            wp rewrite flush

            # Maintenance mode off
            wp maintenance-mode deactivate

            # Cleanup
            rm -rf /tmp/wordpress-deploy

  # Job 7: Deploy to Staging
  deploy-staging:
    name: Deploy to Staging
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'
    environment:
      name: staging
      url: https://staging.example.com

    steps:
      - name: Download build artifact
        uses: actions/download-artifact@v4
        with:
          name: wordpress-build

      - name: Deploy via Deployer
        run: |
          composer global require deployer/deployer
          ~/.composer/vendor/bin/dep deploy staging 
            --tag=${{ github.sha }} 
            --host=${{ secrets.STAGINGHOST }} 
            --user=${{ secrets.STAGINGUSER }} 
            --identity-file=${{ secrets.STAGINGSSHKEY }}

      - name: Run smoke tests
        run: |
          curl -f https://staging.example.com || exit 1
          curl -f https://staging.example.com/wp-admin/ || exit 1

      - name: Notify Slack
        uses: slackapi/slack-github-action@v1.25.0
        with:
          webhook: ${{ secrets.SLACKWEBHOOK }}
          webhook-type: incoming-webhook
          payload: |
            {
              "text": "✅ Staging deployment successful - ${{ github.sha }}"
            }

  # Job 8: Deploy to Production
  deploy-production:
    name: Deploy to Production
    runs-on: ubuntu-latest
    needs: deploy-staging
    if: github.eventname == 'workflowdispatch' && github.event.inputs.environment == 'production'
    environment:
      name: production
      url: https://example.com

    steps:
      - name: Download build artifact
        uses: actions/download-artifact@v4
        with:
          name: wordpress-build

      - name: Blue-Green Deployment
        uses: appleboy/ssh-action@v1.0.0
        with:
          host: ${{ secrets.PRODHOST }}
          username: ${{ secrets.PRODUSER }}
          key: ${{ secrets.PRODSSHKEY }}
          port: ${{ secrets.PRODPORT }}
          script: |
            set -e

            # Déterminer l'environnement actif
            CURRENT=$(readlink /var/www/production/current)
            if [ "$CURRENT" = "/var/www/production/blue" ]; then
              TARGET="green"
              INACTIVE="blue"
            else
              TARGET="blue"
              INACTIVE="green"
            fi

            echo "Deploying to $TARGET environment"

            # Préparer le nouvel environnement
            mkdir -p /var/www/production/$TARGET
            tar -xzf ~/wordpress-build-${{ github.sha }}.tar.gz -C /var/www/production/$TARGET

            # Synchroniser wp-config.php et uploads
            cp /var/www/production/$INACTIVE/wp-config.php /var/www/production/$TARGET/
            rsync -av /var/www/production/$INACTIVE/wp-content/uploads/ /var/www/production/$TARGET/wp-content/uploads/

            # Database migrations
            cd /var/www/production/$TARGET
            wp plugin activate custom-plugin

            # Health check
            curl -f http://localhost:8080/$TARGET || exit 1

            # Switch symlink (atomic switch)
            ln -sfn /var/www/production/$TARGET /var/www/production/current-new
            mv -Tf /var/www/production/current-new /var/www/production/current

            # Reload PHP-FPM
            sudo systemctl reload php8.2-fpm

            # Clear cache
            wp cache flush

            echo "Deployment successful - active: $TARGET"

      - name: Post-deployment monitoring
        run: |
          # Attendre 2 minutes pour les métriques
          sleep 120

          # Vérifier les métriques (APM, logs, etc.)
          # Rollback si nécessaire

      - name: Notify team
        uses: slackapi/slack-github-action@v1.25.0
        with:
          webhook: ${{ secrets.SLACKWEBHOOK }}
          webhook-type: incoming-webhook
          payload: |
            {
              "text": "🚀 Production deployment successful",
              "blocks": [
                {
                  "type": "section",
                  "text": {
                    "type": "mrkdwn",
                    "text": "Production Deployment SuccessfulnnCommit: ${{ github.sha }}nAuthor: ${{ github.actor }}nURL: https://example.com"
                  }
                }
              ]
            }

Scripts de Déploiement avec Deployer

Configuration Deployer

tty', true);
set('keepreleases', 5);
set('sharedfiles', [
    'wp-config.php',
    '.env',
]);
set('shareddirs', [
    'wp-content/uploads',
]);
set('writabledirs', [
    'wp-content/uploads',
    'wp-content/cache',
]);
set('clearpaths', [
    '.git',
    '.github',
    'nodemodules',
    'tests',
    '.md',
    'composer.json',
    'composer.lock',
    'package.json',
    'package-lock.json',
]);

// Environnements
host('staging')
    ->setHostname('staging.example.com')
    ->setRemoteUser('deploy')
    ->setDeployPath('/var/www/staging')
    ->setPort(22)
    ->set('branch', 'main')
    ->set('httpuser', 'www-data')
    ->set('writablemode', 'chmod')
    ->set('writablechmodmode', '0755');

host('production')
    ->setHostname('example.com')
    ->setRemoteUser('deploy')
    ->setDeployPath('/var/www/production')
    ->setPort(22)
    ->set('branch', 'main')
    ->set('httpuser', 'www-data')
    ->set('writablemode', 'chmod')
    ->set('writablechmodmode', '0755');

// Tâches personnalisées

/
  Backup de la base de données
 /
task('wordpress:backup', function () {
    $timestamp = date('Y-m-dH-i-s');
    $backupFile = "backup{$timestamp}.sql.gz";

    within('{{deploypath}}/current', function () use ($backupFile) {
        run("wp db export - | gzip > {{deploypath}}/backups/{$backupFile}");
    });

    writeln("Database backup created: {$backupFile}");
})->desc('Backup WordPress database');

/
  Migrations de base de données
 /
task('wordpress:migrate', function () {
    within('{{releasepath}}', function () {
        run('wp plugin activate custom-plugin');
        run('wp custom-plugin migrate');
    });
})->desc('Run database migrations');

/
  Optimisation de la base de données
 /
task('wordpress:optimize', function () {
    within('{{releasepath}}', function () {
        run('wp db optimize');
        run('wp transient delete --all');
    });
})->desc('Optimize WordPress database');

/
  Clear des caches
 /
task('wordpress:cache:clear', function () {
    within('{{releasepath}}', function () {
        run('wp cache flush');
        run('wp rewrite flush');

        // Clear opcache
        run('php -r "opcachereset();"');

        // Clear Redis si disponible
        if (test('[ -f /usr/bin/redis-cli ]')) {
            run('redis-cli FLUSHALL');
        }
    });
})->desc('Clear all caches');

/
  Maintenance mode
 /
task('wordpress:maintenance:enable', function () {
    within('{{deploypath}}/current', function () {
        run('wp maintenance-mode activate');
    });
})->desc('Enable maintenance mode');

task('wordpress:maintenance:disable', function () {
    within('{{releasepath}}', function () {
        run('wp maintenance-mode deactivate');
    });
})->desc('Disable maintenance mode');

/
  Health check
 /
task('wordpress:health:check', function () {
    $url = get('url');

    $response = runLocally("curl -s -o /dev/null -w '%{httpcode}' {$url}");

    if ($response !== '200') {
        throw new Exception("Health check failed. HTTP status: {$response}");
    }

    writeln("Health check passed: {$url}");
})->desc('Perform health check');

/
  Rollback avec restauration de base de données
 /
task('wordpress:rollback:full', function () {
    // Rollback des fichiers
    invoke('rollback');

    // Restaurer le dernier backup de base de données
    within('{{deploypath}}/current', function () {
        $latestBackup = run('ls -t {{deploypath}}/backups/.sql.gz | head -1');

        if ($latestBackup) {
            writeln("Restoring database from: {$latestBackup}");
            run("gunzip < {$latestBackup} | wp db import -");
            writeln("Database restored successfully");
        }
    });
})->desc('Full rollback (files + database)');

/
  Workflow de déploiement principal
 /
task('deploy', [
    'deploy:prepare',
    'deploy:vendors',
    'wordpress:backup',
    'wordpress:maintenance:enable',
    'deploy:publish',
    'wordpress:migrate',
    'wordpress:optimize',
    'wordpress:cache:clear',
    'wordpress:maintenance:disable',
    'wordpress:health:check',
    'deploy:cleanup',
    'deploy:success',
])->desc('Deploy WordPress application');

// Hook de rollback
after('rollback', 'wordpress:cache:clear');

// Gestion des échecs
fail('deploy', 'deploy:failed');
fail('deploy', 'wordpress:maintenance:disable');

Tests Automatisés

Configuration PHPUnit





    
        
            ./tests/unit/
        
        
            ./tests/integration/
        
    

    
        
            ./plugins/
            ./themes/
        
        
            ./vendor/
            ./tests/
        
        
            
            
        
    

    
        
    

Script d’Installation WordPress Test Suite

#!/usr/bin/env bash
# bin/install-wp-tests.sh

if [ $# -lt 3 ]; then
    echo "usage: $0    [db-host] [wp-version] [skip-database-creation]"
    exit 1
fi

DBNAME=$1
DBUSER=$2
DBPASS=$3
DBHOST=${4-localhost}
WPVERSION=${5-latest}
SKIPDBCREATE=${6-false}

TMPDIR=${TMPDIR-/tmp}
TMPDIR=$(echo $TMPDIR | sed -e "s//$//")
WPTESTSDIR=${WPTESTSDIR-$TMPDIR/wordpress-tests-lib}
WPCOREDIR=${WPCOREDIR-$TMPDIR/wordpress/}

download() {
    if [ which curl ]; then
        curl -s "$1" > "$2";
    elif [ which wget ]; then
        wget -nv -O "$2" "$1"
    fi
}

if [[ $WPVERSION =~ ^[0-9]+.[0-9]+-(beta|RC) ]]; then
    WPBRANCH=${WPVERSION%-}
    WPTESTSTAG="branches/$WPBRANCH"
elif [[ $WPVERSION =~ ^[0-9]+.[0-9]+$ ]]; then
    WPTESTSTAG="branches/$WPVERSION"
elif [[ $WPVERSION =~ [0-9]+.[0-9]+.[0-9]+ ]]; then
    if [[ $WPVERSION =~ [0-9]+.[0-9]+.[0] ]]; then
        WPTESTSTAG="tags/${WPVERSION%??}"
    else
        WPTESTSTAG="tags/$WPVERSION"
    fi
elif [[ $WPVERSION == 'nightly' || $WPVERSION == 'trunk' ]]; then
    WPTESTSTAG="trunk"
else
    WPTESTSTAG="tags/$WPVERSION"
fi

installwp() {
    if [ -d $WPCOREDIR ]; then
        return;
    fi

    mkdir -p $WPCOREDIR

    if [[ $WPVERSION == 'nightly' || $WPVERSION == 'trunk' ]]; then
        mkdir -p $TMPDIR/wordpress-nightly
        download https://wordpress.org/nightly-builds/wordpress-latest.zip  $TMPDIR/wordpress-nightly/wordpress-nightly.zip
        unzip -q $TMPDIR/wordpress-nightly/wordpress-nightly.zip -d $TMPDIR/wordpress-nightly/
        mv $TMPDIR/wordpress-nightly/wordpress/ $WPCOREDIR
    else
        if [ $WPVERSION == 'latest' ]; then
            local ARCHIVENAME='latest'
        elif [[ $WPVERSION =~ [0-9]+.[0-9]+ ]]; then
            local ARCHIVENAME="wordpress-$WPVERSION"
        else
            local ARCHIVENAME="wordpress-$WPVERSION"
        fi
        download https://wordpress.org/${ARCHIVENAME}.tar.gz  $TMPDIR/wordpress.tar.gz
        tar --strip-components=1 -zxmf $TMPDIR/wordpress.tar.gz -C $WPCOREDIR
    fi

    download https://raw.githubusercontent.com/markoheijnen/wp-mysqli/master/db.php $WPCOREDIR/wp-content/db.php
}

installtestsuite() {
    if [ ! -d $WPTESTSDIR ]; then
        mkdir -p $WPTESTSDIR
        svn co --quiet https://develop.svn.wordpress.org/${WPTESTSTAG}/tests/phpunit/includes/ $WPTESTSDIR/includes
        svn co --quiet https://develop.svn.wordpress.org/${WPTESTSTAG}/tests/phpunit/data/ $WPTESTSDIR/data
    fi

    if [ ! -f wp-tests-config.php ]; then
        download https://develop.svn.wordpress.org/${WPTESTSTAG}/wp-tests-config-sample.php "$WPTESTSDIR"/wp-tests-config.php
        WPCOREDIRESCAPED=$(echo $WPCOREDIR | sed 's:/:/:g')
        sed -i "s:dirname( FILE ) . '/src/':'$WPCOREDIRESCAPED':" "$WPTESTSDIR"/wp-tests-config.php
        sed -i "s/youremptytestdbnamehere/$DBNAME/" "$WPTESTSDIR"/wp-tests-config.php
        sed -i "s/yourusernamehere/$DBUSER/" "$WPTESTSDIR"/wp-tests-config.php
        sed -i "s/yourpasswordhere/$DBPASS/" "$WPTESTSDIR"/wp-tests-config.php
        sed -i "s|localhost|${DBHOST}|" "$WPTESTSDIR"/wp-tests-config.php
    fi
}

installdb() {
    if [ ${SKIPDBCREATE} = "true" ]; then
        return 0
    fi

    PASSARG=""
    if [ ! -z $DBPASS ]; then
        PASSARG="-p$DBPASS"
    fi

    mysqladmin create $DBNAME --user="$DBUSER" $PASSARG --host="$DBHOST" --protocol=tcp
}

installwp
installtestsuite
installdb

Tests E2E avec Playwright

// tests/e2e/login.spec.ts
import { test, expect } from '@playwright/test';

test.describe('WordPress Login', () => {
  test('should login successfully with valid credentials', async ({ page }) => {
    await page.goto('/wp-admin');

    await page.fill('#userlogin', process.env.WPADMINUSER || 'admin');
    await page.fill('#userpass', process.env.WPADMINPASS || 'password');
    await page.click('#wp-submit');

    await expect(page).toHaveURL(/wp-admin/);
    await expect(page.locator('#wpadminbar')).toBeVisible();
  });

  test('should show error with invalid credentials', async ({ page }) => {
    await page.goto('/wp-admin');

    await page.fill('#userlogin', 'invalid');
    await page.fill('#userpass', 'wrong');
    await page.click('#wp-submit');

    await expect(page.locator('#loginerror')).toBeVisible();
    await expect(page.locator('#loginerror')).toContainText('Error');
  });
});

// tests/e2e/post-creation.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Post Creation', () => {
  test.beforeEach(async ({ page }) => {
    // Login avant chaque test
    await page.goto('/wp-admin');
    await page.fill('#userlogin', process.env.WPADMINUSER || 'admin');
    await page.fill('#userpass', process.env.WPADMINPASS || 'password');
    await page.click('#wp-submit');
  });

  test('should create a new post', async ({ page }) => {
    await page.goto('/wp-admin/post-new.php');

    // Titre du post
    await page.fill('h1[aria-label="Add title"]', 'Test Post E2E');

    // Contenu (Gutenberg)
    const editor = page.locator('[aria-label="Block: Paragraph"]').first();
    await editor.click();
    await editor.fill('This is a test post created via E2E testing.');

    // Publier
    await page.click('button:has-text("Publish")');
    await page.click('button:has-text("Publish")'); // Confirmation

    await expect(page.locator('.components-snackbar_content')).toContainText('published');
  });

  test('should save post as draft', async ({ page }) => {
    await page.goto('/wp-admin/post-new.php');

    await page.fill('h1[aria-label="Add title"]', 'Draft Post E2E');

    const editor = page.locator('[aria-label="Block: Paragraph"]').first();
    await editor.click();
    await editor.fill('This is a draft post.');

    // Sauvegarder brouillon
    await page.click('button:has-text("Save draft")');

    await expect(page.locator('.editor-post-saved-state')).toContainText('Saved');
  });
});

// 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: [
    ['html'],
    ['junit', { outputFile: 'test-results/junit.xml' }],
  ],
  use: {
    baseURL: process.env.WPBASEURL || 'http://localhost:8080',
    trace: 'on-first-retry',
    screenshot: 'only-on-failure',
  },

  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
  ],

  webServer: {
    command: 'docker-compose -f docker-compose.test.yml up',
    url: 'http://localhost:8080',
    reuseExistingServer: !process.env.CI,
    timeout: 120  1000,
  },
});

Docker pour Environnement de Test

# docker-compose.test.yml
version: '3.8'

services:
  wordpress-test:
    image: wordpress:6.4-php8.2-apache
    ports:
      - "8080:80"
    environment:
      WORDPRESSDBHOST: db-test
      WORDPRESSDBUSER: wordpress
      WORDPRESSDBPASSWORD: wordpress
      WORDPRESSDBNAME: wordpresstest
      WORDPRESSDEBUG: 1
    volumes:
      - ./:/var/www/html/wp-content/plugins/custom-plugin
      - ./themes:/var/www/html/wp-content/themes
    dependson:
      - db-test

  db-test:
    image: mysql:8.0
    environment:
      MYSQLDATABASE: wordpresstest
      MYSQLUSER: wordpress
      MYSQLPASSWORD: wordpress
      MYSQLROOTPASSWORD: rootpassword
    volumes:
      - db-test-data:/var/lib/mysql

  redis-test:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  db-test-data:

Monitoring et Notifications

Configuration Slack Notifications

# .github/workflows/notify.yml
name: Deployment Notifications

on:
  workflowrun:
    workflows: ["WordPress CI/CD Pipeline"]
    types:
      - completed

jobs:
  notify:
    runs-on: ubuntu-latest
    steps:
      - name: Notify on success
        if: ${{ github.event.workflowrun.conclusion == 'success' }}
        uses: slackapi/slack-github-action@v1.25.0
        with:
          webhook: ${{ secrets.SLACKWEBHOOK }}
          webhook-type: incoming-webhook
          payload: |
            {
              "text": "✅ Deployment successful",
              "blocks": [
                {
                  "type": "header",
                  "text": {
                    "type": "plaintext",
                    "text": "Deployment Successful ✅"
                  }
                },
                {
                  "type": "section",
                  "fields": [
                    {
                      "type": "mrkdwn",
                      "text": "Repository:n${{ github.repository }}"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "Branch:n${{ github.refname }}"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "Commit:n${{ github.sha }}"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "Author:n${{ github.actor }}"
                    }
                  ]
                },
                {
                  "type": "actions",
                  "elements": [
                    {
                      "type": "button",
                      "text": {
                        "type": "plaintext",
                        "text": "View Workflow"
                      },
                      "url": "${{ github.event.workflowrun.htmlurl }}"
                    }
                  ]
                }
              ]
            }

      - name: Notify on failure
        if: ${{ github.event.workflowrun.conclusion == 'failure' }}
        uses: slackapi/slack-github-action@v1.25.0
        with:
          webhook: ${{ secrets.SLACKWEBHOOK }}
          webhook-type: incoming-webhook
          payload: |
            {
              "text": "❌ Deployment failed",
              "blocks": [
                {
                  "type": "header",
                  "text": {
                    "type": "plaintext",
                    "text": "Deployment Failed ❌"
                  }
                },
                {
                  "type": "section",
                  "fields": [
                    {
                      "type": "mrkdwn",
                      "text": "Repository:n${{ github.repository }}"
                    },
                    {
                      "type": "mrkdwn",
                      "text": "Branch:n${{ github.refname }}"
                    }
                  ]
                },
                {
                  "type": "actions",
                  "elements": [
                    {
                      "type": "button",
                      "text": {
                        "type": "plaintext",
                        "text": "View Logs"
                      },
                      "url": "${{ github.event.workflowrun.html_url }}",
                      "style": "danger"
                    }
                  ]
                }
              ]
            }

Conclusion

Un pipeline CI/CD moderne pour WordPress en 2025 intègre :

Une remarque, un retour ?

Cet article est vivant — corrections, contre-arguments et retours de production sont les bienvenus. Trois canaux, choisissez celui qui vous convient.