Over the years I’ve used various blogging platforms; after a brief dalliance with Blogger I started for real with the near-inevitable Wordpress.com. From there I decided it would be fun to self-host using Ghost, and then almost exactly two years ago to the day decided it definitely was not fun to spend time patching and upgrading my blog platform instead of writing blog articles, so headed over to my current platform of choice: Hugo hosted on GitHub pages. This has worked extremely well for me during that time, doing everything I want from it until recently.
As a static-site generator, Hugo supports the idea of future-dated posts, but you still need to regenerate that static site once the date has arrived. It’s not the same as serving dynamic content on a blog in which you simply say IF blogDate ⇒ NOW()
. With a static site you publish content by building it and pushing those static files to the server (GitHub Pages in my case) - and if you’ve published content that’s future dated, you need to find a way to trigger that publishing process.
I recently undertook a little project to create and publish twelve videos in the run-up to Christmas; three of them were to be published after I’d hung up my laptop for the year and would be comfortably ensconced on a sofa and up to my ears in Quality Street and mince pies. Whilst there were plenty of tools to publish tweets on a schedule, and YouTube can schedule the publication of a video, my blog (from which the videos were linked) looked like it was going to be a bit of a problem. A bash while
loop running on my laptop, or even a crontab, seemed a bit hacky and ultimately not reliable enough.
The answer turned out to be this thing called GitHub Actions which, like a lot of technology these days, I’d heard of and was vaguely aware of—but had no idea what it actually did.
GitHub Actions lets you take actions (duh!) based on the code in your repo on GitHub. Since my blog is just a repo, it can be set to trigger an action every time I push a new article to it (like this one), or indeed on a schedule also.
I found a few articles to use, primarily this nice one from Gunnar Morling.
Preparation 🔗
See my previous article for details of how I’ve installed and configured Hugo.
I’ve got two repositories:
-
https://github.com/rmoff/rmoff-blog/
- holds the source code for my blog -
https://github.com/rmoff/rmoff.github.io
- the static site served through GitHub Pages. I have a custom domain (rmoff.net
) set on this
Repository secret (source repository) 🔗
In the source repository, from which the GitHub Action will run, you need to create a Repository secret. This is the authorisation under which the Action will run.
I’ve seen tutorials do this with both SSH keypairs, and with GitHub Personal Access Tokens. Based on Gunnar’s article, I used the SSH keypair approach, generating a unique pair just for this purpose:
ssh-keygen -t rsa -b 4096 -C "$(git config user.email)" -f gh-pages -N ""
In the repository secret for your source repository (so https://github.com/rmoff/rmoff-blog/
in my case) put the private half of the key (if you use the above command it’ll be called gh-pages
). Give it a name and make a note of that name. I used ACTIONS_DEPLOY_KEY
.
You configure this secret against the source repository - I wasted time in my GitHub profile settings looking for an option that wasn’t there 🤦♂️ |
Deploy key (target repository) 🔗
The deploy key is configured against the repository to which your Action is going to push the static site content, which is https://github.com/rmoff/rmoff.github.io
in my case. If you are using SSH keys then it is the public part of your keypair that you generated, and using the above code will be called gh-pages.pub
.
You configure this deploy key against the target repository that holds your static site - I wasted time in my GitHub profile settings looking for an option that wasn’t there 🤦♂️ |
Configuring the Action 🔗
Once you’ve set up the auth per above, you need to configure the action itself. This is done through a YAML file that you put in the source repository from which you want it to run. Mine is based heavily on the one in Gunnar’s article, with a few tweaks.
name: Build & deploy live site
on:
push:
branches:
- main
# schedule:
# # Run on the hour
# - cron: '0 * * * *'
jobs:
build-deploy:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v1
- name: Install Ruby Dev
run: sudo apt-get install ruby-dev
- name: Install AsciiDoctor
run: sudo gem install asciidoctor
- name: Install Rouge
run: sudo gem install rouge -v 3.30.0
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: '0.105.0'
- name: Build
run: hugo
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
external_repository: rmoff/rmoff.github.io
publish_branch: master
# Without `keep_files` the `CNAME` file in the target repo will get removed
# and the custom domain configured for GitHub pages will get dropped every
# time the action runs…
keep_files: true
Points to note:
-
The action will trigger every ten minutes (in practice this seems to not be honoured and instead seems to run every 40 minutes or so instead, maybe because I’m on the GitHub free plan?):
schedule: # Run every ten minutes - cron: '*/10 * * * *'
as well as when a push is made to the master branch:
push: branches: - master
-
You can tie the Hugo version to a particular number; I’ve used the same one as I run locally
hugo-version: '0.75.1'
-
You need to make sure
deploy_key
matches whatever name you gave to the repository secret that you configured abovedeploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }}
-
Hugo deployments are usually set up with
/public
as a submodule from another repo (the hosted static site repo). I got tied in knots here with this, so ended up settingexternal_repository
and it works just fine./public
is the folder that the Hugo action pushes by defaultexternal_repository: rmoff/rmoff.github.io
-
This last setting is a crucial one. I use a custom domain on my GitHub Pages site, and when I was setting this up that custom domain kept getting dropped each time I ran the action…very confusing! Until I realised that the custom domain name is stored as a file
CNAME
in the repository, and the action was replacing the contents of the repository each time it ran! So, withoutkeep_files
theCNAME
file in the target repo will get removed and the custom domain configured for GitHub pages will get dropped every time the action runs…keep_files: true
The only issue I had was to do with my theme, and some dodgy include
directives that I’d set up. I ended up ditching the theme that I use as a submodule and just including it in my site code. Dirty, but it works, and the theme isn’t actively developed any more, so ¯_(ツ)_/¯
Summary 🔗
That’s pretty much it. The build chugs away every few minutes, meaning that posts that I’ve written that are future dated will hopefully publish just fine when their time comes around.
Gunnar noted that he’s evolved his build from what he originally published, as well as setting up preview builds for PRs which is a neat idea.
Also got my preview flow properly set up now. Proper base URL, and e.g. Disqus disabled on staging (via Hugo environments). Also considering to add noindex metadata, but probably not worth it, as those previews are rather short-lived. https://t.co/KdPxlrk1E4.
— Gunnar Morling 🌍 (@gunnarmorling) December 20, 2020