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.
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.
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
pushto themasterbranch. - 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.
- Run this workflow whenever there’s there’s a
1name: Build and deploy live site
2
3defaults:
4 run:
5 shell: bash
6
7# run this workflow on...
8on:
9 # ... all pushes to master
10 push:
11 branches:
12 - master
13 # ... this schedule: 1m past the hour @ 7a/9a/12a/5a, M-F (ET)
14 schedule:
15 - 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
I do have a bunch of environment variables set up, but here are the important ones for this blog post.
1env:
2 HUGO_VERSION: 0.71.1
3 SITE_BASEURL: https://www.andrewconnell.com
4 AZURE_STORAGE_ACCOUNT: andrewconnellcom
5 # 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:
1jobs:
2######################################################################
3# build & deploy the site
4######################################################################
5build-deploy:
6name: Build and deploy
7if: "!contains(github.event.head_commit.message,'[skip-ci]')"
8runs-on: ubuntu-latest
9steps:
10######################################################################
11# checkout full codebase
12######################################################################
13- name: Checkout repo codebase
14 uses: actions/checkout@v2
15 with:
16 fetch-depth: 1
17 clean: true
18 submodules: false
19######################################################################
20# download & install Hugo (line break added in URL for readability)
21######################################################################
22- name: Download Hugo v${{ env.HUGO_VERSION }} Linux x64
23 run: "wget https://github.com/gohugoio/hugo/releases/download/v${{ env.HUGO_VERSION }}
24 /hugo_${{ env.HUGO_VERSION }}_Linux-64bit.deb -O hugo_${{ env.HUGO_VERSION }}_Linux-64bit.deb"
25- name: Install Hugo
26 run: sudo dpkg -i hugo*.debYou’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:
1######################################################################
2# build site with Hugo
3######################################################################
4- name: Build site with Hugo
5 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:
1######################################################################
2# deploy site with Hugo deploy comment (only deploys diff)
3######################################################################
4- name: Deploy build to static site (Azure storage blob)
5 run: hugo deploy --maxDeletes -1
6 env:
7 AZURE_STORAGE_ACCOUNT: ${{ env.AZURE_STORAGE_ACCOUNT }}
8 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
I’m doing this with a custom action from Wictor Wilen in my workflow:
1######################################################################
2# add release annotation to Azure App Insights
3# > only annotate on push events & non [content] commits
4######################################################################
5- name: Annotate deployment to Azure Application Insights
6 uses: wictorwilen/application-insights-action@v1
7 if: "github.event_name=='push' && !contains(github.event.head_commit.message,'[content]')"
8 with:
9 applicationId: ${{ env.AZURE_APPLICATION_INSIGHTS_APPID }}
10 apiKey: ${{ secrets.AZURE_APPLICATION_INSIGHTS_APIKEY }}
11 releaseName: ${{ github.event_name }}
12 message: ${{ github.event.head_commit.message }} (${{ github.event.head_commit.id }})
13 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.
1name: Build and deploy live site
2
3defaults:
4 run:
5 shell: bash
6
7# run this workflow on...
8on:
9 # ... all pushes to master
10 push:
11 branches:
12 - master
13 # ... this schedule: 1m past the hour @ 7a/9a/12a/5a, M-F (ET)
14 schedule:
15 - cron: '1 12,14,17,22 * * 1,2,3,4,5'
16
17env:
18 HUGO_VERSION: 0.71.1
19 SITE_BASEURL: https://www.andrewconnell.com
20 AZURE_STORAGE_ACCOUNT: andrewconnellcom
21 # LIVE_AZURE_STORAGE_KEY: <secret>
22 AZURE_APPLICATION_INSIGHTS_APPID: 63YoMama-2e26-W3rs-8Pnk-0fArmyB00tsd
23 # AZURE_APPLICATION_INSIGHTS_APIKEY: <secret>
24
25jobs:
26 ######################################################################
27 # build & deploy the site
28 ######################################################################
29 build-deploy:
30 name: Build and deploy
31 if: "!contains(github.event.head_commit.message,'[skip-ci]')"
32 runs-on: ubuntu-latest
33 steps:
34 ######################################################################
35 # checkout full codebase
36 ######################################################################
37 - name: Checkout repo codebase
38 uses: actions/checkout@v2
39 with:
40 fetch-depth: 1
41 clean: true
42 submodules: false
43 ######################################################################
44 # download & install Hugo
45 ######################################################################
46 - name: Download Hugo v${{ env.HUGO_VERSION }} Linux x64
47 run: "wget https://github.com/gohugoio/hugo/releases/download/v${{ env.HUGO_VERSION }}
48 /hugo_${{ env.HUGO_VERSION }}_Linux-64bit.deb -O hugo_${{ env.HUGO_VERSION }}_Linux-64bit.deb"
49 - name: Install Hugo
50 run: sudo dpkg -i hugo*.deb
51 ######################################################################
52 # build site with Hugo
53 ######################################################################
54 - name: Build site with Hugo
55 run: hugo --baseUrl '${{ env.SITE_BASEURL }}'
56 ######################################################################
57 # deploy site with Hugo deploy comment (only deploys diff)
58 ######################################################################
59 - name: Deploy build to static site (Azure storage blob)
60 run: hugo deploy --maxDeletes -1
61 env:
62 AZURE_STORAGE_ACCOUNT: ${{ env.AZURE_STORAGE_ACCOUNT }}
63 AZURE_STORAGE_KEY: ${{ secrets.LIVE_AZURE_STORAGE_KEY }}
64 ######################################################################
65 # add release annotation to Azure App Insights
66 # > only annotate on push events & non [content] commits
67 ######################################################################
68 - name: Annotate deployment to Azure Application Insights
69 uses: wictorwilen/application-insights-action@v1
70 if: "github.event_name=='push' && !contains(github.event.head_commit.message,'[content]')"
71 with:
72 applicationId: ${{ env.AZURE_APPLICATION_INSIGHTS_APPID }}
73 apiKey: ${{ secrets.AZURE_APPLICATION_INSIGHTS_APIKEY }}
74 releaseName: ${{ github.event_name }}
75 message: ${{ github.event.head_commit.message }} (${{ github.event.head_commit.id }})
76 actor: ${{ github.actor }}