How to de­ploy a Wordpress Site us­ing Github Actions

I started us­ing Netlify a few weeks ago, and I’ve al­ready got­ten very used to the work­flow it en­ables you to have:

  1. You work on a lo­cal copy of your web­site
  2. You push changes to Github
  3. Netlify no­tices you made a change
  4. It makes a fresh clone of the repos­i­tory
  5. It runs what­ever build process you set up
  6. It takes the re­sult of that build process and de­ploys to the web

This work­flow feels so good to me that I want to have it on every one of my pro­jects — in­clud­ing the few Wordpress web­sites I work on. You can’t just throw a Wordpress site onto Netlify (unless you go the head­less CMS route, but that’s a dif­fer­ent story), but you can still have the nice work­flow by lever­ag­ing Github’s own build sys­tem: Github Actions.

Github Actions

While Netlify’s build process feels very much de­signed to build and de­ploy the web­site when you push to the repos­i­tory, Github ac­tions can pretty much do any­thing you want on any event that can hap­pen in a git repos­i­tory. That’s pow­er­ful, but also means that they need a lit­tle more con­fig­u­ra­tion.

You set up an Action (or Workflow — the ter­mi­nol­ogy is a lit­tle con­fus­ing there) by cre­at­ing a YAML file in a spe­cial folder called .github/workflows at the root of your pro­ject repos­i­tory.

Mine looks like this 1:

name: CI
    branches: [main]

    runs-on: ubuntu-latest
    - uses: actions/checkout@v2
    - name: Install dependencies
      run: yarn install
    - name: Run build command
      run: yarn build
    - name: Deploy via FTP
      run: yarn deploy
        NODE_ENV: production
        FTP_HOST: ${{ secrets.FTP_HOST }}
        FTP_USER: ${{ secrets.FTP_USER }}
        FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}

The on key at the top of the file tells Github when to run the work­flow. In this case, that’s when­ever I push to the main branch.

Then you de­scribe what work you want the work­flow to do. Per Github’s doc­u­men­ta­tion:

Workflows must have at least one job, and jobs con­tain a set of steps that per­form in­di­vid­ual tasks. Steps can run com­mands or use an ac­tion. You can cre­ate your own ac­tions or use ac­tions shared by the GitHub com­mu­nity and cus­tomize them as needed.

My work­flow here has one job called deploy with four steps:

  1. actions/checkout@v2 is an ac­tion writ­ten by Github it­self that down­loads a fresh copy of your repos­i­tory.
  2. Install dependencies runs yarn install which pulls down the de­pen­den­cies I’ve listed in my package.json file.
  3. Run build command trig­gers yarn run build, which in turn is pointed at a gulp task that does the ac­tual work of com­pil­ing my Sass, pack­ag­ing my Javascript and what­ever else I need to do2.
  4. Deploy via FTP runs yarn run deploy, which is pointed at an­other gulp task that up­loads the con­tents of the repos­i­tory (including the files we just built) to the server my Wordpress site lives on.


The last step is in­ter­est­ing: How does the gulp task know how to FTP into my server? I cer­tainly don’t want to put my lo­gin cre­den­tials into my repos­i­tory, but how else could I tell my build process about them? Turns out Github has a mech­a­nism called se­crets that’s de­signed just for this pur­pose.

Instead of stor­ing the se­crets in­side your repos­i­tory (which, again, ter­ri­ble idea), you go into your repos­i­to­ry’s set­tings on Github and en­ter them there, where they’re stored safely and well-en­crypted. The in­ter­face looks like this:

Screenshot showing github secrets interface

Then, you can acess those se­crets dur­ing your work­flows by adding them as en­vi­ron­ment vari­ables to in­di­vid­ual steps  — that’s what the env prop­erty in my YAML file does:

    NODE_ENV: production
    FTP_HOST: ${{ secrets.FTP_HOST }}
    FTP_USER: ${{ secrets.FTP_USER }}
    FTP_PASSWORD: ${{ secrets.FTP_PASSWORD }}

Those en­vi­ron­ment vari­ables make it all the way down into my gulp­file, where I ac­cess them us­ing process.env:

function deploy() {
  const ftpConnection = ftp.create({
    host: process.env.FTP_HOST,
    user: process.env.FTP_USER,
    password: process.env.FTP_PASSWORD,
  // (rest of deployment code omitted)

This to­tally works! Whenever I push to the repos­i­tory, the ac­tion is trig­gered and I can fol­low its progress through this nice UI Github gives you:

Screenshot of github actions interface

My process for work­ing on Wordpress sites now looks like this:

  1. I work on a lo­cal copy of the site
  2. When I’ve made a change, I push it to Github
  3. The work­flow we just de­fined checks out the repos­i­tory
  4. It in­stalls my de­pen­den­cies and runs my build process
  5. It FTPs into my server and up­loads the freshly-built files

Just what I set out to do.

  1. I’m only deal­ing with the Wordpress theme here (i.e a sin­gle folder), but if you had a more com­pli­cated setup (maybe in­volv­ing cus­tom func­tions) you could ex­tend this con­fig­u­ra­tion to ac­co­mo­date for that, too. ↩︎

  2. I like us­ing yarn run build in­stead of the ac­tual gulp com­mand here be­cause it means that when I change my build process, I only have to up­date my package.json file and the Action will still work. It’s also nice not to have to re­mem­ber a whole bunch of dif­fer­ent build com­mands as you switch be­tween pro­jects — it’s al­ways yarn build. ↩︎