Automatically deploying a Hugo static website to S3 via sourcehut

This is how I’m auto-deploying a static website generated with Hugo, served from Amazon S3, with a CloudFront frontend (for HTTPS, mostly). The code is stored in sourcehut, and automatically deployed using sourcehut’s builds.sr.ht service.

.build.yml

This is what I’m using now. It includes just the build-specific bits, then delegates to a deploy.sh script (described below):

image: alpine/edge
secrets:
  - <your ~/.aws/config secret UUID>
packages:
  - aws-cli
  - hugo
sources:
  - <https git URL of the repo you're deploying>
tasks:
  - deploy: |
      cd <name of repo>
      ./scripts/deploy.sh

NOTE: This will deploy from any branch that you push. If you want to only deploy when you push main, you can add a check-branch action that aborts early, as suggested here.

To authenticate to the AWS API, we use a build secret containing an entire ~/.aws.config file. This keeps the build file nice and simple.

You don’t actually need the sources section when you’re deploying via a .build.yml – the repo containing the .build.yml file is automatically added. But it’s very convenient for testing by submitting ad-hoc build files via sourcehut’s web interface.

scripts/deploy.sh

I use variations of this script for different repos, so it’s designed somewhat parametrized. To adapt to a new repo you only need to adjust the variables at the top.

Note that I use a non-default named profile with the --profile flag to aws. This is not necessary; it’s just my preference. You can set it to default.

#!/bin/bash
set -u
set -e
cfg=$HOME/.aws/config
profile=<your profile name>
bucket=s3://<your s3 bucket name>
dir=public
cf_id=<your cloudfront ID>
scripts=$(dirname $0)

which aws || (echo "'aws' command not found. Aborting."; exit 2)
[ -f $cfg ] || (echo "Config file $cfg does not exist. Aborting."; exit 2)

echo "Building..."
${scripts}/generate.sh

echo "Deploying..."
echo "Synchronizing directory $PWD/$dir"
aws --profile="$profile" \
  s3 sync "$dir" "$bucket" \
  --cache-control=max-age=3600
echo "Invalidating CloudFront..."
aws --profile=admin \
  cloudfront create-invalidation \
  --distribution-id "${cf_id}" \
  --paths "/*"

Note that you don’t need to generate the static site content beforehand – this script runs generate.sh automatically. This prevents you from accidentally deploying outdated generated content.

There’s a lot of room for improvement around CloudFront here – this script wastefully invalidates everything every time you deploy. It works for me because my sites are relatively small and infrequently updated.

scripts/generate.sh

This one is simple. It generates the contents of the website, and is just hugo plus whatever flags you decide to use:

#!/bin/bash
hugo --minify --cleanDestinationDir -d ./public || exit 1

By splitting everything apart like this, you can easily test and use each component individually: You can make changes to generate.sh (for instance, adding a minifying operation) and inspect the output in the public directory without deploying; you can make changes to deploy.sh and deploy manually, from your local machine, before trying the automated version in .build.yml.

Real examples

For blog.michaelkelly.org:

I hope this is useful to someone, if only to myself, in the future.


Next Post: LXC Containers on Debian, Part 1 (Setup)

Previous Post: Mirroring sourcehut repositories to GitHub