Automated Hugo Releases (CI/CD) with Github Actions

In this article, I'll share how I automate my scheduled Hugo deployments using a CI/CD process and GitHub Actions.

When I originally set up this site, I used Azure Multistage Pipelines to build & deploy the site automatically. I recently switched the CI/CD process to replace Azure Pipelines with GitHub Actions and in this post, I want to show you how I did it.

Info: Interested in deploying Hugo with Azure Pipelines?
If you want to read more about how to publish your Hugo site to Azure static sites with Azure Pipelines, you can my original post here: Automated Hugo Releases With Azure Pipelines.

Before I get into this, let me jump in front of a few questions you may have:

  • Why aren’t you use Azure Static Web Apps? First and foremost, Azure Static Web Apps weren’t around when I launched this site. While a good option, at the time of writing this post they have some limitations. I personally prefer my set up as it gives me much more control.
  • Why did I switch to GitHub Actions? GitHub is more natural to me and I like it’s simplicity over Azure DevOps. Azure DevOps is a great platform, but it’s overly complicated for most of my work.

The original post on Azure Pipelines already covered CI/CD & my requirements. Please refer to that post if you want to see how I roll things out:

GitHub Actions for Building & Deploying Hugo Sites

GitHub Actions are just like Azure Pipelines in concept. You create a workflow which is in a .github/workflows/*.yml file in your repo. This file contains one or more jobs & each job contains one or more steps.

You start by defining a schedule when you want the workflow to run & other global settings.

There are a few things to note here:

  • defaults: This is where you can specify defaults for your entire workflow. Here, I say I want to use a bash shell whenever I run commands in the console.
  • on: This section is the trigger definition. Here I’m saying two things:
    • Run this workflow whenever there’s there’s a push to the master branch.
    • Run this workflow on a specific schedule using the cron syntax. I use Hugo’s feature where you can specify content should not be published before a specific time. With these constant builds, I get the scheduled publishing capability of a CMS without the cost of a dynamic site.
name: Build and deploy live site

defaults:
  run:
    shell: bash

# run this workflow on...
on:
  # ... all pushes to master
  push:
    branches:
      - master
  # ... this schedule: 1m past the hour @ 7a/9a/12a/5a, M-F (ET)
  schedule:
    - cron: '1 12,14,17,22 * * 1,2,3,4,5'

Unlike Azure Pipelines where you have to specify the variables you want to use in your pipeline by specifying the variable group you created, your workflow has access to any of the Settings > Secrets defined in your GitHub repo. I’ve set a few of these on my repo:

AndrewConnell.com repro Secrets

AndrewConnell.com repro Secrets

I do have a bunch of environment variables set up, but here are the important ones for this blog post.

env:
  HUGO_VERSION: 0.71.1
  SITE_BASEURL: https://www.andrewconnell.com
  AZURE_STORAGE_ACCOUNT: andrewconnellcom
  # LIVE_AZURE_STORAGE_KEY: <secret>

Building Hugo Sites with GitHub Actions

Unlike Azure Pipelines, I have my entire build & deploy process in a single job. It’s simpler that way with how Hugo is set up with the deploy command.

Step 1: Download & Install Hugo

So, I start by cloning the repo & then downloading and installing the Hugo executable:

jobs:
######################################################################
# build & deploy the site
######################################################################
build-deploy:
name: Build and deploy
if: "!contains(github.event.head_commit.message,'[skip-ci]')"
runs-on: ubuntu-latest
steps:
######################################################################
# checkout full codebase
######################################################################
- name: Checkout repo codebase
  uses: actions/checkout@v2
  with:
    fetch-depth: 1
    clean: true
    submodules: false
######################################################################
# download & install Hugo (line break added in URL for readability)
######################################################################
- name: Download Hugo v${{ env.HUGO_VERSION }} Linux x64
  run: "wget https://github.com/gohugoio/hugo/releases/download/v${{ env.HUGO_VERSION }}
  /hugo_${{ env.HUGO_VERSION }}_Linux-64bit.deb -O hugo_${{ env.HUGO_VERSION }}_Linux-64bit.deb"
- name: Install Hugo
  run: sudo dpkg -i hugo*.deb

You’ll notice this job build-deploy contains an if condition. At the present time, GitHub Actions doesn’t support skipping a workflow run when the text [skip-ci] is present in the commit message. So, I added a condition to check and run this job only if that text isn’t found in the commit message.

Step 2: Build the site Using the Hugo Executable

With Hugo installed, build the site:

######################################################################
# build site with Hugo
######################################################################
- name: Build site with Hugo
  run: hugo --baseUrl '${{ env.SITE_BASEURL }}'

Notice I’m using an environment variable passed into the job to control an argument on the Hugo executable.

Deploy Hugo Sites with GitHub Actions

With the site built, I’m ready to publish! Use the Hugo deploy CLI command to deploy the site. This will determine what files need to be uploaded or deleted from the Azure Storage Blob:

######################################################################
# deploy site with Hugo deploy comment (only deploys diff)
######################################################################
- name: Deploy build to static site (Azure storage blob)
  run: hugo deploy --maxDeletes -1
  env:
    AZURE_STORAGE_ACCOUNT: ${{ env.AZURE_STORAGE_ACCOUNT }}
    AZURE_STORAGE_KEY: ${{ secrets.LIVE_AZURE_STORAGE_KEY }}

BONUS: Annotate the release in Azure Application Insights

Azure Application Insights supports adding an annotation to your deployment. These will show up in your your AppInsights reports as little callouts in the graphs:

Azure Application Insights Annotations

Azure Application Insights Annotations

I’m doing this with a custom action from Wictor Wilen in my workflow:

######################################################################
# add release annotation to Azure App Insights
# > only annotate on push events & non [content] commits
######################################################################
- name: Annotate deployment to Azure Application Insights
  uses: wictorwilen/application-insights-action@v1
  if: "github.event_name=='push' && !contains(github.event.head_commit.message,'[content]')"
  with:
    applicationId: ${{ env.AZURE_APPLICATION_INSIGHTS_APPID }}
    apiKey: ${{ secrets.AZURE_APPLICATION_INSIGHTS_APIKEY }}
    releaseName: ${{ github.event_name }}
    message: ${{ github.event.head_commit.message }} (${{ github.event.head_commit.id }})
    actor: ${{ github.actor }}

In mine, notice the if condition is making sure to only include annotations when the workflow trigger is a push (so I don’t add annotations for every scheduled build) and when I don’t have the string [content] in the commit message.

This ensures I only add annotations when I’m making a non-content change to the site which is what I want.

I’ve posted the full workflow at the end of this post

Wrapping it up

That’s it! I’ve written about a few other ways I was using Azure Pipelines with my Hugo site.

In the future days, I’ll show you how I moved those things over to GitHub Actions as well. This includes, for example, the post Automatically Reindex Hugo Sites with Azure Pipelines.

name: Build and deploy live site

defaults:
  run:
    shell: bash

# run this workflow on...
on:
  # ... all pushes to master
  push:
    branches:
      - master
  # ... this schedule: 1m past the hour @ 7a/9a/12a/5a, M-F (ET)
  schedule:
    - cron: '1 12,14,17,22 * * 1,2,3,4,5'

env:
  HUGO_VERSION: 0.71.1
  SITE_BASEURL: https://www.andrewconnell.com
  AZURE_STORAGE_ACCOUNT: andrewconnellcom
  # LIVE_AZURE_STORAGE_KEY: <secret>
  AZURE_APPLICATION_INSIGHTS_APPID: 63YoMama-2e26-W3rs-8Pnk-0fArmyB00tsd
  # AZURE_APPLICATION_INSIGHTS_APIKEY: <secret>

jobs:
  ######################################################################
  # build & deploy the site
  ######################################################################
  build-deploy:
    name: Build and deploy
    if: "!contains(github.event.head_commit.message,'[skip-ci]')"
    runs-on: ubuntu-latest
    steps:
      ######################################################################
      # checkout full codebase
      ######################################################################
      - name: Checkout repo codebase
        uses: actions/checkout@v2
        with:
          fetch-depth: 1
          clean: true
          submodules: false
      ######################################################################
      # download & install Hugo
      ######################################################################
      - name: Download Hugo v${{ env.HUGO_VERSION }} Linux x64
        run: "wget https://github.com/gohugoio/hugo/releases/download/v${{ env.HUGO_VERSION }}
        /hugo_${{ env.HUGO_VERSION }}_Linux-64bit.deb -O hugo_${{ env.HUGO_VERSION }}_Linux-64bit.deb"
      - name: Install Hugo
        run: sudo dpkg -i hugo*.deb
      ######################################################################
      # build site with Hugo
      ######################################################################
      - name: Build site with Hugo
        run: hugo --baseUrl '${{ env.SITE_BASEURL }}'
      ######################################################################
      # deploy site with Hugo deploy comment (only deploys diff)
      ######################################################################
      - name: Deploy build to static site (Azure storage blob)
        run: hugo deploy --maxDeletes -1
        env:
          AZURE_STORAGE_ACCOUNT: ${{ env.AZURE_STORAGE_ACCOUNT }}
          AZURE_STORAGE_KEY: ${{ secrets.LIVE_AZURE_STORAGE_KEY }}
      ######################################################################
      # add release annotation to Azure App Insights
      # > only annotate on push events & non [content] commits
      ######################################################################
      - name: Annotate deployment to Azure Application Insights
        uses: wictorwilen/application-insights-action@v1
        if: "github.event_name=='push' && !contains(github.event.head_commit.message,'[content]')"
        with:
          applicationId: ${{ env.AZURE_APPLICATION_INSIGHTS_APPID }}
          apiKey: ${{ secrets.AZURE_APPLICATION_INSIGHTS_APIKEY }}
          releaseName: ${{ github.event_name }}
          message: ${{ github.event.head_commit.message }} (${{ github.event.head_commit.id }})
          actor: ${{ github.actor }}
Andrew Connell
Developer & Chief Course Artisan, Voitanos LLC. | Microsoft MVP
Written by Andrew Connell

Andrew Connell is a full stack developer who focuses on Microsoft Azure & Microsoft 365. He’s a 20+ year recipient of Microsoft’s MVP award and has helped thousands of developers through the various courses he’s authored & taught. Andrew’s mission is to help web developers become experts in the Microsoft 365 ecosystem, so they can become irreplaceable in their organization.

Share & Comment