Use a WP CLI command to prepare your plugins for release

If you’ve built plugins you’ve undoubtedly spent a whole lot of time and energy finishing up on sweet new feature X only to then face the hurdle of having to prep the whole shebang for release.

I was tired of hitting that final hurdle whilst working on the ACF Custom Database Tables plugin so I decided to do something clever – I automated stuff.

At first, I had a bash script that was doing all the goods for me. It was basically just a command that had the plugin’s directory and various details hard-coded into it. It was decent and saved me time but kinda fell on its face when I moved the dev installation to another directory and forgot about the command…

At that point, I was like “Dafuq am I doing? Why aren’t I using WP CLI?!”

So that’s where I took it next and I’m so glad I did! It’s been super-duper handy and the release process is now super-duper dandy as I no longer have to think hard or work through a checklist in order to make a release-ready archive of a plugin. All I need to do is run wp makerelease on the command line and an archive will be prepared in the wp-content/releases directory with the plugin version in the filename.

Let’s take a quick look at the command options

There isn’t a lot. You either define a custom suffix or you don’t.

# To make a release, just call your command.
# This will produce a file.
wp makerelease
# If you need to suffix your release to differentiate it without
# changing the version, use the –suffix="whatever" assoc arg.
# This will produce a file.
wp makerelease –suffix="feature-test"
view raw hosted with ❤ by GitHub

What does the command actually do?

  1. Uses rsync to create a copy of the plugin dir.
  2. Excludes files and directories that aren’t needed in the release (configurable).
  3. Zips up the copy into an archive with a filename matching your plugin.
  4. Extracts the plugin version from the plugin headers and appends that to the filename.
  5. Adds a custom suffix to archive if you need it.
  6. Opens up the directory containing your new release.

The command class

The following class is an invokable class the represents one command. You can customise the command along with the file exclusions and a few other bits by modifying the class constants.

FYI, this particular example is namespaced to a project I’m working on so you should change that to suit your own plugin.

namespace WpLandingKitPlugin;
use WP_CLI;
use WP_CLI_Command;
* Class MakeRelease
* @package WpLandingKit
class MakeReleaseCommand extends WP_CLI_Command {
const PLUGIN_SLUG = 'wp-landing-kit';
const PLUGIN_DIR_PATH = WP_CONTENT_DIR . '/plugins/' . self::PLUGIN_SLUG;
const RELEASES_DIR_PATH = WP_CONTENT_DIR . '/releases';
* Files & directories to exclude when building the archive.
private $positional_args;
private $associative_args;
* Creates a release archive with the version name appended to the archive file.
* e.g;
* [–suffix=<suffix>]
* : Optional suffix to append to end of archive file name. Leading hyphen is automatically added.
* @param $positional_args
* @param $associative_args
public function __invoke( $positional_args, $associative_args ) {
$this->positional_args = $positional_args;
$this->associative_args = $associative_args;
exec( $this->shell_command(), $output );
foreach ( $output as $line ) {
WP_CLI::log( $line );
WP_CLI::success( 'Done.' );
* Build the shell command sequence.
* @return string
private function shell_command() {
// Assign class constants and method outputs to vars so we can interpolate in our shell command string. This
// just keeps the command easier to read as opposed to sprintf'ing the shit out of it.
$slug = self::PLUGIN_SLUG;
$plugin_dir = self::PLUGIN_DIR_PATH;
$releases_dir = self::RELEASES_DIR_PATH;
$suffix = $this->suffix();
$version = $this->version_suffix();
$exclusions = $this->format_exclusions();
return "rsync -a $exclusions $plugin_dir $releases_dir \
&& cd $releases_dir \
&& zip -rm {$slug}{$version}{$suffix}.zip $slug \
&& cd – \
&& open $releases_dir;";
* Extract the value of the suffix arg passed to the CLI command and format it ready for use in the release archive
* file name.
* @return string
private function suffix() {
return ( $suffix = WP_CLI\Utils\get_flag_value( $this->associative_args, 'suffix' ) )
? "-$suffix"
: '';
* Extract the plugin version from the plugin headers and format it ready for use in the release archive file name.
* @return string
private function version_suffix() {
$data = get_plugin_data( $this->plugin_file_path(), false, false );
return empty( $data['Version'] ) ? '' : "-v{$data['Version']}";
* Build full path to plugin main file.
* @return string
private function plugin_file_path() {
* Build the exlude associative args as expected by the rsync command.
* @return string
private function format_exclusions() {
if ( empty( self::RELEASE_EXCLUSIONS ) ) {
return '';
return join( ' ', array_map( function ( $pattern ) {
return "–exclude=\"$pattern\"";
}, (array) self::RELEASE_EXCLUSIONS ) );

How to set it up

I just include this in my wp-config.php file and register it with WP CLI using the following:

use WpLandingKitPlugin\MakeReleaseCommand;
if ( defined( 'WP_CLI' ) && WP_CLI ) {
try {
require 'lib/MakeReleaseCommand.php';
WP_CLI::add_command( MakeReleaseCommand::COMMAND, MakeReleaseCommand::class );
} catch ( \Exception $e ) {
// Your command appears to have shit the bed… ¯\_(ツ)_/¯
view raw wp-config.php hosted with ❤ by GitHub

Need a website? Let's talk.

From website design & SEO through to custom WordPress plugin development. I transform ideas into dynamic, engaging, and high-performing solutions.
Subscribe to get the latest insights & updates in the world of web and how it impacts your business and website.
© 2024 Phil Kurth  |  All rights reserved.