Managing WordPress Dev Environments With WP-CLI and Robo

Automating repetitive tasks is one of the best ways to save time in your development workflow. In my day to day work as a plugin developer, I often have to reset the database, reinstall a specific version of WordPress, install one or more plugins, and update the settings. This quickly gets tiresome, so it’s a natural fit for automation. In this article, I’ll show you how I use WP-CLI in combination with Robo to automate the tasks needed to manage my WordPress development environment.

The WordPress command line tool, WP-CLI, is a great start to speeding up your workflow. If you’re not familiar with it, I highly recommend reading one of our many blog posts. Our installation guide is a great starting point, laying out how to install it on your OS, set up tab completion, and create a configuration file.

Table of Contents

  1. What is Robo?
    1. Robo Simplifies Command Line PHP
    2. Installing Robo
    3. Your First Command
    4. Input and Output
    5. Great Helpers
    6. Benefits of Robo
  2. Maintaining WordPress With Robo
    1. The Important Files
    2. My Environment
    3. Database User
    4. Web Server
    5. Taking Care of Dependencies
  3. Leveraging wp-cli.yml
    1. What Is an Alias in WP-CLI?
    2. Command Defaults
  4. Creating Custom Robo Commands
    1. Command Configuration
    2. The reset Command
    3. Post-Install Steps
    4. The profile Command
  5. Wrapping Up

What is Robo?

Robo is an open source modern task runner used by a number of projects including Drush and Codeception.

Robo is similar to Gulp and Grunt, but using PHP rather than JavaScript.

The idea is that you can create nifty little commands like this:

# Reset my WP dev environment and make it multisite
robo reset --multi

# Install and setup the WP Offload Media plugin
robo profile ome-dev

The Robo task runner makes it easy to document the commands you create by adding DocBlock comments to your commands, so you can provide help for future versions of yourself:

# Robo without any arguments will show the available commands.
robo

Available commands:
  help     Displays help for a command
  list     Lists commands
  profile   Run a set of wp-cli commands on an existing WordPress environment
  reset    Reset the WordPress environment to a known state

Asking for more info about a specific command:

robo help reset

Description:
  Reset the WordPress environment to a known state. Reads environments and configuration 
  from robo.yml

Usage:
  reset [options]

Options:
      --env[=ENV]                      Environment (dev, test etc) [default: "dev"]
      --multi                          Make a multi site install
      --ver[=VER]                      WordPress version [default: "latest"]

I’ve created some powerful commands with Robo that I use every day to manage my WordPress development environment. In this article, I’m going to share these commands with you and provide an introduction to Robo commands along the way.

Robo Simplifies Command Line PHP

Let’s check the rear view mirror just a bit. Writing command-line PHP commands is certainly not new. It’s always been possible to just run a PHP script from the command line like this:

php myscript.php

And PHP has been available as a command line environment in most *NIX environments for as long as I can remember. Adding the PHP shebang will work in most environments that have the PHP interpreter installed.

// file: myscript

#!/usr/bin/env php
<?php
// do stuff here…

Which makes it possible to execute the script from the command line without specifying that it should be parsed by PHP (or including the .php file extension):

myscript

The downside of running raw PHP scripts from the command line is that there’s quite a lot of overhead for input and output to take care of in each script. The process of accepting arguments from the command line, and then outputting the messages to the command line is a bit cumbersome and doesn’t feel very flexible.

Robo sets out to make it easier to write command line PHP by taking care of a lot of the standard “plumbing” that most scripts need. This lets you focus on the core functionality of your scripts.

Installing Robo

You’ll need to install Robo before we can get started. I keep Robo installed globally on my machine, so I’ve installed it like this:

composer global require consolidation/robo

But as with anything installed via Composer, you can keep it as a project dependency if you’re more comfortable with that. Alternatively, the GitHub has instructions on installing it by downloading robo.phar.

Your First Command

First, we need to initialize Robo in the project folder:

cd /path/to/myproject
robo init

This really just creates a new RoboFile.php in your folder with the following content:

<?php
class RoboFile extends \Robo\Tasks
{
}

To add our first command, we just add a public method:

<?php
class RoboFile extends \Robo\Tasks
{
   public function hello($world)
    {
        $this->say("Hello, $world");
    }
}

As you can probably guess, the above method creates the command hello and simply outputs a message on the screen.

Parsing Arguments

Just adding that method like we did above is a great way to show one of the more important reasons why I like Robo, namely parsing command line arguments.

To show you what I mean, let’s try to run this command:

robo hello

Not enough arguments (missing: "world").

hello [-h|--help] [-q|--quiet] [-v|vv|vvv|--verbose] [-V|--version] [--ansi] [--no-ansi] [-n|--no
-interaction] [--simulate] [--progress-delay PROGRESS-DELAY] [-D|--define DEFINE] [--]  

Aha! Robo gives me an error message because the hello command actually expects a $world parameter and then proceeds to write out the full usage syntax for the command.

Let’s change the method a bit and make the parameter optional:

   public function hello($world = ‘from Robo’)
    {
        $this->say("Hello, $world");
    }

…and now let’s run it again:

# Without arguments
robo hello
Hello, from Robo

# With a simple argument
robo hello there!
Hello, there!

# With an arg containing spaces
robo hello “I live on the command line”
Hello, I live on the command line

That’s better! By making the parameter optional, Robo now happily executes our method even without passing an argument. We’ve also seen that we can pass a simple argument and an argument within quotes and it just works.

If you ever spent any time writing argument checking logic for a command line script you probably realize why this is a nice feature. It just takes a lot of pain away.

The example hello command uses one single positional argument. Robo also supports using flags and named arguments by creating an array argument with some default values.

Let’s modify the function further to optionally print out some additional characters:

public function hello( $world = 'from Robo', $flags = [ 'stars' => false, 'stripes' => false ] ) {

  if ( $flags['stars'] ) $this->say( '***************' );
  if ( $flags['stripes'] ) $this->say( '===============' );

  $this->say( "Hello, $world" );

  if ( $flags['stripes'] ) $this->say( '===============');
  if ( $flags['stars'] ) $this->say( '***************' );
}

This will tell Robo that we may optionally pass in named arguments using double dashes:

# Just by including a named argument, it will get the value ‘true’
robo hello --stars
 ***************
 Hello, from Robo
 ***************

Input and Output

We’ve also already seen an example using IO. The say() function of the Robo task object simply outputs a string back to the user. There is also an ask() function that lets you ask the user for input:

public function hello() {
  $word = $this->ask("Tell me what to say:");
  $this->say( $word );
}
robo hello
?  Tell me what to say: foobar
foobar

Robo uses Symfony Console for creating user interactions. This means that besides the two simple say() and ask() functions, we have access to any function from Symfony Console, like table():

public function table() {
  $this->io()->table(
     ['Header 1', 'Header 2'],
     [
        ['Cell 1-1', 'Cell 1-2'],
        ['Cell 2-1', 'Cell 2-2'],
        ['Cell 3-1', 'Cell 3-2'],
     ]
  );
}
robo table
---------- ----------
 Header 1   Header 2
---------- ----------
 Cell 1-1   Cell 1-2
 Cell 2-1   Cell 2-2
 Cell 3-1   Cell 3-2
---------- ----------

Pretty cool, eh?

Great Helpers

Another reason to like Robo is that it has built-in support for many common tasks that makes it easier to write understandable code, even if you’re coming back to it 3 months later. Let’s have a look at how Robo helps with writing very clean code for some pretty standard tasks:

# Create a directory, and switch to it:
$this->taskExecStack()
 ->stopOnFail()
 ->exec('mkdir site')
 ->exec('cd site')
 ->run();

# Search and replace inside a text file:
$this->taskReplaceInFile('VERSION')
 ->from('0.2.0')
 ->to('0.3.0')
 ->run();

# Run composer update:
$this->taskComposerUpdate()->run();

# SSH into a server, go to a specific directory, list the contents of the directory, 
and set permissions on the logs subdirectory
$this->taskSshExec('remote.example.com', 'user')
    ->remoteDir('/var/www/html')
    ->exec('ls -la')
    ->exec('chmod g+x logs')
    ->run();

All of the above is possible using normal PHP and the exec() function, but that quite often becomes a hopeless exercise of trying to glue command strings together and correctly escaping arguments without messing things up too badly.

Besides the examples above, there is also similar support for Git, Subversion, Rsync, Bower, Gulp, Docker, NPM, and other tools often used in development environments.

Benefits of Robo

Taken together, Robo makes creating command line scripts a lot easier and the end result is usually a lot more semantically appealing than plain PHP.

I find that scripts for Robo end up being easier to read and understand compared with pure PHP scripts, because so much of the command line boilerplate stuff is hidden inside Robo itself. Personally, I can look at my own code after a few months and honestly wonder who wrote it, so anything that helps me write clear, readable code is a welcome addition in my tool belt.

Maintaining WordPress With Robo

The rest of this article assumes that you are somewhat familiar with WP-CLI, using Composer, and working with the command line.

The Important Files

There are four important files in this setup. I will cover each one of them throughout this post:

  1. wp-cli.yml – The standard WP-CLI configuration file. I’m using this to leverage several of the great built-in features of WP-CLI when it comes to managing multiple environments.
  2. RoboFile.php – This is the base file for implementing robo commands.
  3. robo.yml – A custom YAML file for some additional configuration parameters for our Robo commands.
  4. composer.json – The standard Composer config file.

My Environment

Just to set the stage for the rest of this article, I’m going to quickly describe how my local development environment is set up. This is a simplified version of how I’ve organized things on disk:

~/src
├── devenv/
│   ├── RoboFile.php
│   ├── composer.json
│   ├── wp-cli.yml
│   ├── robo.yml
│   ├── wordpress-dev/
│   └── wordpress-test/    
├── plugin1/
│   ├── assets/
│   └── include/
└── plugin2
    └── ...

The src folder is where everything development related is stored and the devenv folder holds files specific to the actual runtime environment. Since I usually have more than one WordPress environment running at the same time, each WordPress installation has its own sub folder, named wordpress-dev and wordpress-test in this example.

Each of the plugins I work on sits in a separate folder per plugin.

In the real world, I have multiple devenv folders so that I can keep my work for Delicious Brains separate from various side projects, but that’s not relevant for this article.

Database User

To make everything work I’ve also created local databases for each of the WordPress installations in my local MySQL installation and there’s a database user, aptly named wordpress, with the correct access to those databases. The exact details of the databases as well as the user credentials are stored in the wp-cli.yml file.

Web Server

I’m using nginx as my local web server, but you will find that Apache2 works just as well. My nginx config is set up so that http://www.wordpress-dev.local and http://www.wordpress-test.local point to the two WordPress folders mentioned above.

Taking Care of Dependencies

To make my Robo scripts more flexible, I’m using some additional functionality installed via Composer, specifically the Symfony Yaml parser. Since RoboFile.php is really just a normal PHP file, I’m free to include any libraries I want and I naturally use Composer to do that. The composer.json file for this project looks like this:

{
    "require": {
        "symfony/yaml": "^5.2"
    }
}

If you copy that, don’t forget to actually install the library using composer update.

Leveraging wp-cli.yml

When working with WP-CLI, you can make life a lot simpler by using a configuration file such as wp-cli.yml. I use the WP-CLI configuration file for two main reasons: aliases and setting up defaults for various subcommands.

What Is an Alias in WP-CLI?

At its core, a WP-CLI alias is just a label in the config file that lets you override some of the defaults.

The most common usage of aliases is probably to override the default path so that each alias points to a separate WordPress installation. Since each WordPress installation keeps its own configuration file with database credentials, the alias used this way also represents a separate database. An alias in the WP-CLI configuration file can override the url, path, user, ssh, and http settings, but it can’t override default values for subcommands.

Creating an additional WordPress environment named @test allows me to run WP-CLI commands like this:

# List all plugins in the dev WordPress installation
wp plugin list

# List all plugins in the test WordPress installation
wp @test plugin list

Command Defaults

If you haven’t tried this before, setting up default parameters for subcommands is super handy. For instance, when you create a new WordPress configuration file using the config create command, you need to specify at least three parameters every time:

$ wp config create --dbname=somedb --dbuser=myuser --dbpass=secret

If you ever get tired of typing this, you can stick the parameters into a wp-cli.yml file:

config create:
 dbuser: myuser
 dbpass: secret
 dbname: somedb

Once you’ve done that, you can just use wp config create and it will pick up the right parameters from your wp-cli.yml file.

Unfortunately it’s not possible to set up different command defaults for different aliases. This was actually one of the reasons I started looking at Robo for more automation.

My WP-CLI config file looks like this:

# Global parameter defaults
path: wordpress-dev
url: http://www.wordpress-dev.local
user: admin

@test:
   path: wordpress-test
   url: www.wordpress-test.local

# Subcommand defaults 
config create:
  dbuser: wordpress
  dbpass: *****
  dbname: wordpress
  extra-php: |
    define( 'WP_DEBUG', true );
    define( 'WP_DEBUG_LOG', true);
    define( 'SCRIPT_DEBUG', true );

core install:
  admin_user: admin
  admin_password: admin
  admin_email: [email protected]
  title: WordPress Dev

core multisite-install:
  admin_user: admin
  admin_password: admin
  admin_email: [email protected]

With just this configuration file in place, I’m able to run common commands without having to specify the individual parameters every time:

# Reset the database on the test environment
wp @test db reset --yes

# Download the latest version and create the wp-config.php file
wp @test core download
wp @test config create

# Install WordPress
wp @test core install --title="WordPress Test"

As WP-CLI picks up most of the parameters from the configuration file, I don’t have to type out all the command line parameters like --dbuserand --admin_email that I would normally have to.

Note that in the last example above I provided the title parameter separately. This is because I want the site title to be different in the test environment, but it’s not possible to override this parameter using an alias.

Creating Custom Robo Commands

Setting up a fresh WordPress installation is almost never enough. Usually there are one or more plugins that need to be installed and activated and quite often a few settings to fix up here and there.

Even with a carefully written WP-CLI configuration file, I’d still end up with a long string of commands if I want to reset my WordPress environment and get everything ready. I often did sequences like this over and over again:

# Reset my dev environment
wp db reset --yes
rm -rf path/to/wordpress
wp core download
wp config create
wp core install
ln -s path/to/my/plugin1 path/to/wordpress/wp-content/plugins/
wp plugin activate plugin1

Even when taking full advantage of the WP-CLI config file, this is a lot of typing. To avoid typing this over and over again and also getting it wrong every now and then, I created two specialized commands using Robo to do it for me:

  • reset – Resets a WordPress environment to a known state.
  • profile – Runs a set of commands on an existing environment.

Since the WP-CLI subcommand default parameters don’t extend to different command-line environments, we can use Robo to kill that bird with the same stone. Let’s see how.

Command Configuration

Our first stop is to have a look at the Robo configuration file I’ve created for this. It’s written in YAML, which should make it fairly straightforward to understand and extend. I’ll go over each section as we get to the command that uses it:

wordpress-dev:
  cli-prefix: ""
  path: "wordpress"
  core-multisite-install:
    title: WordPress Multisite
  post-install:
    - ln -s $cwd/../plugins1 $path/wp-content/plugins/
    - ln -s $cwd/../plugins2 $path/wp-content/plugins/

wordpress-test:
  cli-prefix: "@test"
  path: "wordpress-test"
  config-create:
    dbname: wordpress-test
  core-install:
    title: WordPress Test
  core-multisite-install:
    title: WordPress Test Multisite    
  post-install:
    - ln -s $cwd/../plugins1 $path/wp-content/plugins/

profiles:
  woocommerce:
    - $wp plugin install --activate woocommerce
    - $wp wc payment_gateway update cheque --enabled=true --user=admin
    - $wp option update woocommerce_calc_taxes yes
    - $wp wc tax create --name=VAT --rate=10 --user=admin
    - $wp wc shipping_zone_method create 0 --method_id=flat_rate --user=admin
    - $wp option update --format=json woocommerce_flat_rate_1_settings '{"title":"Flat rate","tax_status":"taxable","cost":"15"}'    
  imageimport:
    - $wp media import $cwd/../media/lots-of-images/*
  issue2530:
    - $wp plugin install --activate some_plugin
    - $wp config set FOOBAR true --raw

Each WordPress environment is identified using a wordpress-$env key. Each key can hold several configuration values.

The reset Command

The first command is reset. It’s used from the command line like this:

# Reset the dev (default) environment
robo reset

# Or being more explicit
robo reset –env=dev

# Reset the test environment
robo reset –env=test

# Reset the dev environment and install a WordPress multisite
robo reset --multi

# Reset the dev environment to a specific WordPress version
robo reset --ver=5.6.1

The first thing this command does is to delete all existing files and folders in the target WordPress installation directory and reset the configured WordPress database.

Then, the reset command uses the WP-CLI commands core download, config create and one of core install or core multisite-install depending on the --multi option.

To the largest extent possible, this uses the command parameter defaults located in the wp-cli.yml file. The reason for this is that those defaults are also helpful when running WP-CLI directly without the Robo wrapper. But as discussed above, in some cases it’s just not possible.

Therefore the robo.yml config file offers the possibility to specify overrides for the defaults in wp-cli.yml. For example, when installing the @test environment we want to override the parameters for the core install parameter --title. We can do that by adding the following in robo.yml:

 wordpress-test:
  ...
  ...
  core-install:
    title: WordPress Test
  ...
  ...

The syntax here is quite easy: the CLI command name (spaces replaced with dashes) as the key and one subkey for each named parameter. The above example would generate the following extra parameter to the core install cli command:

wp @test core install --title="WordPress Test"

Post-Install Steps

Each environment key in the config file can optionally specify an array with post-install steps. This is simply a list of bash commands that are executed when the WordPress installation is done. For added flexibility, a few string replacements are made before the command is executed:

String Replaced with
$cwd The current working directory
$path The path of the target WordPress installation
$wp The wp-cli command, including the alias prefix, i.e., wp @test
~ (tilde symbol) The HOME directory of the current user

So a post-install step of ln -s $cwd/../plugin1 $path/wp-content/plugins/ would create a symlink from one of my plugins folders to the plugins subfolder in the target WordPress install.

The profile Command

The profile command is quite similar to the post-install steps, but its intended use is to run a set of commands on an existing WordPress installation. Let’s say that you have a very plain development environment that you do most of your work in. However, sometimes you need to install the WooCommerce plugin and do some basic setup for it. This is what the profile command is for. It can be used like this:

# Reset the dev environment
robo reset

# Install WooCommerce and make some setup changes
robo profile woocommerce

The sample robo.yml file above has a WooCommerce profile. Applying that profile will:

  • Install and activate WooCommerce using WP-CLI.
  • Use the wc sub command to set up a payment gateway, tax zone, and shipping settings.
  • Use the option sub command to modify some settings directly in the WordPress options table.

Using different profiles is quite useful. I spend most of my days working on the WP Offload Media plugin and I often need to import lots of images to the WordPress media library. WP-CLI has a really convenient command for this:

wp import media /some/long/path/I/often/forget/*

Since I often forget a lot of stuff in general and long path names in particular, I find it easier to remember:

robo profile imageimport

Here’s another example. I’ve been working on a particular GitHub issue where we’re trying to fix a compatibility issue between our plugin and another popular WordPress plugin. When I work on that issue, I need to install that plugin and set a config value in wp-config.php. So I created a profile for it:

  ....
  issue2530:
    - $wp plugin install --activate some_plugin
    - $wp config set FOOBAR value 

Now I can get my environment ready to go in one step, just robo profile issue2530.

Just like the post-install steps in the reset command, each row in the profiles definition is really just a separate bash command. You could use it to launch separate scripts, delete files, or whatever you fancy. It’s also quite possible to shoot yourself in the foot, so tread carefully.

The Source

If any of the above sounds interesting to try out, here’s the RoboFile that I’m using for all of the above stuff, feel free to use it to get started with managing WordPress using Robo.

<?php
use Robo\Symfony\ConsoleIO;
use Robo\Tasks;
use Symfony\Component\Yaml\Yaml;

require_once 'vendor/autoload.php';

/**
 * Class RoboFile
 */
class RoboFile extends Tasks {
    /**
     * Reset the WordPress environment to a known state. Reads environments
     * and configuration from robo.yml
     *
     * @option  env   Environment (dev, test etc)
     * @option  multi Make a multi site install
     * @option  ver   WordPress version
     *
     * @return bool
     */
    public function reset( $opts = [ 'env' => 'dev', 'multi' => false, 'ver' => 'latest' ] ) {
        $env       = $opts['env'];
        $version   = $opts['ver'];
        $multi     = $opts['multi'];
        $all_config = $this->read_yaml();
        $key       = "wordpress-$env";

        if ( ! $this->ensure_basic_config( $all_config, $env ) ) {
            return false;
        }

        if ( ! isset( $all_config[ $key ]['path'] ) ) {
            $this->say( "No path set for environment $env." );
        }

        $config = $all_config[ $key ];
        $prefix = $config['cli-prefix'];
        $wp    = trim( "wp $prefix" );
        $path  = $config['path'];
        $path  = substr( $path, 0, 1 ) !== '/' ?
            __DIR__ . '/' . $path :
            $path;

        $version        = $version === 'latest' ? '' : "--version=$version";
        $config_create   = $this->additional_parameters( 'config create', $config );
        $install_cmd    = $multi ? 'core multisite-install' : 'core install';
        $install_params = $this->additional_parameters( $install_cmd, $config );

        echo "$wp $install_cmd $install_params\n";

        $this->taskExec( "$wp db reset --yes" )->run();
        $this->taskExecStack()
            ->exec( "rm -rf $path/*" )
            ->exec( "$wp core download $version" )
            ->exec( "$wp config create $config_create" )
            ->exec( "$wp config delete WP_DEBUG" )
            ->exec( "$wp $install_cmd $install_params" )
            ->run();

        foreach ( $config['post-install'] as $cmd ) {
            $cmd = str_replace( '$wp', $wp, $cmd );
            $cmd = str_replace( '$path', $path, $cmd );
            $cmd = str_replace( '$cwd', __DIR__, $cmd );
            $cmd = str_replace( '~', getenv( "HOME" ), $cmd );
            echo $cmd . "\n";
            $this->taskExec( $cmd )->run();
        }
    }

    /**
     * Run a set of wp-cli commands on an existing WordPress environment
     *
     * @param string                $profileName Name of the profile in robo.yml
     *
     * @option  env   Environment (dev, test etc)
     */
    public function profile( $profileName, $opts = ['env' => 'dev']) {
        $env        = $opts['env'];
        $all_config  = $this->read_yaml();
        $key        = "wordpress-$env";

        if ( ! $this->ensure_basic_config( $all_config, $env ) ) {
            return false;
        }

        $config = $all_config[ $key ];
        $prefix = $config['cli-prefix'];
        $wp     = trim( "wp $prefix" );
        $path   = $config['path'];
        $path   = substr( $path, 0, 1 ) !== '/' ?
            __DIR__ . '/' . $path :
            $path;

        if ( ! isset( $all_config['profiles'][ $profileName ] ) ) {
            $this->say( "Profile $profileName not found" );

            return false;
        }

        $profile = $all_config['profiles'][ $profileName ];
        foreach ( $profile as $cmd ) {
            $cmd = str_replace( '$wp', $wp, $cmd );
            $cmd = str_replace( '$path', $path, $cmd );
            $cmd = str_replace( '$cwd', __DIR__, $cmd );
            $cmd = str_replace( '~', getenv( "HOME" ), $cmd );

            // Quick and dirty. If the cmd exactly matches another profile, run it!
            if ( isset( $all_config['profiles'][ $cmd ] ) ) {
                $this->profile( $cmd, $env );
                continue;
            }

            echo $cmd . "\n";
            $this->taskExec( $cmd )->run();
        }
    }

    /**
     * @return array
     */
    private function read_yaml() {
        return Yaml::parseFile( __DIR__ . '/robo.yml' );
    }

    /**
     * @param $config
     * @param $env
     *
     * @return bool
     */
    private function ensure_basic_config( $config, $env ) {
        $key = "wordpress-$env";

        if ( ! isset( $config[ $key ] ) ) {
            $this->say( "No path set for environment $env." );

            return false;
        }

        if ( ! isset( $config[ $key ]['cli-prefix'] ) ) {
            $this->say( "No wp-cli prefix set for environment $env." );

            return false;
        }

        return true;
    }

    /**
     * @param string                $name
     * @param array<string, string> $config
     *
     * @return string
     */
    private function additional_parameters( $name, $config ) {
        $name = str_replace( ' ', '-', $name );
        if ( ! isset( $config[ $name ] ) ) {
            return '';
        }

        return implode( ' ', array_map(
            function ( $v, $k ) {
                return sprintf( "--%s='%s'", $k, $v );
            },
            $config[ $name ],
            array_keys( $config[ $name ] )
        ) );
    }
}

Wrapping Up

In development, repetitive tasks tend to go with the territory. I can’t see a way of doing away with them completely, but automating as many of them as possible really helps you to get as much real work done with the time available.

Automating some of those tasks with the combination of WP-CLI and Robo that I’ve outlined here has saved time for me every single day as a plugin developer. I could never go back to doing these things manually again.

What tools do you use for automating the most tedious parts of your development workflow? Let me know in the comments.

About the Author

Erik Torsner Senior Software Developer

Well experienced software developer from Stockholm, Sweden with a specific taste for WordPress plugins, software quality and automations. Loves any technology that turns out to be the right solution to the problem.