A GitHub Action for automated deployment to WP Engine
At 4/19/2024
We recently set up a GitHub Action to automatically upload our site updates to WP Engine whenever we push to a specific branch. It took us a couple tries to get it right, but we are pleased with our solution, and we suspect other teams might find it useful, too.
Why Automated Deployments?
If a site is managed by only one person, manually deploying updates might work just fine. There are several straightforward ways to upload code to WP Engine:
-
git push
- SFTP uploads
- Local app (formerly Local by Flywheel)
If multiple people are working on the site, manual deployment can become unpredictable and cumbersome. As the number of contributors increases, so does the need for coordination. Therefore, we prefer a workflow that deploys our code automatically. This saves us time we would have spent on repetitive deployment sequences, and we always know exactly which code is live on the server.
The GitHub Action
GitHub Actions allow us to automate our development workflow from within the repository. There is a marketplace of shared GitHub Actions to choose from, but despite the popularity of WordPress and WP Engine, we were not able to find one that was current and met our requirements. So we wrote our own.
GitHub Actions are defined in a “Workflow” file that is written in yaml. Our Workflow instructs GitHub to upload changes to WP Engine whenever new commits arrive in the develop
branch. This is the complete solution, but we will look at some handy additions further below.
name: Auto-deploy
on:
push:
branches:
- develop
env:
WPENGINE_ENVIRONMENT_NAME: cloudFourDev
WPENGINE_SSH_KEY_PRIVATE: ${{secrets.WPENGINE_SSH_KEY_PRIVATE}}
WPENGINE_SSH_KEY_PUBLIC: ${{secrets.WPENGINE_SSH_KEY_PUBLIC}}
jobs:
deploy_to_wpengine:
name: Deploy to WP Engine
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
# CONFIGURE SSH
- run: mkdir ~/.ssh
- run: echo "$WPENGINE_SSH_KEY_PRIVATE" >> ~/.ssh/wpekey
- run: echo "$WPENGINE_SSH_KEY_PUBLIC" >> ~/.ssh/wpekey.pub
- run: chmod 600 ~/.ssh/wpekey
- run: chmod 644 ~/.ssh/wpekey.pub
- run: ssh-keyscan -t rsa "$WPENGINE_ENVIRONMENT_NAME.ssh.wpengine.net" >> ~/.ssh/known_hosts
# PUSH
- run: rsync --itemize-changes -av -e "ssh -i ~/.ssh/wpekey" $GITHUB_WORKSPACE/ ${WPENGINE_ENVIRONMENT_NAME}@${WPENGINE_ENVIRONMENT_NAME}.ssh.wpengine.net:/home/wpe-user/sites/$WPENGINE_ENVIRONMENT_NAME/
Code language: YAML (yaml)
Let’s take a closer look at this code and explain what each part is doing.
Naming and configuring the Workflow
name: Auto-deploy
on:
push:
branches:
- develop
Code language: YAML (yaml)
Here we give this workflow a name:
and then the on
property specifies that the following jobs should run whenever new commits are pushed to the develop
branch.
Environment variables
Next, we set some environment variables we are going to reference in the commands that follow.
env:
WPENGINE_ENVIRONMENT_NAME: cloudFourDev
WPENGINE_SSH_KEY_PRIVATE: ${{secrets.WPENGINE_SSH_KEY_PRIVATE}}
WPENGINE_SSH_KEY_PUBLIC: ${{secrets.WPENGINE_SSH_KEY_PUBLIC}}
Code language: YAML (yaml)
The WPENGINE_ENVIRONMENT_NAME
is set to the unique name assigned to the WP Engine site. This value will match the default subdomain that WP Engine provides. For example, if your site is available at awesomesite.wpengine.com then the value of this property would be awesomesite
.
The following two environment variables are SSH keys that GitHub will use for authentication when it connects with WP Engine. GitHub provides a helpful guide for how to generate an SSH key as well as add them as encrypted secrets in GitHub. It is also necessary to add the public key to WP Engine.
Defining a job
jobs:
deploy_to_wpengine:
name: Deploy to WP Engine
runs-on: ubuntu-latest
Code language: YAML (yaml)
A workflow can have more than one job, but we only define one. The deploy_to_wpengine
property name is arbitrary, and then the name
property is what will appear in the GitHub UI. The runs-on
property is mandatory, and tells GitHub which virtual environment to use. For our purposes, the latest version of Ubuntu is fine.
Checkout the project files
steps:
- uses: actions/checkout@v2
Code language: YAML (yaml)
Now things start to get good! The steps
property is an array of actions that our job will do in the specified order. This first step invokes the prebuilt checkout action that is provided by GitHub. This triggers a git checkout
that makes the project files available in the environment where the job is running.
Configuring SSH
So far, we have the files we want to upload. And we have SSH keys stored in variables. But the virtual environment is not yet configured to use these keys. So that is what happens next.
# CONFIGURE SSH
- run: mkdir ~/.ssh
- run: echo "$WPENGINE_SSH_KEY_PRIVATE" >> ~/.ssh/wpekey
- run: echo "$WPENGINE_SSH_KEY_PUBLIC" >> ~/.ssh/wpekey.pub
- run: chmod 600 ~/.ssh/wpekey
- run: chmod 644 ~/.ssh/wpekey.pub
- run: ssh-keyscan -t rsa "$WPENGINE_ENVIRONMENT_NAME.ssh.wpengine.net" >> ~/.ssh/known_hosts
Code language: YAML (yaml)
We write the public and private keys into the ~/.ssh
directory and set the required permissions. We also append the WP Engine SSH endpoint to known_hosts
so our script is not interrupted by the interactive prompt that asks if we want to add it.
rsync
Uploading with rsync
Finally, the moment has arrived! We are about to send our updated code off to WP Engine using the mighty rsync
, which is a powerful tool for synchronizing local and remote files reliably and efficiently.
# PUSH
- run: rsync --itemize-changes -av -e "ssh -i ~/.ssh/wpekey" $GITHUB_WORKSPACE/ ${WPENGINE_ENVIRONMENT_NAME}@${WPENGINE_ENVIRONMENT_NAME}.ssh.wpengine.net:/home/wpe-user/sites/$WPENGINE_ENVIRONMENT_NAME/
Code language: YAML (yaml)
rsync
can do many things, but we will only look at the options being used here.
- The flag
--itemize-changes
adds a more verbose output message explaining which files were transferred. This is helpful for debugging because the output appears in the GitHub Action logs. - The
-av
options allow rsync to work recursively on the contents of directories and log the output verbosely. - The
-e
option is how we specify the SSH configurations, ensuring the SSH keys we set up earlier get used. - The rest of the command specifies the local path to files we want to sync, followed by the remote host (whose name we build dynamically from the environment name) and finally the path after the colon is the location on the remote server that we want to sync to.
For simple projects, what we have done so far may be entirely sufficient. But we encountered some complexities that required a bit more work.
Our site uses third-party libraries that are not handled by the code we’ve seen so far. We also noticed that WP Engine’s caching layers were making it difficult for us to see our newest updates. The next two sections show our solutions for these issues.
Untracked dependencies
Our theme uses some third-party libraries which are not tracked in version control. They would never get deployed by the workflow above because only versioned files are provided by the checkout
step. So we added an additional step before the final rsync
command:
- run: cd ${GITHUB_WORKSPACE}/wp-content/themes/cloudfour && npm ci
Code language: YAML (yaml)
This step navigates to the theme directory and then installs the dependencies specified in package-lock.json
. With this in place, node_modules
will also get deployed with the rest of our code. This command could be replaced with any command(s) needed to replicate the production installation (yarn
, bower
, compser
, etc).
Caching
Finally, we encountered one more snag: WP Engine has several layers of caching. These are great for performance, but it means changes to theme templates or front-end assets will not automatically become visible. You can manually flush all these caches from inside the WordPress dashboard, or from the WP Engine UI, but this defeats the purpose of an automated deployment.
At the time of writing this, WP Engine does not offer a programmatic way to flush all caches, but there is a simple custom WordPress plugin we can install to accomplish this with a GET request to the site. Here’s how to set that up:
- Install this custom plugin.
- Define a new constant in your
wp-config.php
file that will act as an auth token to restrict who can flush the caches. Ex:define('WPE_CACHE_FLUSH', 'secretpassword');
- Add another encrypted secret in GitHub with a value that matches the token defined in the previous step.
- Add this additional secret to the environment variables at the top of the workflow. (The syntax will match the way we referenced the SSH key secrets.)
- Append the following action at the end of the steps in the workflow:
- run: curl https://${WPENGINE_ENVIRONMENT_NAME}.wpengine.com?wpe-cache-flush=$WPENGINE_FLUSH_CACHE_TOKEN
Code language: YAML (yaml)
This last action we added makes a simple GET request to the site, which triggers the plugin we just installed to flush all the caches. It only works if the secret passed in the query string matches the token we defined in wp-config.php
, otherwise nothing will happen.
Conclusion
With these last few additions, we achieved our goal. Whenever we push new commits to the develop
branch, those changes automatically go live on our development site and we can view them right away because the cache is also flushed. We never need to ask if something has been deployed yet; if the code is merged into the develop
branch, it is. No need to wrangle credentials and remember each step of a manual deployment sequence. Instead, we push our work and move on to the next issue.