Rihards Steinbergs

GitHub Actions & rsync based deployments for Hugo

Let’s start with the full GitHub Actions workflow file and then work our way through it step by step. You can also view the file on GitHub as a gist. This will serve as documentation for myself as well (the next time I’ll need it).

 1name: Build & Deploy
 2on:
 3  push:
 4    branches: [ main ]
 5  workflow_dispatch:
 6
 7jobs:
 8  deploy:
 9    runs-on: ubuntu-latest
10    concurrency:
11      group: ${{ github.workflow }}-${{ github.ref }}
12      cancel-in-progress: true
13
14    steps:
15      - uses: actions/checkout@v7
16        with:
17          submodules: recursive
18          fetch-depth: 1
19          lfs: false
20
21      - name: Setup Hugo
22        uses: peaceiris/actions-hugo@v3
23        with:
24          hugo-version: '0.169.0'
25          
26      - name: Build
27        run: hugo --minify
28        
29      - name: Deploy
30        uses: burnett01/rsync-deployments@v9
31        with:
32          switches: -avzr --delete
33          path: public/
34          remote_path: ${{ secrets.REMOTE_PATH }}
35          remote_host: ${{ secrets.REMOTE_HOST }}
36          remote_user: ${{ secrets.REMOTE_USER }}
37          remote_key: ${{ secrets.REMOTE_PRIVATE_KEY }}

The Setup

Pretty basic setup at the top. We have on push to main branch which means that every time there’s a new commit added to the main branch this action is activated, and this includes merges to the main branch or direct pushes to it.

I personally like to add workflow_dispatch: to my workflows because it allows you to manually trigger the workflow in GitHub. I’ve only used it once so far for this project (and that to test if it works), but there are cases where it is quite handy to have it. Otherwise you’ll have to create empty (or test) commits just to trigger the workflow.

Runs On & Concurrency

As this is quite a small project that has very few moving parts and in general very well maintained and popular software I’m using ubuntu-latest. Which means that it uses whatever the latest version of Ubuntu (as deemed by GitHub) is. Over time the version used gets updated to, in fact, it has just recently changed to Ubuntu 26.

In bigger and more important projects you’d want to pin this to a specific version to make sure that it runs as expected. And then over time review changing the pinned version so that you aren’t running an end of life version that no longer receives updates.

The concurrency is something that is a bit much for this project, but it’s a good practice to have it, and a good thing to learn about.

With the above configuration it allows for in progress workflows in the same branch to be cancelled should a newer one be started. That is, no point in finishing an old run if there’s already a newer one starting, but do this only for the specific workflow and specific branch combination.

Step: Checkout

Submodules are needed in this case because I’m using a Hugo theme, and those are typically added as a git submodule to a project. Recursive in theory because the theme could be using submodules as well, so just making sure that we don’t break that.

The fetch-depth and lfs are actually both using the default values, so you don’t need to have them as such, but I like to be explicit with things like this. fetch-depth: 1 will only fetch the specific commit from the git repository, this is very helpful for performance as your repository grows in size. While lfs specifically says do not download git LFS (Large File Storage) files, which are files that are larger than 50 MiB and require special setup to be added to GitHub’s git repositories.

Steps: Setup Hugo & Build

These are pretty barebone steps, the only options I’ve added are pinning the Hugo version to the same version that I run locally currently, and during the build process to minify the output.

Step: Deploy

The step the requires most work (and most testing) is the step that takes the Hugo generated content and uploads it with rsync to my server. In order for this step to work you’ll need to (or at least want to) setup a new SSH Key to be used purely for these deployments so that you can revoke it in case of security needs.

There’s good documentation and troubleshooting guides available for this step in the git repository of the burnett01/rsync-deployments action.

You will also need to setup the needed secrets in your repository settings. You can learn more about them in the Using secrets in GitHub Actions article in GitHub Docs.

Important, and I can’t stress this enough: you will definitely want to run this workflow a few times first with -avzr --delete --dry-run switches (note the --dry-run part), just to make sure it doesn’t delete things it shouldn’t. Yes, it should have the delete flag because otherwise you’ll end up with content that shouldn’t be there anymore. So run it a few times, check the run summary and debug output, and once you are happy and brave enough, remove the --dry-run from the switches to actually deploy the site.

Future improvements and general thoughts

While I’m quite happy with how it works currently, there’s always room for improvement and changes. Some thoughts that you (or I) might want to change about this:

Help & Troubleshooting

Should you have any questions, or feedback, or be in need of help, feel free to contact me.

#Automation #Hugo #Webdev