When deciding to resurrect this blog, I first had to do some research to choose the right software to do the job. The original blog years ago was deployed using WordPress and while I’m still a fan of WordPress I wanted to keep things super simple this time around.

At work last year I redid some internal documentation for a large Ansible automation project and had a few things in mind:

  1. The documentation for the code should live with that same code to make it easy to keep up to date as the code changes.

  2. The documentation should be as lightweight as possible, avoiding the need to maintain any hosting software or databases.

  3. The documentation should be easy to write so it doesn’t feel like a chore.

These requirements made me immediately think of Markdown. We could commit Markdown alongside the code itself, write it easily, use any editor, read it locally and so on.

Paired with a static site generator, you can actually get some really nice looking documentation with a minimum of effort. The generator takes the Markdown files, and creates static HTML pages using a template.

For the work project we ended up using software called MkDocs which is very good, but focussed around project documentation; for this site I needed to look at something more focussed on blogging. I had already decided I wanted to try GitHub Pages so that publishing a new post would be as easy as committing to a repository, and the new post would be generated and made available almost right away.

Jekyll is a static site generator that is supported by GitHub Pages out of the box and is written in Ruby, which I have experience with, so it felt like a good place to start.

I followed the quick-start instructions on the Jekyll website and was up and running pretty quickly, but I’m a tinkerer, I like to dig deeper into things and see how they work, so I found myself cloning the default theme to see exactly how it built a page and how I could customise it.

Most of the customisations I made were small, things like adding the estimated reading time to each post, displaying the categories the post belongs to, adding ARIA tags for accessibility and so on.

Committing all of this to a GitHub repository and turning GitHub Pages on in the repo settings was painless and everything worked as expected, hurray!

My next issue was that I wanted to add archive pages to list posts by category or date, so that I wouldn’t need to add this later once I actually had some content. Going back to the earlier goal of simplicity I wanted the tool to handle generating archive pages for me, but out of the box Jekyll can’t do this. It is however extensible through plugins, and I was able to find and install a plugin called jekyll-archives, but here I hit a snag; this plugin isn’t supported by the version of Jekyll that is used by GitHub Pages.

What to do? Well luckily for me, GitHub have recently launched their own CI/CD automation workflow called GitHub Actions, allowing you to perform certain actions when code is committed to a repository, so we have an option to run Jekyll directly instead of relying on the one integrated with GitHub Pages. This allows us to do anything you could do normally with Jekyll, including use custom plugins.

Building a workflow

GitHub Pages can be configured to skip running Jekyll itself and instead just take an existing set of static HTML files from a branch named gh-pages and use those, so I needed to setup a workflow for GitHub Actions that would do the following:

  • Checkout the latest revision of the repository

  • Set up a Ruby environment and install Jekyll

  • Run the jekyll build command

  • Push the result of the build command to the gh-pages branch

To create a GitHub Action you define the workflow in YAML format and commit that file into a .github/workflows/ folder in your repository, after which you’ll see it listed under the Actions tab of your repository. So let’s translate each step above into an action in our workflow, using the actions available.

  1. We want to start by naming the workfow and configuring it to run any time commits are pushed to the master branch, we also configure the workflow to be run inside an Ubuntu linux environment which is setup fresh each time the workflow runs. We define the steps key which we will fill in as we go.

     ---
     name: "Build Jekyll and push to 'gh-pages' branch"
     on:
       push:
         branches:
           - master
    
     jobs:
       build:
         runs-on: ubuntu-latest
         steps:
    
  2. The first item to add to steps is an action to checkout the latest revision of the master branch, which is done like so:

           - name: Checkout
             uses: actions/checkout@v2
    
  3. Next we want to setup a Ruby environment and there is an action that can handle that for us:

           - name: Set up Ruby
             uses: actions/setup-ruby@v1
             with:
               ruby-version: 2.6
    
  4. Now we should have a Ruby environment and our Jekyll project checked out ready to go, we need to install Jekyll and it’s dependencies from the Gemfile in our project. Here we don’t need any particular action, we just want to run a command:

           - name: Bundle install
             run: |
               bundle install
    
  5. Next we run Jekyll’s build command against our project and tell it to place the resulting static HTML files in a folder named target.

           - name: Bundle install
             run: |
               bundle exec jekyll build -d target
    

Now we should have the static content ready to commit to the gh-pages branch, but I wasn’t sure how to proceed, I came across a blog post by Benjamin Lannon which shows that you can run git commands normally, however I wanted to commit to a different branch (gh-pages in this case) and only commit the result of the jekyll build command, to avoid accidentally including non-content, such as the Gemfile. After a bit of Googling I found this very helpful Gist which showed a very tidy way of avoiding this issue entirely.

Using the subtree split feature of Git we can split the repository apart by taking only the commits that affected our target folder created in the previous step. Since this will be a single commit containing the entire generated site, we then force push that to the desired branch for GitHub Pages to pick up and deploy for us.

Let’s configure a user for the commits to be made as:

      - name: Setup Git config
        run: |
          git config user.name "GitHub Actions Bot"
          git config user.email "<>"

Next we create the commit using git subtree split:

      - name: Commit
          run: |
            git add target
            git commit -m "$(git log -1 --pretty=%B)"
            git subtree split --prefix target -b gh-pages
            git push -f origin gh-pages:gh-pages

That’s it! Now we should be able to use the full functionality of Jekyll and any plugins we like, but we still get the simplicity of pushing a single commit to our repository, in order to publish new content. There are a couple of enhancements we can make to speed up the performane of this workflow though. Rather than setting up the Ruby environment from scratch every time, we want to cache the gems we’re using so that they can be installed much quicker next time. There’s a Cache action we can use to do this for a variety of languages including Ruby.

First we need to include the Cache action in our workflow, place this new task above the Set up Ruby task:

      - uses: actions/cache@v2
        with:
          path: vendor/bundle
          key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
          restore-keys: |
            ${{ runner.os }}-gems-

This will instruct GitHub Actions to cache the data in vendor/bundle using a specific key, in this case the key includes the operating system of the system running our workflow, and the hashFiles function is used to generate a hash for a given file, in this case our Gemfile.lock, so that we only rebuild our cache if the Gemfile (and therefore the hash) changes.

Next we need to configure Bundler to actually use this cache when installing gems. Modify the Bundle install task to look like this:

      - name: Bundle install
        run: |
          bundle config path vendor/bundle
          bundle install

That’s it! We should now have a self-contained automatic workflow for publishing new content to the blog by simply commiting a new Markdown document.