Deploying Hugo site to GitHub Pages with Azure Pipelines

Summary

The intent is to deploy a Hugo site hosted on GitHub to GitHub Pages. We will be using Azure Pipelines as the CI tool to build the site using Docker and then push the compiled site back to GitHub for deployment.

Prerequisites

Process

Setup Repositories

The first thing we want to do is to create two GitHub repos. One will contain the source code for the website and the other will contain the compiled and minified code to drive the GitHub Pages site.

For the source code I went with a repo name of rob-mccloud since the site will be available at https://rob-mc.cloud. The name of this repo is entirely up to you. Check in your site code except any files in /resources/_gen or /public. Be sure not to add /public to the .gitignore we will need this folder later.

The GitHub Pages repo on the other hand has a strict requirement in naming. As stated in the doco, the repo name must be in the form <username>.github.io The casing does not matter. My repo is called robfaie.github.io despite my username being RobFaie. I chose to make this repo private, but it doesn’t matter much.

Adding the GitHub Pages repo as a submodule

When Hugo builds the result is placed in the public folder. We will be adding our GitHub Pages repo as a submodule in that folder so that we can simply build and then push the submodule change to deploy. To do this we use the following command from the top of your source code repo.

git submodule add -b master ../<username>.github.io.git public

Note that we are using a relative path for the repo. This is important so that Azure Pipelines uses the same creds for both repos. Make sure to check-in the submodule update.

Building Locally

Let’s build the site locally to check things out and make sure we haven’t made any mistakes up to this point. Up until now you have likely been calling hugo server -D to serve your site locally. The -D bit will build the draft pages which you don’t build for the production deployment, so if your pages are still in draft, publish them by setting draft: false in the metadata. Now run the following command:

hugo --gc --minify

This will build the site, minify the results, and run some garbage collection. Check the output in the terminal to check that you have built the number of pages you were expecting and have a flick through the public folder to make things look right.

Next let’s do it again but using docker which we will be using in the Azure Pipeline. I will be using the jguyomard/hugo-builder image from dockerhub. It’s a simple image that uses alpine linux as a base which is nice and lightweight. Delete the public folder and run the following:

docker run --rm -v $PWD:/src jguyomard/hugo-builder hugo --gc --minify

Deploy locally

Now that we have our site built we can do a test deployment to iron out any issues with the submodule. Navigate into the public folder and give it a push:

git add .
git commit -m "Local Deployment Test"
git push origin master

After a minute your site should be live at <username>.github.io If you have set a custom domain set in your hugo config, your site will likely have broken css and assets. We’ll fix that up when we set up the custom domain.

Creating an Azure DevOps Organization

We’re going to head over to https://aex.dev.azure.com/signup/ to setup an Azure DevOps Services Organization. We can click “Start free with GitHub” to create one with our existing GitHub account. Once the org is setup, create a project for the website using the “Create Project” in the upper right hand corner.

Create Azure Pipeline

From our ADOS project, select ‘Pipelines’ => ‘Builds’ from the left side bar and then ‘New build pipeline’ from the ‘New’ drop down menu. We’re going to be going with the ‘github (YAML)’.

Select your source code repo from the list. During the OAuth propts that follow select both the source code repo and the GitHub Pages repo when giving access to repos.

On the ‘Configure’ step, choose ‘Starter pipeline’. This will create an azure-pipelines.yml file and check it into your repo for you. Alternatively you can create the file yourself and at this step choose ‘Existing Azure Pipelines YAML file’.

The Pipeline Yaml file

The first thing we want in the pipeline configuration file is the trigger. The whole point of going through this effort is so that we don’t need to anything other than push code to GitHub for the live website to get updated.

trigger:
- master

We’re going to be using Ubuntu 16.04 for the build agent since it comes with Docker and there we’re not doing anything more than a docker command and a few git commands.

pool:
  vmImage: 'ubuntu-16.04'

The first step we’re going to configure the checkout step. This step checks out the source code from git and will run regardless of whether it is specified in the pipeline configuration file, but by putting it in here we can change the behaviour slightly.

In this case we want to checkout submodules so we get our Hugo theme. Doing a clean checkout takes a little longer but ensures that we don’t have any assets siting around from last build. Lastly we request that the credentials are persisted. This is important for when we want to push to our submodule repo.

steps:
- checkout: self
  clean: true
  submodules: recursive
  persistCredentials: true

Now comes the meat of the pipeline. Firstly we want to grab the credentials that Azure Pipelines has left for us and store it in a variable for later. Inside the public folder we checkout master so that we aren’t on a detached head and have the latest commit for clean diffs. I have then chosen to delete all contents so that any removed posts or tags are cleaned up fully.

Next we issue the same Docker command as earlier simply replacing the $PWD in the volume argument with $(System.DefaultWorkingDirectory) which points to the root of our source code repo.

All that is left is to commit the changes and push them. We need to provide a user.name and user.email for git to use in the commit. I’ve chosen to use the built in pipeline variables $(Build.RequestedFor) and $(Build.RequestedForEmail). In order for Azure Pipelines to be able to do the push it needs to be given the credentials we stored earlier to authenticate the push.

- script: |
    AUTH=$(git config http.$(Build.Repository.Uri).extraheader)
    cd public
    git checkout master
    rm -rf *
    docker run --rm -v $(System.DefaultWorkingDirectory):/src jguyomard/hugo-builder hugo --gc --minify
    git add .
    git reset -- CNAME
    git reset -- README.md
    git -c "user.name=$(Build.RequestedFor)" -c "user.email=$(Build.RequestedForEmail)" commit -m "CICD: $(Build.BuildNumber)"
    git -c http.extraheader="$AUTH" push

Putting all of that together we get the following full pipelines configuration file:

trigger:
- master

pool:
  vmImage: 'ubuntu-16.04'

steps:
- checkout: self
  clean: true
  submodules: recursive
  persistCredentials: true

- script: |
    AUTH=$(git config http.$(Build.Repository.Uri).extraheader)
    cd public
    git checkout master
    rm -rf *
    docker run --rm -v $(System.DefaultWorkingDirectory):/src jguyomard/hugo-builder hugo --gc --minify
    git add .
    git reset -- CNAME
    git reset -- README.md
    git -c "user.name=$(Build.RequestedFor)" -c "user.email=$(Build.RequestedForEmail)" commit -m "CICD: $(Build.BuildNumber)"
    git -c http.extraheader="$AUTH" push

One of the nice things about setting up the Azure Pipeline this way is that this file is 100% agnostic of the actual repos and doesn’t require any Personal Access Tokens or SSH keys floating around. That makes it dead simple to reuse this on another Hugo site deployed to a GitHub Pages Project Page in the future.

GitHub Pages Custom Domain.

Setting up the custom domain for Github pages is quite simple. The doco can be found at https://help.github.com/en/articles/using-a-custom-domain-with-github-pages

Setup a CNAME DNS record in your DNS provider pointing directly at <username>.github.io. If you are planning on using an apex domain, you may need to use an ALIAS, ANAME, or A record since apex CNAME records are not part of the DNS spec. An apex domain is one without a subdomain, rob-mc.cloud vs www.rob-mc.cloud. My personal provider CloudFlare does CNAME flattening, so I was able to add the CNAME record and CloudFlare took care of converting it to actual to spec records.

Once the DNS is setup and has had time to propagate (up to 48 hours) the custom domain can be added in GitHub. From the settings page for your GitHub Pages repo, scroll nearly to the end to just above the Danger Zone. Here you can enter your custom domain and hit save. GitHub will redirect <username>.github.io to your custom domain and request a Let’s Encrypt SSL certificate for you. The certificate should only take an hour to be issued.


comments powered by Disqus