Continuous Deployments for WordPress Using GitHub Actions

Avatar of Steffen Bewersdorff
Steffen Bewersdorff on (Updated on )

Continuous Integration (CI) workflows are considered a best practice these days. As in, you work with your version control system (Git), and as you do, CI is doing work for you like running tests, sending notifications, and deploying code. That last part is called Continuous Deployment (CD). But shipping code to a production server often requires paid services. With GitHub Actions, Continuous Deployment is free for everyone. Let’s explore how to set that up.

DevOps is for everyone

As a front-end developer, continuous deployment workflows used to be exciting, but mysterious to me. I remember numerous times being scared to touch deployment configurations. I defaulted to the easy route instead — usually having someone else set it up and maintain it, or manual copying and pasting things in a worst-case scenario. 

As soon as I understood the basics of rsync, CD finally became tangible to me. With the following GitHub Action workflow, you do not need to be a DevOps specialist; but you’ll still have the tools at hand to set up best practice deployment workflows.

The basics of a Continuous Deployment workflow

So what’s the deal, how does this work? It all starts with CI, which means that you commit code to a shared remote repository, like GitHub, and every push to it will run automated tasks on a remote server. Those tasks could include test and build processes, like linting, concatenation, minification and image optimization, among others.

CD also delivers code to a production website server. That may happen by copying the verified and built code and placing it on the server via FTP, SSH, or by shipping containers to an infrastructure. While every shared hosting package has FTP access, it’s rather unreliable and slow to send many files to a server. And while shipping application containers is a safe way to release complex applications, the infrastructure and setup can be rather complex as well. Deploying code via SSH though is fast, safe and flexible. Plus, it’s supported by many hosting packages.

How to deploy with rsync

An easy and efficient way to ship files to a server via SSH is rsync, a utility tool to sync files between a source and destination folder, drive or computer. It will only synchronize those files which have changed or don’t already exist at the destination. As it became a standard tool on popular Linux distributions, chances are high you don’t even need to install it.

The most basic operation is as easy as calling rsync SRC DEST to sync files from one directory to another one. However, there are a couple of options you want to consider:

  • -c compares file changes by checksum, not modification time
  • -h outputs numbers in a more human readable format
  • -a retains file attributes and permissions and recursively copies files and directories
  • -v shows status output
  • --delete deletes files from the destination that aren’t found in the source (anymore)
  • --exclude prevents syncing specified files like the .git directory and node_modules

And finally, you want to send the files to a remote server, which makes the full command look like this:

rsync -chav --delete --exclude /.git/ --exclude /node_modules/ ./ [email protected]:/mydir

You could run that command from your local computer to deploy to any live server. But how cool would it be if it was running in a controlled environment from a clean state? Right, that’s what you’re here for. Let’s move on with that.

Create a GitHub Actions workflow

With GitHub Actions you can configure workflows to run on any GitHub event. While there is a marketplace for GitHub Actions, we don’t need any of them but will build our own workflow.

To get started, go to the “Actions” tab of your repository and click “Set up a workflow yourself.” This will open the workflow editor with a .yaml template that will be committed to the .github/workflows directory of your repository.

When saved, the workflow checks out your repo code and runs some echo commands. name helps follow the status and results later. run contains the shell commands you want to run in each step.

Define a deployment trigger

Theoretically, every commit to the master branch should be production-ready. However, reality teaches you that you need to test results on the production server after deployment as well and you need to schedule that. We at bleech consider it a best practice to only deploy on workdays — except Fridays and only before 4:00 pm — to make sure we have time to roll back or fix issues during business hours if anything goes wrong.

An easy way to get manual-level control is to set up a branch just for triggering deployments. That way, you can specifically merge your master branch into it whenever you are ready. Call that branch production, let everyone on your team know pushes to that branch are only allowed from the master branch and tell them to do it like this:

git push origin master:production

Here’s how to change your workflow trigger to only run on pushes to that production branch:

name: Deployment
on:
  push:
    branches: [ production ]

Build and verify the theme

I’ll assume you’re using our WordPress starter theme Flynt, which comes with dependency management via Composer and npm as well as a preconfigured build process. If you’re using a different theme, the build process is likely to be similar, but might need adjustments. And if you’re checking in the built assets to your repository, you can skip all steps except the checkout command.

For our example, let’s make sure that node is executed in the required version and that dependencies are installed before building:

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    
    - uses: actions/setup-node@v2
      with:
        node-version: 12.x

    - name: Install dependencies
      run: |
        composer install -o
        npm install

    - name: Build
      run: npm run build

The Flynt build task finally requires, lints, compiles, and transpiles Sass and JavaScript files, then adds revisioning to assets to prevent browser cache issues. If anything in the build step fails, the workflow will stop executing and thus prevents you from deploying a broken release.

Configure server access and destination

For the rsync command to run successfully, GitHub needs access to SSH into your server. This can be accomplished by doing the following:

  1. Generate a new SSH key (without a passphrase)
  2. Add the public key to your ~/.ssh/authorized_keys on the production server
  3. Add the private key as a secret with the name DEPLOY_KEY to the repository

The sync workflow step needs to save the key to a local file, adjust file permissions and pass the file to the rsync command. The destination has to point to your WordPress theme directory on the production server. It’s convenient to define it as a variable so you know what to change when reusing the workflow for future projects.

- name: Sync
  env:
    dest: '[email protected]:/mydir/wp-content/themes/mytheme'
  run: |
    echo "${{secrets.DEPLOY_KEY}}" > deploy_key
    chmod 600 ./deploy_key
    rsync -chav --delete \
      -e 'ssh -i ./deploy_key -o StrictHostKeyChecking=no' \
      --exclude /deploy_key \
      --exclude /.git/ \
      --exclude /.github/ \
      --exclude /node_modules/ \
      ./ ${{env.dest}}

Depending on your project structure, you might want to deploy plugins and other theme related files as well. To accomplish that, change the source and destination to the desired parent directory, make sure to check if the excluded files need an update, and check if any paths in the build process should be adjusted. 

Put the pieces together

We’ve covered all necessary steps of the CD process. Now we need to run them in a sequence which should:

  1. Trigger on each push to the production branch
  2. Install dependencies
  3. Build and verify the code
  4. Send the result to a server via rsync

The complete GitHub workflow will look like this:

name: Deployment
on:
  push:
    branches: [ production ]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - uses: actions/setup-node@v2
      with:
        node-version: 12.x
    - name: Install dependencies
      run: |
        composer install -o
        npm install
    - name: Build
      run: npm run build
    
    - name: Sync
      env:
        dest: '[email protected]:/mydir/wp-content/themes/mytheme'
      run: |
        echo "${{secrets.DEPLOY_KEY}}" > deploy_key
        chmod 600 ./deploy_key
        rsync -chav --delete \
          -e 'ssh -i ./deploy_key -o StrictHostKeyChecking=no' \
          --exclude /deploy_key \
          --exclude /.git/ \
          --exclude /.github/ \
          --exclude /node_modules/ \
          ./ ${{env.dest}}

To test the workflow, commit the changes, pull them into your local repository and trigger the deployment by pushing your master branch to the production branch:

git push origin master:production

You can follow the status of the execution by going to the “Actions” tab in GitHub, then selecting the recent execution and clicking on the “deploy“ job. The green checkmarks indicate that everything went smoothly. If there are any issues, check the logs of the failed step to fix them.

Check the full report on GitHub


Congratulations! You’ve successfully deployed your WordPress theme to a server. The workflow file can easily be reused for future projects, making continuous deployment setups a breeze.

To further refine your deployment process, the following topics are worth considering:

  • Caching dependencies to speed up the GitHub workflow
  • Activating the WordPress maintenance mode while syncing files
  • Clearing the website cache of a plugin (like Cache Enabler) after the deployment