← All posts

Automating WordPress Deploys: My Journey from FTP to GitHub Actions and WP-CLI

Automating WordPress Deploys: My Journey from FTP to GitHub Actions and WP-CLI

I remember the dread of a Friday afternoon deploy. FTPing files, maybe an `rsync -avz` if I was feeling fancy, then SSHing in to run a `wp cache flush`. More often than not, I'd forget `wp db update` after a schema change in my plugin, or miss a `composer install` for a new dependency. That led to bizarre frontend errors, or worse, backend failures that only surfaced when an admin tried to save something. The frantic rollback, the lost 15 minutes of trying to remember which files I had actually updated. It felt like I was defusing a bomb every time.

That's why I'm evangelical about proper deploy pipelines using WP-CLI and GitHub Actions for any WordPress project that matters. We moved to this setup about two years ago for a complex e-commerce integration, and it's been a game changer. The initial setup was a push, but the headaches it saved later are immeasurable.

Here's the core idea: your GitHub repository is the source of truth. When code lands on `main`, a GitHub Actions workflow kicks off. This workflow connects to your server via SSH, pulls the latest code, runs WP-CLI commands, and handles any Composer dependencies.

A typical workflow for our custom CRM plugin, for instance, looks something like this: first, we make sure the server has PHP and Composer. Then, the workflow clones the repository. Next, `composer install --no-dev` runs to grab all our backend library dependencies. This is crucial. I once spent an hour debugging a production site because a new class wasn't found; turned out I'd forgotten to commit the `vendor` directory and the automated `composer install` was never set up for that particular deployment target. We learned that lesson hard.

After dependencies, WP-CLI takes over. `wp cache flush --all` is usually the first thing, clearing any persistent object cache or transient data that might hold stale information. Then, if our plugin has any schema changes, `wp db update` runs. This is non-negotiable. I can't stress enough how many times I personally overlooked a `db_schema_version` increment in a custom table definition, a simple `dbDelta` call, leading to functions trying to access non-existent columns. The site just wouldn't work right until I manually ran the command. Automating it prevents that whole class of error. We even have `wp rewrite flush` for any custom post type or taxonomy slug changes, which avoids the dreaded 404s after a deploy.

The key to all of this, especially with GitHub Actions, is secure SSH access and environment variables. You set up an SSH key on your server (read-only for deployments if possible), add the private key as a GitHub Secret, and then your workflow uses that to connect. This means no FTP credentials lying around, no shared passwords. It's a clean, auditable process.

Some might argue that building a full artifact, zipping the plugin with `vendor` already included, and deploying that is cleaner than `composer install` on the server. And yes, it absolutely can be, especially for very large projects or multiple environments. For us, with dedicated staging and production servers that are relatively robust, running `composer install` directly has been fine and slightly simpler to configure in the workflow. The trade-off is slightly longer deploy times, but we’re talking seconds, not minutes.

The biggest win? Our deployment-related bug reports dropped by 80% within the first month. Our internal "rollback" count, where we had to revert a deploy entirely, went from 3-4 times a month to practically zero. That’s real time saved, real stress avoided. If you’re not automating your WordPress deploys with WP-CLI and GitHub Actions, you’re leaving stability and developer sanity on the table.