A GitHub Action for automated deployment to WP Engine

Written by Emerson Loustau on

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:

SpongeBob with nervous sweat
Worrying about manual deployments causes SpongeBob a lot of anxiety

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/

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

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}}

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

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

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

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.

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/

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.
SpongeBob contemplates

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

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:

  1. Install this custom plugin.
  2. 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');
  3. Add another encrypted secret in GitHub with a value that matches the token defined in the previous step.
  4. 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.)
  5. 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

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

SpongeBob jumps for joy
SpongeBob lives his best life thanks to automated deployments.

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.

Emerson Loustau

Emerson Loustau is a designer, developer, and problem solver. He also publishes thoughts and experiments on his website.

Never miss an article!

Get Weekly Digests


Leave a Comment

Please be kind, courteous and constructive. You may use simple HTML or Markdown in your comments. All fields are required.


Let’s discuss your project! Email Us