How to deploy automatically a Hugo website
| hugo git scriptI while ago I asked myself if there is a way to deploy automatically all the updates of my website, which is written with Hugo framework .
Before describing the automated procedure that I ended up using, I will describe my workflow in order to make more clear my decisions.
Overview «
The only thing we need to know, about how Hugo works, is that the static files that
will be used by Hugo to display the website, are located in the public directory.
To generate these files we execute the hugo
command, which will check for
changes in the articles or the HTML templates, and if there are it will
create/edit the files in the public directory.
This is my workflow with git: I do all my changes as many times as I wish
and commit as many times as I want. Best if each commit as a single scope.
When I feel that my work is ready to be released, I made sure that there are no
pending changes and execute the hugo
command. With the static files generated,
I commit them with a message that always begins with release:
, for example:
$ git commit -m "release: added 'How to deploy automatically a Hugo website'"
Manual procedure «
To actually release the changes I have to SSH into the website server, go to the
project directory and git pull
all the changes, and finally restart the docker
container: which will apply all the new changes.
Automatic procedure «
After some time that I have done this procedure, I started thinking if I can execute
all these steps automatically.
The first part was easy, create a cron job that will execute a bash script
every morning (which is shown at the end of this article).
The second part was to create the script that will check if it is time to do a new deployment and restart the container.
Obviously, the script has to pull the latest changes, so I have to add a flag
to specify the project directory.
But how to check if it is time to do a new deployment?
There were two solutions that came to my mind:
- check the commit messages, if there has been, through all the new commits, a message that begins with “release:"
- check if the public directory has changed since the last time.
The first solution didn’t convince me, even if I have this convention at the moment it could change at any time, and what if I do a typo while writing the commit message? It was not reliable.
Only one thing will remain the same over time until Hugo changes it: when I’m ready to do a release I will update the public directory. So it is clear that a good solution is to check if this directory has changed since the last time.
To check if the directory has changed I used the following git command
git diff --quiet HEAD <commit_to_compare> -- <dir_to_check>
<commit_to_compare> is the hash of the HEAD before doing git pull
and <dir_to_check>
is the “public” directory. This command returns 0 if there are no changes, and 1 otherwise.
This is the bash script:
#!/bin/bash
function exit_w_error {
echo "$1"
exit 1
} >&2 # redirect to STDERR
function is_installed {
[ -z "$1" ] && exit_w_error "Command name missing"
COMMAND=$1
FOUND=false
PATH_SPACED=${PATH//:/ } # separate each path by space instead of :
for individual_path in ${PATH_SPACED}; do
# true if file exist and is executable
[ -x "$individual_path/$COMMAND" ] && FOUND=true && break
done
}
SCRIPT_NAME=${0##*/} # performed a string manipulation operation to get the script name
DIR_TO_MONITOR='website/public'
function help {
# here-document used instead of echoing all lines
cat <<-HMESSAGE
Perform a git pull and check if the '$DIR_TO_MONITOR' directory has changed.
If this directory has changed, restart the production container to
release the new updates.
This is possible because the Hugo framework uses the 'public' directory to
store the files that have to be published, and we update it only when
we are ready to publish.
So, if the directory is updated this means that we can publish the changes.
Syntax: $SCRIPT_NAME [-hvd]
Options:
-h Print this help and exit.
-v Make messages more verbose
-d Directory of the git project to check
HMESSAGE
}
DIR=''
DEBUG=':' # no-op, do nothing command
# Get the options
while getopts "hvd:" option; do
case $option in
h )
help
exit 0;;
v )
DEBUG='echo';;
d )
DIR=$OPTARG;;
\?)
echo "Error: Invalid option"
exit 1;;
esac
done
shift $((OPTIND -1)) # remove all options from $#
echo "$(date) - executing '$SCRIPT_NAME' script"
is_installed git
is_installed docker
[ -d "$DIR" ] && cd "$DIR" && $DEBUG "Moved to $DIR"
[ -d "$DIR_TO_MONITOR" ] || exit_w_error "'$DIR_TO_MONITOR' does not exist in the current directory"
PRE_PULL_HASH=$(git rev-parse HEAD)
$DEBUG 'Pulling from remote'
if [ $DEBUG = ':' ];then
git pull --quiet
else
git pull
fi
# --quiet makes the command return 0 if there are no changes, 1 otherwise
$(git diff --quiet HEAD $PRE_PULL_HASH -- "$DIR_TO_MONITOR") && { $DEBUG "The '$DIR_TO_MONITOR' directory was not updated"; echo "NOT UPDATING the website"; exit 0; }
$DEBUG "The '$DIR_TO_MONITOR' directory was updated, restarting the production container"
echo "UPDATING the website"
docker compose restart
exit 0
This is how I set the cron job
# every day at 6:00 AM, in the Rome timezone
0 3 * * * <path to script> -d <path to website> >> <path to log file>