Hugo Deployment via GitHub Actions

Using GitHub actions to build and deploy a static site is fun.

For a long time I’ve been managing deployment of a Hugo site via a few home-grown scripts and actions. All that has changed.

The Setup

So let’s get our setup explained:

  1. Hugo is used to manage a library of data
  2. The posts and the theme are in the same repo, but stored under ‘Content’ (which has data, posts, and static) and themes (which has … the theme)
  3. Most of the changes are in the post content

Okay, now this is not a conversation about why (or why not) use Hugo. I like it for my pretty much text-only wiki type content, in that it lets me keep things organized and usable from everything including my iPad.

But this use of Hugo comes with a cost. One of the reasons people love CMS tools like WordPress is that you can edit in your browser, and frankly that’s super easy. Using a static site builder, you have to run (somewhere) the static site build command. For a while I had a ‘deployme’ local command that did this:

$ hugo -F -s /home/username/Development/site/hugo
$ rsync -aCt --delete --exclude-from '/home/username/Development/rsync-exclude.txt' --force --omit-dir-times -e ssh /home/username/Development/site/hugo/ user@example.com:/home/username/domain/hugo

Not super complicated, right? I have it on my laptop but it means I can’t always push code. Like if I edit directly on Github or on my iPad.

Normally I’d look into something like Codeship (which I’ve talked about before) but … I thought it was high time I sat down and made things simpler.

What’s Simpler?

In this case, simpler means “fewer moving parts that could go wrong.”

See I love Codeship, it lets me do a lot of cool things, but it’s also a little fragile and (honestly) sucky when it comes to Hugo. Creating a server and running hugo took longer than it did on my laptop. A lot longer. Many minutes longer.

If I ran it locally it was a few seconds:

Start building sites …
hugo v0.87.0+extended darwin/arm64 BuildDate=unknown

                   |  EN
-------------------+-------
  Pages            | 1762
  Paginator pages  |    0
  Non-page files   |    0
  Static files     |   92
  Processed images |    0
  Aliases          |    2
  Sitemaps         |    1
  Cleaned          |    0

Built in 1562 ms

When I did it on Codeship it would be 5-14 minutes! That’s crazy, right? Which is why I moved off Codeship and down to local. But that came with the cost of limiting when and where I could run anything. While my code is super simple, it’s also silo’d and that’s bad.

In order to achieve simplicity, what I really needed is code that runs from Github. Or on the server where the site is. Back in ‘the day’ I installed hugo on the server, but also Git! That means I pushed to my git repo, which was on the same server, and I used post-commit hooks to deploy. I’ve toyed around with a few iterations, but then I moved to a new server where I didn’t install Go because … I didn’t need it.

And that means here, simple is:

  • runnable from anywhere
  • automated
  • restricted when needed
  • not crossing multiple services

Which led me to Github actions.

Github Actions

This is a service from Github.

GitHub Actions makes it easy to automate all your software workflows, now with world-class CI/CD. Build, test, and deploy your code right from GitHub. Make code reviews, branch management, and issue triaging work the way you want.

In other words, Github saw us all using Travis and Codeship and thought “We could do that and keep people here, right?”

But Actions goes beyond just automation. The Actions interface allows you to run tests, builds, checks, and, yes, deploys. It’s an order of magnitude faster than tools like Codeship because it’s a stripped down, basic interface. It’s also controlled by Github so you don’t need more access than committing code.

There are some cons, though. One of the headaches with Codeship was that when Hugo updated, Codeship might just … stop working right. So you had to find the magic sauce to make it work. With Github Actions, you’re using ‘actions’ built by other people a lot of the time, and if you’re familiar with the drama that happened in npm a while ago, you may share my fear of “What if someone else deletes their action…?”

Yeah, I have concerns/

main.yml

Here’s my code:

name: 'Generate and deploy'

on:
  push:
    branches: [ production ]

jobs:
  deploy-website:
    runs-on: ubuntu-latest
    steps:
      - name: Do a git checkout including submodules
        uses: actions/checkout@v2
        with:
          submodules: true

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v2
        with:
          hugo-version: 'latest'
          # extended: true

      - name: Build Hugo
        run: hugo --minify

      - name: Deploy to Server
        uses: easingthemes/ssh-deploy@main
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SERVER_SSH_KEY }}
          ARGS: "-rlgoDzvc -i"
          SOURCE: "public/"
          REMOTE_HOST: ${{ secrets.REMOTE_HOST }}
          REMOTE_USER: ${{ secrets.REMOTE_USER }}
          TARGET: "/home/username/domain/library/"
          #EXCLUDE: "/dist/, /node_modules/"

There are a number of alternatives, but I picked peaceiris/actions-hugo because that developer is well known and respected. And while there are all-in-one Hugo build and deploy, I decided to separate them because I linked peaceiris’ code. This meant I needed an Rsync or ssh deployment. I settled on easingthemes/ssh-deploy because they strongly encouraged the use of secrets, and that’s a good sign to me. Also it’s heavily recommended by Flywheel, and a I cannot imagine them being reckless.

The only ‘gotcha’ I had was the directions about how to setup SSH was not great.

To make it work, you need to create a pem key on the server:

ssh-keygen -m PEM -t rsa -b 4096

Then you need to put that key in a secret (I named mine SERVER_SSH_KEY). But what they don’t mention quite as clearly is what this means:

Private key part of an SSH key pair. The public key part should be added to the authorized_keys file on the server that receives the deployment.

Yes, they’re saying “the public key for your own server has to be on the authorized_keys for the server.” And yes, that’s a weird thing to say, but there it is. That means you copy your own key from your server at ~/.ssh/id_rsa and you paste that in to the end of ~/.ssh/authorized_keys on the same server. Yes, it’s really funky.

My ongoing concerns

I mentioned that there are security issues. I spend a lot of time in WordPress plugin land, where people could commit nefarious code to a plugin any day. Some do. I’ve banned enough people from .org for stupid stuff like that. And some of Github’s advice matches my own: the only way to be safe is to do the damn research yourself.

But that’s not really something most people can do. And it’s something for a longer post in general. My short list of concerns right now is:

  • the action I’m using is deleted
  • the action is edited and breaks my flow/injects malware
  • the action is used to steal my credentials

There are ways to mitigate this

  • I can use actions made and managed and maintained by Github only (those are under the Actions org) — those are unlikely to be deleted and can be trusted as much as Github
  • I can make copies of the actions I want to use (and periodically remember to update them…)
  • I can make use of encrypted secrets to hide sensitive information

But. It’s a risk. And I know it.

%d bloggers like this: