Si vous avez déjà vu un déploiement WordPress “réussi” casser le site à cause d’un plugin mis à jour au mauvais moment, vous savez pourquoi le CI/CD n’est pas un luxe. La bonne nouvelle : avec GitHub Actions, SSH et un peu de discipline sur la structure du dépôt, vous pouvez rendre vos déploiements répétables, auditables et réversibles.

Ce qu’on va construire

Vous allez mettre en place une chaîne CI/CD GitHub Actions qui :

  • Construit un paquet “déployable” (thème(s), plugin(s) maison, mu-plugins, fichiers de config) à partir de votre dépôt Git.
  • Déploie automatiquement sur un serveur (VPS, VM, dédié) via SSH + rsync, sans écraser wp-content/uploads ni toucher à wp-config.php.
  • Exécute des tâches post-déploiement avec WP-CLI (flush de cache, mises à jour de DB si nécessaire, régénération d’index, etc.).
  • Permet un rollback en remettant la release précédente.

Public visé : blogueurs WordPress avancés et équipes techniques légères (1–3 personnes) qui gèrent un site WordPress 6.9.4 (avril 2026) et qui veulent des déploiements propres, sans “FTP à la main”.

À la fin, vous saurez :

  • structurer un dépôt Git “WordPress-friendly” sans versionner le core ;
  • sécuriser des secrets GitHub Actions (clé SSH, host, chemin) ;
  • déployer en environnement de staging puis production avec des garde-fous ;
  • diagnostiquer les erreurs classiques (permissions, rsync, WP-CLI, caches).

Résumé rapide

  • Vous versionnez uniquement ce qui vous appartient : wp-content/themes, wp-content/plugins (custom), mu-plugins, scripts de déploiement.
  • GitHub Actions construit un artefact et le pousse via rsync over SSH vers un dossier releases/.
  • Vous basculez un symlink current (rollback facile) ou vous synchronisez directement wp-content (plus simple).
  • WP-CLI exécute les tâches post-déploiement : caches, permaliens, actions spécifiques.
  • Vous excluez strictement uploads, cache, logs, et tout secret.
  • Vous testez d’abord en staging, avec un workflow identique.

Quand utiliser cette solution

  • Vous avez un serveur auquel vous pouvez accéder en SSH (même mutualisé “premium” si SSH + rsync sont disponibles).
  • Vous voulez tracer qui a déployé quoi (commit SHA), et pouvoir revenir en arrière en 30 secondes.
  • Votre site a des composants versionnables : thème enfant, plugins maison, mu-plugins, snippets “propres”.
  • Vous utilisez Divi 5 / Elementor / Avada : ça ne change rien au pipeline, tant que vous ne versionnez pas les caches générés.

Quand ne PAS utiliser cette solution

  • Vous êtes sur un mutualisé sans SSH, sans rsync, sans accès à WP-CLI. Dans ce cas, regardez la variante “plugin de déploiement” ou migrez l’hébergement.
  • Vous déployez un WordPress “complet” (core + wp-config) sans séparation : vous finirez par versionner des secrets ou casser des environnements.
  • Vous n’avez pas de staging. Déployer automatiquement sur production sans filet, je l’ai vu finir en incident à cause d’un simple fatal error PHP 8.1.

Avant de commencer (prérequis)

Versions et contraintes (avril 2026)

  • WordPress : 6.9.4 (cible). Les commandes WP-CLI et la structure wp-content restent stables.
  • PHP : 8.1 minimum recommandé. Vérifiez aussi la version PHP côté CI si vous lancez des tests.
  • Serveur : SSH + rsync + un utilisateur non-root (recommandé) + accès en écriture au dossier du site.
  • WP-CLI installé côté serveur (ou au minimum téléchargeable). Doc officielle : wp-cli.org.

Sauvegarde et environnement

  • Créez un staging (sous-domaine ou VM). Même pipeline, variables différentes.
  • Avant le premier déploiement : sauvegarde fichiers + base de données. Si vous avez un hébergeur managé, déclenchez un snapshot.
  • Assurez-vous de pouvoir revenir en arrière : soit via release précédente, soit via snapshot.

Sécurité (à ne pas rater)

  • Ne mettez jamais wp-config.php dans Git. Jamais.
  • La clé SSH utilisée par GitHub Actions doit être limitée (utilisateur dédié, droits minimaux).
  • Évitez d’exécuter des commandes WP-CLI arbitraires venant de PR non approuvées (risque RCE via workflow). Utilisez des protections d’environnement GitHub.

Sources officielles utiles


Étape 1 : Structurer le dépôt Git pour un WordPress déployable

Le problème vient souvent d’un dépôt Git “fourre-tout” : core WordPress versionné, uploads dans Git, caches de builder commités… et chaque déploiement devient imprévisible. Le but ici : versionner ce que vous maîtrisez et laisser le reste au serveur.

1.1 Arborescence recommandée

À la racine de votre dépôt :

# Exemple d'arborescence
.
├─ wp-content/
│  ├─ mu-plugins/
│  │  └─ bpcab-deploy-hooks.php
│  ├─ plugins/
│  │  └─ mon-plugin-custom/
│  └─ themes/
│     └─ mon-theme-enfant/
├─ deploy/
│  ├─ rsync-excludes.txt
│  └─ post-deploy.sh
├─ .github/
│  └─ workflows/
│     └─ deploy.yml
└─ composer.json (optionnel)

Vous ne commitez pas :

  • wp-admin, wp-includes (le core)
  • wp-content/uploads
  • wp-content/cache et assimilés
  • wp-config.php

1.2 Ajouter un fichier d’exclusions rsync

Créez deploy/rsync-excludes.txt :

# Fichiers/dossiers à ne jamais pousser
wp-content/uploads/
wp-content/cache/
wp-content/upgrade/
wp-content/backups/
wp-content/wflogs/
wp-content/ai1wm-backups/
wp-content/debug.log

# Artefacts OS/IDE
.DS_Store
Thumbs.db
.vscode/
.idea/

# Git
.git/
.github/

Dans mon expérience, l’oubli de uploads/ est la cause n°1 des déploiements “qui prennent 45 minutes” et saturent le serveur.

1.3 (Optionnel) mu-plugin de hooks de déploiement

Créez wp-content/mu-plugins/bpcab-deploy-hooks.php pour centraliser des actions post-déploiement (sans dépendre du thème). Les mu-plugins sont chargés tôt et ne peuvent pas être désactivés par erreur.

<?php
/**
 * Plugin Name: BPCAB Deploy Hooks
 * Description: Hooks utilitaires déclenchés après déploiement (WP 6.9.4+, PHP 8.1+).
 */

declare(strict_types=1);

if (!defined('ABSPATH')) {
	exit;
}

/**
 * Exemple : ajouter un endpoint de healthcheck interne.
 * Pratique pour vérifier qu'une release répond sans exposer d'infos sensibles.
 */
add_action('rest_api_init', function (): void {
	register_rest_route('bpcab/v1', '/health', [
		'methods'             => 'GET',
		'permission_callback' => '__return_true',
		'callback'            => function () {
			return new WP_REST_Response([
				'status'   => 'ok',
				'wp'       => get_bloginfo('version'),
				'php'      => PHP_VERSION,
				'time'     => time(),
			], 200);
		},
	]);
}, 10);

Résultat attendu : en staging, /wp-json/bpcab/v1/health retourne un JSON simple.


Étape 2 : Préparer l’accès serveur (SSH, clés, permissions)

Vous allez donner à GitHub Actions un accès SSH. Le piège classique : utiliser la clé perso de l’admin. Ne faites pas ça. Créez un utilisateur dédié, limité au dossier du site.

2.1 Créer un utilisateur “deploy” (VPS/dédié)

Sur le serveur :

# À exécuter en root ou via sudo
sudo adduser deploy
sudo usermod -aG www-data deploy

Adaptez le groupe web (www-data, nginx, apache) selon votre OS.

2.2 Préparer les dossiers releases

Supposons que votre site vit dans /var/www/mon-site et que le document root pointe vers /var/www/mon-site/current.

sudo mkdir -p /var/www/mon-site/releases
sudo mkdir -p /var/www/mon-site/shared/wp-content/uploads
sudo mkdir -p /var/www/mon-site/shared/wp-content/cache

# Permissions (à adapter à votre stack)
sudo chown -R deploy:www-data /var/www/mon-site
sudo find /var/www/mon-site -type d -exec chmod 775 {} ;
sudo find /var/www/mon-site -type f -exec chmod 664 {} ;

Oui, les permissions sont un sujet pénible. Mais si rsync n’a pas le droit d’écrire, vous aurez des “silent failures” (ou un site partiellement mis à jour).

2.3 Générer une clé SSH pour GitHub Actions

Sur votre machine locale :

ssh-keygen -t ed25519 -C "github-actions-deploy" -f ./github_actions_deploy_key

Ajoutez la clé publique sur le serveur :

# Sur le serveur, en utilisateur deploy
mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys

Collez le contenu de github_actions_deploy_key.pub.

2.4 Ajouter les secrets dans GitHub

Dans GitHub : Settings → Secrets and variables → Actions.

  • SSH_PRIVATE_KEY : contenu de github_actions_deploy_key (la clé privée)
  • SSH_HOST : IP ou hostname
  • SSH_USER : deploy
  • SSH_PORT : 22 (ou autre)
  • DEPLOY_PATH : /var/www/mon-site
  • WP_CLI_PATH : ex /usr/local/bin/wp (selon installation)

Pour la prod, utilisez idéalement Environments (staging/prod) avec approbation manuelle. Doc officielle : GitHub Environments.


Étape 3 : Créer le workflow GitHub Actions (build + artefacts)

On va construire un artefact “propre” puis le déployer. Le build inclut typiquement : install Composer (si vous en avez), build assets (si vous avez un bundler), puis création d’un tarball.

3.1 Créer .github/workflows/deploy.yml

name: Deploy WordPress wp-content

on:
  push:
    branches:
      - main
  workflow_dispatch:

concurrency:
  group: deploy-main
  cancel-in-progress: true

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

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

      - name: Préparer PHP (pour outils éventuels)
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          tools: composer:v2

      - name: Installer dépendances Composer (optionnel)
        run: |
          set -euo pipefail
          if [ -f composer.json ]; then
            composer install --no-dev --prefer-dist --no-interaction --no-progress
          else
            echo "Pas de composer.json, étape ignorée."
          fi

      - name: Créer l'artefact de déploiement
        run: |
          set -euo pipefail

          # On ne déploie que ce qu'on veut : wp-content + scripts nécessaires
          mkdir -p build
          rsync -a --delete 
            --exclude-from="deploy/rsync-excludes.txt" 
            wp-content/ build/wp-content/

          # On embarque les scripts de déploiement (côté runner)
          mkdir -p build/deploy
          cp -a deploy/ build/deploy/

          # Archive pour transfert plus rapide (optionnel, mais pratique)
          tar -czf artifact.tar.gz -C build .

      - name: Ajouter la clé SSH
        uses: webfactory/[email protected]
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Ajouter le serveur aux known_hosts
        run: |
          set -euo pipefail
          mkdir -p ~/.ssh
          chmod 700 ~/.ssh
          ssh-keyscan -p "${{ secrets.SSH_PORT }}" -H "${{ secrets.SSH_HOST }}" >> ~/.ssh/known_hosts

      - name: Déployer l'artefact sur le serveur
        env:
          SSH_HOST: ${{ secrets.SSH_HOST }}
          SSH_USER: ${{ secrets.SSH_USER }}
          SSH_PORT: ${{ secrets.SSH_PORT }}
          DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
        run: |
          set -euo pipefail

          RELEASE_ID="$(date +%Y%m%d%H%M%S)-${GITHUB_SHA::7}"
          echo "Release: ${RELEASE_ID}"

          # 1) Envoyer l'artefact
          scp -P "${SSH_PORT}" artifact.tar.gz "${SSH_USER}@${SSH_HOST}:/tmp/artifact-${RELEASE_ID}.tar.gz"

          # 2) Déployer côté serveur : extraire dans releases/RELEASE_ID
          ssh -p "${SSH_PORT}" "${SSH_USER}@${SSH_HOST}" bash -lc "'
            set -euo pipefail

            RELEASE_DIR="${DEPLOY_PATH}/releases/${RELEASE_ID}"
            mkdir -p "${RELEASE_DIR}"

            tar -xzf "/tmp/artifact-${RELEASE_ID}.tar.gz" -C "${RELEASE_DIR}"
            rm -f "/tmp/artifact-${RELEASE_ID}.tar.gz"

            # Lier uploads partagés (si votre docroot est current/)
            mkdir -p "${DEPLOY_PATH}/shared/wp-content/uploads"
            rm -rf "${RELEASE_DIR}/wp-content/uploads"
            ln -s "${DEPLOY_PATH}/shared/wp-content/uploads" "${RELEASE_DIR}/wp-content/uploads"

            # Lier cache partagé si vous en avez besoin
            mkdir -p "${DEPLOY_PATH}/shared/wp-content/cache"
            rm -rf "${RELEASE_DIR}/wp-content/cache"
            ln -s "${DEPLOY_PATH}/shared/wp-content/cache" "${RELEASE_DIR}/wp-content/cache"

            # Basculer current (symlink) : rollback facile
            ln -sfn "${RELEASE_DIR}" "${DEPLOY_PATH}/current"
          '"

      - name: Post-déploiement (WP-CLI)
        env:
          SSH_HOST: ${{ secrets.SSH_HOST }}
          SSH_USER: ${{ secrets.SSH_USER }}
          SSH_PORT: ${{ secrets.SSH_PORT }}
          DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
          WP_CLI_PATH: ${{ secrets.WP_CLI_PATH }}
        run: |
          set -euo pipefail

          ssh -p "${SSH_PORT}" "${SSH_USER}@${SSH_HOST}" bash -lc "'
            set -euo pipefail

            cd "${DEPLOY_PATH}/current"

            # Remplacez --path si votre WordPress n'est pas dans current/
            WP="${WP_CLI_PATH}"

            # Vérifier que WP-CLI voit bien l'installation
            "${WP}" core version

            # Exemple : vider les caches WordPress (transients)
            "${WP}" transient delete --all || true

            # Exemple : régénérer les règles de réécriture (si vous touchez aux routes)
            "${WP}" rewrite flush --hard || true
          '"

3.2 Pourquoi cette architecture “releases + current”

  • Vous évitez l’état “mi-déployé” si rsync est interrompu.
  • Le rollback consiste à repointer current vers la release précédente.
  • Vous pouvez garder N releases pour audit.

Edge case réel : si vous utilisez OPcache agressif + FPM, un basculement de symlink peut laisser des scripts en cache. Prévoyez un reload PHP-FPM si nécessaire (voir section maintenance).


Étape 4 : Déployer via rsync (zéro-downtime “pragmatique”)

La méthode tar+extract marche très bien. Mais rsync a deux avantages : diff incrémental et logs plus clairs. Ici, je vous montre une variante où l’artefact est déjà prêt et vous rsync vers la release.

4.1 Script serveur : deploy/post-deploy.sh

Ce script sera copié dans l’artefact. Il s’exécute côté serveur. Créez deploy/post-deploy.sh :

#!/usr/bin/env bash
# Script post-déploiement côté serveur (à adapter)
set -euo pipefail

DEPLOY_PATH="${1:-}"
RELEASE_ID="${2:-}"
WP_CLI_PATH="${3:-/usr/local/bin/wp}"

if [ -z "${DEPLOY_PATH}" ] || [ -z "${RELEASE_ID}" ]; then
  echo "Usage: post-deploy.sh /chemin/deploy RELEASE_ID /chemin/wp"
  exit 1
fi

cd "${DEPLOY_PATH}/current"

# Vérifier WP-CLI
"${WP_CLI_PATH}" core version

# Vider transients (évite des incohérences après changements d'options)
"${WP_CLI_PATH}" transient delete --all || true

# Flush rewrite si vous avez modifié CPT, endpoints, etc.
"${WP_CLI_PATH}" rewrite flush --hard || true

# Exemple : si vous utilisez un plugin de cache, vous pouvez déclencher un purge via WP-CLI si disponible
# "${WP_CLI_PATH}" cache flush || true

echo "Post-déploiement terminé pour ${RELEASE_ID}"

N’oubliez pas de rendre le script exécutable dans Git :

chmod +x deploy/post-deploy.sh

4.2 Ajuster le workflow pour rsync (option)

Remplacez l’étape “Déployer l’artefact” par :

# Exemple d'étape run (à intégrer dans votre YAML)
set -euo pipefail

RELEASE_ID="$(date +%Y%m%d%H%M%S)-${GITHUB_SHA::7}"

ssh -p "${SSH_PORT}" "${SSH_USER}@${SSH_HOST}" bash -lc "'
  set -euo pipefail
  mkdir -p "${DEPLOY_PATH}/releases/${RELEASE_ID}"
'"

# Rsync vers la release (plus lisible que tar pour certains)
rsync -az --delete 
  -e "ssh -p ${SSH_PORT}" 
  --exclude-from="deploy/rsync-excludes.txt" 
  build/ "${SSH_USER}@${SSH_HOST}:${DEPLOY_PATH}/releases/${RELEASE_ID}/"

# Symlinks et bascule current
ssh -p "${SSH_PORT}" "${SSH_USER}@${SSH_HOST}" bash -lc "'
  set -euo pipefail
  RELEASE_DIR="${DEPLOY_PATH}/releases/${RELEASE_ID}"

  mkdir -p "${DEPLOY_PATH}/shared/wp-content/uploads"
  rm -rf "${RELEASE_DIR}/wp-content/uploads"
  ln -s "${DEPLOY_PATH}/shared/wp-content/uploads" "${RELEASE_DIR}/wp-content/uploads"

  ln -sfn "${RELEASE_DIR}" "${DEPLOY_PATH}/current"
'"

Résultat attendu : un dossier releases/... avec votre wp-content et un current qui pointe dessus.


Étape 5 : Lancer les tâches post-déploiement avec WP-CLI (migrations, caches)

Le point délicat : WordPress n’a pas de “migrations” natives comme Laravel. Les plugins, eux, font souvent des upgrades au chargement admin. En CI/CD, vous voulez éviter que la première visite admin déclenche une migration longue.

5.1 Déclencher des upgrades “safe”

Ce que vous pouvez faire de façon raisonnable :

  • Vérifier la version (sanity check).
  • Flush rewrite si vous avez touché à des routes.
  • Purger caches (transients, caches plugin si WP-CLI le supporte).
  • Exécuter vos propres migrations (dans un mu-plugin) via une commande WP-CLI custom.

5.2 Ajouter une commande WP-CLI custom (mu-plugin)

Dans wp-content/mu-plugins/bpcab-deploy-cli.php :

<?php
/**
 * Plugin Name: BPCAB Deploy CLI
 * Description: Commandes WP-CLI de déploiement (WP 6.9.4+, PHP 8.1+).
 */

declare(strict_types=1);

if (!defined('ABSPATH')) {
	exit;
}

if (defined('WP_CLI') && WP_CLI) {
	/**
	 * Commande: wp bpcab deploy
	 * Exemple: wp bpcab deploy --allow-root
	 */
	WP_CLI::add_command('bpcab deploy', function (array $args, array $assoc_args): void {
		// Exemple : “migration” maison basée sur une option de version.
		$current = (string) get_option('bpcab_schema_version', '0');
		$target  = '1';

		if (version_compare($current, $target, '>=')) {
			WP_CLI::log('Schéma déjà à jour.');
			return;
		}

		// Exemple concret : créer une option/structure requise par une nouvelle release.
		// Remplacez par vos vrais besoins (création de tables, backfill, etc.).
		update_option('bpcab_feature_flag_new_header', '1', false);

		update_option('bpcab_schema_version', $target, false);

		WP_CLI::success('Migration appliquée, schéma = ' . $target);
	});
}

Puis, dans le workflow, remplacez/ajoutez :

"${WP}" bpcab deploy || true

Pourquoi || true ? Sur certains sites, je préfère que le déploiement n’échoue pas pour une migration “non critique”. Pour une migration critique, retirez-le et faites échouer le job.


Étape 6 : Ajouter un rollback simple et fiable

Un rollback efficace, c’est 80% de la valeur d’un pipeline. Le plus simple : garder les releases et repointer current.

6.1 Garder les N dernières releases

Ajoutez une étape serveur après le switch :

# À exécuter côté serveur
cd /var/www/mon-site/releases
ls -1dt */ | tail -n +6 | xargs -r rm -rf

Ici on garde 5 releases. Ajustez selon l’espace disque.

6.2 Workflow GitHub “Rollback” (manuel)

Ajoutez un second workflow .github/workflows/rollback.yml :

name: Rollback WordPress release

on:
  workflow_dispatch:
    inputs:
      release:
        description: "Nom du dossier release (ex: 20260413121000-abc1234)"
        required: true
        type: string

jobs:
  rollback:
    runs-on: ubuntu-latest
    steps:
      - name: Ajouter la clé SSH
        uses: webfactory/[email protected]
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: known_hosts
        run: |
          set -euo pipefail
          mkdir -p ~/.ssh
          chmod 700 ~/.ssh
          ssh-keyscan -p "${{ secrets.SSH_PORT }}" -H "${{ secrets.SSH_HOST }}" >> ~/.ssh/known_hosts

      - name: Basculer current vers la release demandée
        env:
          SSH_HOST: ${{ secrets.SSH_HOST }}
          SSH_USER: ${{ secrets.SSH_USER }}
          SSH_PORT: ${{ secrets.SSH_PORT }}
          DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
          RELEASE: ${{ inputs.release }}
        run: |
          set -euo pipefail
          ssh -p "${SSH_PORT}" "${SSH_USER}@${SSH_HOST}" bash -lc "'
            set -euo pipefail
            TARGET="${DEPLOY_PATH}/releases/${RELEASE}"
            if [ ! -d "${TARGET}" ]; then
              echo "Release introuvable: ${TARGET}"
              exit 1
            fi
            ln -sfn "${TARGET}" "${DEPLOY_PATH}/current"
            echo "Rollback effectué vers ${RELEASE}"
          '"

Résultat attendu : rollback déclenché depuis GitHub UI, sans toucher à la base de données.


Le résultat complet

Si vous voulez tout copier d’un coup, voici le minimum viable : exclusions, workflow deploy, et un post-deploy WP-CLI.

Fichier deploy/rsync-excludes.txt

wp-content/uploads/
wp-content/cache/
wp-content/upgrade/
wp-content/backups/
wp-content/wflogs/
wp-content/ai1wm-backups/
wp-content/debug.log
.DS_Store
Thumbs.db
.vscode/
.idea/
.git/
.github/

Fichier deploy/post-deploy.sh

#!/usr/bin/env bash
# Script post-déploiement côté serveur
set -euo pipefail

DEPLOY_PATH="${1:-}"
RELEASE_ID="${2:-}"
WP_CLI_PATH="${3:-/usr/local/bin/wp}"

if [ -z "${DEPLOY_PATH}" ] || [ -z "${RELEASE_ID}" ]; then
  echo "Usage: post-deploy.sh /chemin/deploy RELEASE_ID /chemin/wp"
  exit 1
fi

cd "${DEPLOY_PATH}/current"

"${WP_CLI_PATH}" core version
"${WP_CLI_PATH}" transient delete --all || true
"${WP_CLI_PATH}" rewrite flush --hard || true

echo "OK post-déploiement ${RELEASE_ID}"

Fichier .github/workflows/deploy.yml

name: Deploy WordPress wp-content

on:
  push:
    branches: [main]
  workflow_dispatch:

concurrency:
  group: deploy-main
  cancel-in-progress: true

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Préparer PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          tools: composer:v2

      - name: Build (Composer optionnel)
        run: |
          set -euo pipefail
          if [ -f composer.json ]; then
            composer install --no-dev --prefer-dist --no-interaction --no-progress
          fi

      - name: Préparer build/
        run: |
          set -euo pipefail
          mkdir -p build
          rsync -a --delete --exclude-from="deploy/rsync-excludes.txt" wp-content/ build/wp-content/
          mkdir -p build/deploy
          cp -a deploy/ build/deploy/
          tar -czf artifact.tar.gz -C build .

      - name: SSH agent
        uses: webfactory/[email protected]
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: known_hosts
        run: |
          set -euo pipefail
          mkdir -p ~/.ssh
          chmod 700 ~/.ssh
          ssh-keyscan -p "${{ secrets.SSH_PORT }}" -H "${{ secrets.SSH_HOST }}" >> ~/.ssh/known_hosts

      - name: Déployer
        env:
          SSH_HOST: ${{ secrets.SSH_HOST }}
          SSH_USER: ${{ secrets.SSH_USER }}
          SSH_PORT: ${{ secrets.SSH_PORT }}
          DEPLOY_PATH: ${{ secrets.DEPLOY_PATH }}
          WP_CLI_PATH: ${{ secrets.WP_CLI_PATH }}
        run: |
          set -euo pipefail

          RELEASE_ID="$(date +%Y%m%d%H%M%S)-${GITHUB_SHA::7}"

          scp -P "${SSH_PORT}" artifact.tar.gz "${SSH_USER}@${SSH_HOST}:/tmp/artifact-${RELEASE_ID}.tar.gz"

          ssh -p "${SSH_PORT}" "${SSH_USER}@${SSH_HOST}" bash -lc "'
            set -euo pipefail
            RELEASE_DIR="${DEPLOY_PATH}/releases/${RELEASE_ID}"
            mkdir -p "${RELEASE_DIR}"
            tar -xzf "/tmp/artifact-${RELEASE_ID}.tar.gz" -C "${RELEASE_DIR}"
            rm -f "/tmp/artifact-${RELEASE_ID}.tar.gz"

            mkdir -p "${DEPLOY_PATH}/shared/wp-content/uploads"
            rm -rf "${RELEASE_DIR}/wp-content/uploads"
            ln -s "${DEPLOY_PATH}/shared/wp-content/uploads" "${RELEASE_DIR}/wp-content/uploads"

            ln -sfn "${RELEASE_DIR}" "${DEPLOY_PATH}/current"

            chmod +x "${RELEASE_DIR}/deploy/post-deploy.sh"
            "${RELEASE_DIR}/deploy/post-deploy.sh" "${DEPLOY_PATH}" "${RELEASE_ID}" "${WP_CLI_PATH}"

            cd "${DEPLOY_PATH}/releases"
            ls -1dt */ | tail -n +6 | xargs -r rm -rf
          '"

Personnalisation

  • Ajoutez un environnement staging avec un autre DEPLOY_PATH et des secrets séparés.
  • Si vous avez un build front (Vite/Webpack), lancez-le avant la création de l’artefact et copiez les fichiers buildés dans le thème.
  • Si vous utilisez Composer pour des libs PHP dans un plugin, gardez vendor/ dans l’artefact (mais pas forcément dans Git).

Adapter pour Divi 5 / Elementor / Avada

Le CI/CD ne dépend pas du builder. Ce qui change : ce que vous ne devez surtout pas déployer.

Divi 5

  • Évitez de versionner/déployer des caches générés (selon votre setup : CSS statique, fichiers dans wp-content).
  • Si vous avez un thème enfant Divi, versionnez-le comme n’importe quel thème enfant.
  • Après déploiement, si vous observez un front “sans styles”, le problème est souvent un cache (plugin cache/CDN) et pas Divi lui-même.

Elementor

  • Elementor stocke beaucoup de choses en DB. Un déploiement de fichiers ne “migre” pas vos templates.
  • Si vous déployez un widget custom, ajoutez un rewrite flush seulement si vous ajoutez des endpoints, sinon évitez (coûteux).
  • Si vous utilisez un cache agressif, prévoyez un purge ciblé après déploiement.

Avada (Fusion Builder)

  • Avada génère aussi des assets/caches. Ne versionnez pas ces dossiers de cache.
  • Pour des éléments custom (shortcodes, CPT), placez-les dans un plugin custom versionné plutôt que dans functions.php du thème parent.

Vérification finale

  1. Dans GitHub, vérifiez que le workflow passe en vert sur main.
  2. Sur le serveur : ls -la /var/www/mon-site doit montrer releases/, shared/ et current (symlink).
  3. Visitez /wp-json/bpcab/v1/health si vous avez ajouté le mu-plugin : vous devez voir wp: 6.9.4 (ou votre version).
  4. Vérifiez que wp-content/uploads contient bien vos médias et n’a pas été écrasé.
  5. Ouvrez le front et l’admin. Si vous avez un plugin de cache, purgez-le et retestez.

Si le résultat n’est pas celui attendu

Symptôme Cause probable Vérification Solution
Le workflow échoue sur ssh-keyscan Port/host incorrect, firewall Depuis votre machine : ssh -p PORT user@host Corrigez SSH_HOST/SSH_PORT, ouvrez le port, vérifiez DNS
Déploiement OK mais site en erreur 500 Fatal PHP, dépendance manquante, incompatibilité PHP 8.1+ Logs PHP-FPM/Apache/Nginx, wp-content/debug.log Rollback via workflow, corrigez, redeploy ; vérifiez la version PHP serveur
Les médias ont disparu uploads/ écrasé ou symlink absent ls -la current/wp-content/uploads Recréez shared/uploads et le symlink ; excluez uploads de rsync
Les changements CSS n’apparaissent pas Cache navigateur/CDN/plugin cache Test en navigation privée + purge cache Purger cache, versionner les assets avec hash, invalider CDN
wp core version échoue en CI WP-CLI introuvable ou mauvais --path SSH puis which wp, wp --info Corrigez WP_CLI_PATH ou ajoutez --path=/chemin
  • Copier le code au mauvais endroit : le workflow doit être dans .github/workflows/, pas ailleurs.
  • Oublier un point-virgule… : si vous ajoutez un mu-plugin, une erreur PHP casse tout le site. Testez en staging.
  • Tester sur production sans sauvegarde : j’ai vu des rollbacks impossibles parce qu’une migration DB s’est déclenchée après le switch. Faites staging d’abord.

Pièges et erreurs courantes

Erreur Cause Solution
Vous déployez wp-config.php par accident Dépôt mal structuré Supprimez-le du dépôt, ajoutez-le aux exclusions, régénérez des secrets si exposés
Le site mélange anciens/nouveaux fichiers Déploiement “in place” interrompu Utilisez releases/ + symlink current (switch atomique)
Permissions incohérentes après rsync Umask/owner différents Fixez owner/groupe, évitez de déployer en root, normalisez via chown/chmod
Action vs filtre : hook WP mal choisi Code “snippet” copié d’un vieux tuto Placez le code dans un plugin, vérifiez le hook sur developer.wordpress.org/reference
Assets JS/CSS non chargés Mauvais wp_enqueue_scripts ou chemins build Vérifiez l’enqueue, chemins, et que le build tourne avant l’artefact

Variante / alternative

Alternative 1 : Déploiement via GitHub Releases + téléchargement serveur

Si votre serveur ne permet pas rsync mais permet de sortir sur Internet, vous pouvez :

  • Créer un artefact GitHub (Release)
  • Télécharger côté serveur via curl
  • Extraire dans releases/ puis switch symlink

C’est moins “propre” (dépend d’un download public/privé, tokens), mais parfois c’est la seule option sur des hébergements bridés.

Alternative 2 : Plateforme managée (si vous pouvez)

Si vous voulez du CI/CD sans gérer SSH, regardez un hébergeur WordPress managé avec pipeline intégré. Je le mentionne parce que le coût humain de la maintenance SSH n’est pas négligeable.


Conseils sécurité, performance et maintenance

  • Protégez la production avec GitHub Environments : approbation requise, secrets séparés, logs contrôlés. Doc : Using environments for deployment.
  • Limitez la clé SSH : utilisateur dédié, pas de shell si vous pouvez, pas de sudo. Sur OpenSSH, vous pouvez restreindre via options dans authorized_keys (à manier avec précaution).
  • Ne loggez jamais les secrets : évitez set -x dans vos scripts, attention aux echo.
  • OPcache / PHP-FPM : si vous voyez des comportements “fantômes” après switch, reload FPM peut aider. Ajoutez une commande côté serveur si vous avez les droits (sinon, baissez les TTL OPcache ou utilisez opcache.validate_timestamps selon votre politique).
  • Cache plugin/CDN : prévoyez une purge API après déploiement si votre stack le permet. Sinon, vous allez “déboguer” des non-bugs.

Pour aller plus loin

  • Ajouter des jobs de tests : lint PHP, PHPCS WordPress, tests JS, build assets.
  • Déployer un must-use plugin qui expose la version de release (SHA) dans l’admin pour debug.
  • Implémenter un vrai “maintenance mode” pendant la bascule (ou un drain des workers) si vous avez du trafic.
  • Ajouter une étape de healthcheck (HTTP 200 + contenu attendu) avant de valider le déploiement.
  • Mettre en place un service container pour vos plugins custom (DI) afin de réduire les effets de bord en production.

Ressources


FAQ

Est-ce que je dois versionner le core WordPress ?

Non. Versionnez votre code (wp-content) et gérez le core via l’hébergeur, ou via un processus séparé. Versionner le core augmente le risque de conflits et de secrets accidentels.

Pourquoi ne pas déployer la base de données ?

Parce que c’est un autre problème : migrations, données utilisateurs, médias, contenu. Pour un blog, la DB est “vivante”. Déployer la DB à chaque release est rarement souhaitable.

Comment gérer les plugins premium (Divi, Avada, etc.) ?

Évitez de les commiter si la licence l’interdit. Gardez-les installés côté serveur, ou utilisez un dépôt privé si le contrat le permet. Dans tous les cas, ne mélangez pas “code premium” et “code maison” sans vérifier les conditions.

Mon site est sur un mutualisé : c’est mort ?

Pas forcément. Si vous avez SSH + rsync, ça passe. Sinon, regardez la variante “GitHub Release + curl” ou changez d’hébergement.

Dois-je exécuter wp plugin update en post-déploiement ?

Je l’évite en général. Vous voulez que votre déploiement soit déterministe. Mettre à jour des plugins “au moment du déploiement” rend le résultat dépendant de l’état du dépôt WordPress.org à l’instant T.

Que faire si un plugin déclenche une migration DB au premier chargement ?

Ça arrive. Essayez d’identifier une commande WP-CLI du plugin (certains en ont), sinon prévoyez une fenêtre de maintenance et surveillez les logs. Dans le pire cas, rollback fichiers ne suffit pas si la DB a changé.

Comment éviter les surprises de cache après déploiement ?

Purge cache plugin/CDN, incrémentez les versions d’assets (hash), et évitez de versionner des caches générés par Divi/Elementor/Avada.

Pourquoi mon rewrite flush ne prend pas ?

Souvent parce que --path n’est pas bon, ou parce que WP-CLI n’exécute pas dans le bon répertoire. Faites wp --info et forcez --path.

Comment savoir quelle release est en production ?

Le plus simple : exposez le SHA dans un mu-plugin (admin footer, endpoint REST interne). Ou lisez le nom du dossier current côté serveur.

Est-ce compatible avec PHP 8.1+ et WordPress 6.9.4 ?

Oui, le pipeline est agnostique. Le point critique est la compatibilité de vos plugins/thèmes avec PHP 8.1+ ; testez en staging et surveillez les warnings/deperecations.