Un client nous a écrit un jour parce que son extension premium livrait, dans le zip qu’il recevait, un dossier node_modules complet et un fichier .env de développement. Rien de grave en soi, mais ce genre de détail sème le doute sur le sérieux d’une livraison. Depuis, avant chaque tag, une seule commande construit l’archive de release, sans oubli et sans surplus.
Cette commande s’appelle dist-archive-command, un petit utilitaire distribué sous forme de script shell et repris par plusieurs générateurs de squelettes d’extensions WordPress. Il ne fait qu’une chose : lire un fichier de règles d’exclusion et produire un zip contenant exactement ce qui doit être livré, rien de plus.
Le problème du zip artisanal
Avant d’adopter cet outil, l’équipe utilisait un script maison bâti autour de zip -r et d’une longue liste d’exclusions passées en ligne de commande. Le script fonctionnait, jusqu’au jour où quelqu’un ajoutait un nouveau dossier de tests sans penser à l’exclure. Le zip livré au client contenait alors la suite de tests Pest complète, avec ses fixtures.
Le souci de fond n’est pas la maladresse d’un développeur isolé : c’est qu’un script d’exclusion manuel n’a pas de source de vérité partagée. Chaque développeur qui le modifie ajoute sa propre exception, et personne ne sait plus vraiment ce qui est censé sortir du zip.
Un fichier .distignore comme unique référence
dist-archive-command reprend un principe simple, calqué sur .gitignore : un fichier .distignore à la racine du dépôt liste les chemins à exclure de l’archive finale. Contrairement à .gitignore, qui décide ce qui entre dans le dépôt Git, .distignore décide ce qui sort du zip de distribution. Les deux listes ne se recoupent pas forcément : un fichier de configuration de build doit rester versionné dans Git, mais n’a rien à faire dans le zip livré.

Un exemple de .distignore pour une extension gérée avec Composer et un build front :
.git
.github
.distignore
.gitattributes
node_modules
src
tests
phpunit.xml.dist
composer.lock
webpack.config.js
*.map
.env
.env.example
Notez que src ne désigne pas ici le code PHP principal, mais un dossier de sources JavaScript non transpilées : seul le résultat du build, déposé dans assets/build, doit voyager dans le zip.
Générer l’archive
Une fois le fichier en place, la commande se résume à ceci, lancée depuis la racine du dépôt :
dist-archive-command . dist/mon-extension.zip
L’outil lit .distignore, copie l’arborescence du dépôt dans un répertoire temporaire en excluant les chemins listés, puis compresse ce répertoire dans le fichier cible. Le nom du dossier racine à l’intérieur du zip correspond au slug de l’extension, comme l’exige WordPress.org pour un dépôt SVN, ce qui évite la manipulation manuelle habituelle consistant à renommer le dossier avant de zipper.
Intégration dans un tag Git
La commande devient utile quand elle se déclenche automatiquement. Dans une Action GitHub, un job dédié se lance sur chaque tag au format v* :
on:
push:
tags:
- "v*"
jobs:
package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Installer les dépendances de production
run: composer install --no-dev --optimize-autoloader
- name: Construire les assets
run: npm ci && npm run build
- name: Générer l'archive
run: dist-archive-command . dist/${{ github.ref_name }}.zip
- name: Publier l'archive en artefact
uses: actions/upload-artifact@v4
with:
name: release-zip
path: dist/*.zip
Le point important, souvent oublié dans un script maison : l’installation Composer se fait avec --no-dev avant l’archivage, pour que le zip contienne les dépendances de production réelles et non les outils de test ou de lint.
Vérifier le contenu avant de livrer
Un zip généré automatiquement mérite tout de même un contrôle. Deux vérifications rapides suffisent en général :
- Lister le contenu de l’archive avec
unzip -l dist/mon-extension.zippour repérer un fichier oublié. - Installer le zip sur un WordPress vierge via l’écran Extensions > Ajouter > Téléverser une extension, pour s’assurer qu’elle s’active sans erreur de chemin.
Sur un projet suivi par l’agence, cette dernière étape a un jour révélé qu’un chemin absolu s’était glissé dans un fichier de configuration généré par le build, cassant l’activation sur un serveur dont l’arborescence différait de celle de la machine de développement. Le contrôle manuel post-génération reste donc utile, même avec un outil fiable.
Cas particulier : plusieurs cibles de livraison
Certaines extensions livrées à la fois sur WordPress.org et à un client premium ont besoin de deux zips différents, l’un incluant un module payant, l’autre non. La solution la plus propre consiste à maintenir deux fichiers d’exclusion, par exemple .distignore et .distignore-premium, et à appeler la commande deux fois avec l’option correspondante :
dist-archive-command . dist/gratuite.zip --ignore-file .distignore
dist-archive-command . dist/premium.zip --ignore-file .distignore-premium
Le fichier d’exclusion mérite une revue de code au même titre que le code lui-même : c’est lui qui décide ce que le client final reçoit, pas un script bricolé la veille d’une deadline.
Notre verdict
Adopter dist-archive-command ne change rien à la façon d’écrire l’extension, seulement à la façon de la livrer. Le gain n’est pas spectaculaire au quotidien, mais il élimine une catégorie entière d’incidents bêtes : le fichier de config oublié, le dossier de tests livré par erreur, le zip qui ne correspond pas à ce qui a été testé. Sur un dépôt qui vit plusieurs années, avec plusieurs développeurs qui se succèdent, ce filet de sécurité vaut largement les quelques minutes passées à écrire le .distignore initial.