Intermediaire 5 min de lecture · 1 017 mots

Déployer WordPress en production avec GitHub Actions et SSH

Estimated reading time: 5 minutes

Pendant deux ans, j’ai déployé à la main. scp, rsync dans le terminal, parfois un tar envoyé sur le serveur. Ça marchait. Mais un vendredi soir, j’ai mergé une régression dans le plugin, déployé sans tester, et cassé la page d’accueil pendant trois heures. Le genre d’incident qui convainc sans discours.

Voilà ce que j’ai mis en place depuis, sur you.arewel.com, un VPS OVH sous Debian 12 Bookworm.

Ce que fait le workflow

Le déclencheur est simple : quand je publie une release GitHub (ou un workflowdispatch manuel), le workflow :

  • Construit les artefacts (ZIPs plugin et thème, versionnés)
  • Valide les tailles
  • Les attache à la release GitHub comme assets
  • Déclenche le deploy sur le VPS via SSH + rsync
  • Pas de branche deploy, pas de tag auto, pas de magie. Un bouton GitHub → une release → un déploiement.

    La clé SSH dédiée

    Je n’utilise pas la clé personnelle du développeur pour le deploy. C’est la première chose à mettre en place.

# En local — générer une paire dédiée au déploiement
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/iddeployactions -N ""

La clé publique va dans ~/.ssh/authorizedkeys du compte de deploy sur le serveur (scytale dans mon cas) :

cat ~/.ssh/iddeployactions.pub | ssh scytale@you.arewel.com "cat >> ~/.ssh/authorizedkeys"

La clé privée va dans les secrets GitHub du repo : Settings → Secrets and variables → Actions → New repository secret. Nom du secret : SSHPRIVATEKEY.

Ajouter également :

  • SSHHOST : you.arewel.com
  • SSHUSER : scytale
  • Le workflow complet

    name: Release & Deploy
    
    on:
      release:
        types: [published]
      workflowdispatch:
        inputs:
          tag:
            description: "Tag à déployer"
            required: true
    
    jobs:
      build:
        runs-on: ubuntu-latest
        outputs:
          pluginzip: ${{ steps.build.outputs.pluginzip }}
          themezip: ${{ steps.build.outputs.themezip }}
        steps:
          - uses: actions/checkout@v6
    
          - name: Build plugin and theme
            id: build
            run: |
              bash scripts/build-plugin.sh
              bash scripts/build-theme.sh
              # Récupère les noms de ZIPs générés
              PLUGINZIP=$(ls build/yawc-core-plugin-.zip | head -1)
              THEMEZIP=$(ls build/youarewelcom-theme-.zip | head -1)
              echo "pluginzip=$PLUGINZIP" >> "$GITHUBOUTPUT"
              echo "themezip=$THEMEZIP" >> "$GITHUBOUTPUT"
    
          - name: Validate ZIP sizes
            run: |
              PLUGINSIZE=$(stat -c%s "${{ steps.build.outputs.pluginzip }}")
              THEMESIZE=$(stat -c%s "${{ steps.build.outputs.themezip }}")
              [ "$PLUGINSIZE" -gt 10000 ] || { echo "Plugin ZIP trop petit"; exit 1; }
              [ "$THEMESIZE" -gt 10000 ] || { echo "Theme ZIP trop petit"; exit 1; }
    
          - name: Upload to GitHub Release
            uses: softprops/action-gh-release@v3
            with:
              files: |
                ${{ steps.build.outputs.pluginzip }}
                ${{ steps.build.outputs.themezip }}
    
      deploy:
        needs: build
        runs-on: ubuntu-latest
        steps:
          - uses: actions/checkout@v6
    
          - name: Deploy via rsync + SSH
            env:
              SSHPRIVATEKEY: ${{ secrets.SSHPRIVATEKEY }}
              SSHHOST: ${{ secrets.SSHHOST }}
              SSHUSER: ${{ secrets.SSHUSER }}
            run: |
              echo "$SSHPRIVATEKEY" > /tmp/deploykey
              chmod 600 /tmp/deploykey
              SSHOPTS="-i /tmp/deploykey -o StrictHostKeyChecking=no -p 22"
              HTDOCS=/var/www/you.arewel.com/htdocs
              rsync -avz --delete -e "ssh $SSHOPTS" 
                plugins/yawc-core/ 
                "$SSHUSER@$SSHHOST:$HTDOCS/wp-content/plugins/yawc-core/"
              rsync -avz --delete -e "ssh $SSHOPTS" 
                themes/youarewelcom/ 
                "$SSHUSER@$SSHHOST:$HTDOCS/wp-content/themes/youarewelcom/"
              ssh $SSHOPTS "$SSHUSER@$SSHHOST" 
                "wp --path=$HTDOCS cache flush && echo 'Deploy OK — $(date)'"
              rm /tmp/deploykey
    

    [tip]Le rsync tourne directement sur le runner GitHub, pas sur le serveur — le serveur n’a pas besoin de clone git. Les fichiers construits localement par le job build sont transférés directement via SSH. Chaque rsync est atomique : si un fichier échoue, le step s’arrête et le déploiement est marqué en échec.[/tip]

    Ce qui a coincé au premier essai

    La première version du workflow utilisait appleboy/ssh-action pour faire un git pull côté serveur. Ça suppose un clone git sur le serveur — qui n’existait pas. Résultat : step en échec silencieux parce que le répertoire manquait.

    La deuxième erreur : wp cache flush dans la commande SSH finale échouait avec Error: This does not seem to be a WordPress installation. WP-CLI cherche wp-config.php dans le répertoire courant, qui est le home du compte SSH. Il faut passer --path= explicitement.

    L’autre bug : softprops/action-gh-release@v3 cherche par défaut la release correspondant au tag github.refname. Quand je lance le workflow en workflowdispatch avec un tag manuel, ce tag n’existe pas encore comme release — le step échoue. La solution : ne pas attacher les ZIPs à la release depuis le workflow_dispatch, ou créer la release en amont.

    Durée et coût

    Le job build prend ~45 secondes sur un runner ubuntu-latest. Le job deploy (SSH + rsync + wp cache flush) prend ~20 secondes. Total release → prod : moins de 2 minutes.

    GitHub Actions est gratuit pour les repos publics. Pour les repos privés, c’est 2000 minutes/mois incluses dans le plan gratuit. Sur un rythme d’une release par semaine, ça représente ~10 minutes par mois. Pas un sujet.

    Ce que je ferais différemment

    Je n’aurais pas attendu deux ans. L’investissement de setup est de l’ordre de deux heures. Et depuis que c’est en place, je n’ai plus déployé manuellement une seule fois.

    Si je devais refaire : je mettrais en place un test de smoke post-deploy automatique — un curl sur la homepage qui vérifie que le status HTTP est 200 et que X-Cache: HIT est présent dans les headers. Ça m’aurait évité l’incident du vendredi soir.


    Article hors série

    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.