Hugo & GitHub Actions

March 28, 2021

Since I wrote this post, the process I was explaining here has been simplified A LOT by GitHub. You can now achieve the same result in just a couple of clicks from any of your repo main page; though it still work this way too.

I'm writing this mostly so I can remember what I did to make it work. This might not be super comprehensive.

Developing my website

I use Hugo to generate my website. I write my templates, stylesheets and scripts with VS Code and thanks to the hugo serve command, I can see the results instantly on my Internet browser at localhost:1313.

When I'm satisfied, I use Git to commit my changes. Git keeps track of every single line of code that I wrote, modified or deleted at any time. This source control repository lives on my computer and can be easily analyzed with VS Code & its GitLens extension. Even for a small, single-user project like a personal blog, it's super fun to see it evolve and being able to look back in time or visualize heatmaps of my edits.

Setting up my live website

Now I could run the hugo command in my terminal to generate Internet-ready files on my computer. Then I could initialize a new Git repository out of that new folder and I could push its content online every time that I want to edit something, but please no.

I certainly don't want to deal with the Internet-ready stuff. They're the standalone HTML files now stripped of all of Hugo's logic and functions. They're the different sizes thumbnails of my processed images. They are the transpiled, concatanated and minified CSS and JS ... They are Hugo-stein's monster, a necessary evil to display static pages properly on everyone's browser as fast as possible.

I often make local changes that don't even result in anything different regarding the public website, but they improve the way I do things on my end. If I have to push something online related to this project, it has to be my development files. They are organized, human-readable and workable. Posts are written in plain text Markdown in a dedicated folder. Layouts are separated in another folder. I have partials for common sections like navigation or footer, that I can refer to in my layouts so that I never have to write the same code twice. CSS and JS can also be produced with whatever library I want to use.

On top of that, if those are accessible on GitHub then I could update them with my credentials from anywhere in a matter of seconds. When I come back to my main computer, I can just git pull on my own remote repository and continue where I left off.

Now I know what I need so here's what I can do:

  • I develop my local website with Hugo as usual.
  • I commit and push it to the main branch of a new remote repository on GitHub. This branch is synced with my local one.
  • In the new GitHub repository only, I create another branch, empty for now, called live. I won't access it manually.

My subdomain (www) will point directly to the live branch and that's what the public will see. GitHub Pages requires a CNAME file to be present at the root of the repository when using a custom domain name so I prep Hugo to generate this file on each future build of my website.

OK so how can I push my development files to the main branch and have a normal website automatically come out of it in the live branch?

Enters GitHub Actions

GitHub Actions is a relatively recent feature offered by GitHub for both public and private repos. It helps automate workflow and it has a marketplace for published user-made actions.

First, I need to give permission to GitHub Actions to perform the job on my behalf otherwise it's going to fail. This is done directly on the GitHub website. The permission is a token that GitHub can help me customize in Settings > Developer settings > Personal access tokens. Once I have one with my workflow permissions, I go to my Repository Settings > Secrets and add it there with the name LXP_TOKEN. This name will be referred to later but its token value will always stay hidden.

Then I create and push a YAML file .github/workflows/deploy_hugo.yml to my repo main branch. This is a series of jobs instructions. Because it's YAML indentation matters.

# I call this action "Deploy Hugo".

name: Deploy Hugo

# It will be triggered everytime there's a push event on the main branch of
# my repo. It could also be triggered manually at any time on the repo Actions
# tab to test the action, or paused or disabled.

on:
  push:
    branches:
      - main

# Now I tell GitHub that it has a single multi-steps job to do called "deploy"
# (not a reserved keyword) and that it should perform it on the latest version
# of Linux Ubuntu.

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:

      # First I want GitHub to checkout on my repo so it can work with it. This
      # step uses a convenient built-in action named checkout@v2 found in the
      # marketplace.

      - name: Checkout
        uses: actions/checkout@v2

      # Then GitHub needs to setup the same Hugo environment as mine on its
      # Linux VM to obtain the same results as I do when I process my files.
      # Fortunately, a user-made action that does that exists and accepts
      # custom settings. I use Hugo Extended because it comes with Sass.

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

      # Then GitHub needs to generate my static pages by simply running the
      # hugo command in the shell.

      - name: Build Hugo
        run: hugo

      # Finally, GitHub needs to publish those to my 'live' branch. Because
      # it will now make actual changes to my repository, it needs its secret
      # token to do so. Also, force_ophan set to true means that every time
      # this step is performed, the commit history for the target branch will
      # be erased because I don't care about it.

      - name: Publish
        uses: peaceiris/actions-gh-pages@v3
        with:
          personal_token: ${{ secrets.LXP_TOKEN }}
          publish_branch: live
          force_orphan: true

As you can see, half those steps are other user-made actions. Thanks peaceiris!

This particular job took about 10 seconds to perform and GitHub shows exactly how long each step took. Setting up the VM takes 2s, the "Build Hugo" step takes ~350ms, etc. GitHub also gives access to verbose logs for each job ever performed.

This is important if you use a lot of actions or your build takes longer because there's a quota of minutes each month on private repos, depending on your account. Free accounts get 2,000 minutes/month on private repos for example. This quota gets eaten 2x faster on Windows, and 10x faster on macOS, so it's best to tell it to run your actions on Linux unless it can't.