Using GitHub Actions to Publish Hugo Site From Private to Public Repo

Overview

Background

I restarted my blogging journey earlier this year when I started looking into Jekyll Hugo to generate a static website. I had past experience with Blogger and Wordpress, but frankly had periodic problems with both platforms that ended up being a time suck. As it has been, Hugo has been a simplistic publishing method and GitHub a reliable (and FREE) hosting provider. Yet, my desire to keep my drafts private (.e.g the use of 2 separate repositories) has created a small overhead in that I have to build and manually commit the website changes to the public repository to make them live.

Github Actions

GitHub Actions is a well-documented feature... that takes a little trial-and-error for us amateur devs (e.g. scripting sysadmins). When I first started learning, I really couldn't make heads-or-tails of it. But really, all you need to do is think of it as an automation workflow, triggered by GitHub events (check-in, etc). This automation engine spins up a temporary virtual machine, runs whatever code you need it to do, and then destroys the VM.

What's the Gotcha?

I found a bunch of articles where folks were using GitHub actions to deploy their Hugo sites based on repo check-ins. Cool, right? Push new code, and a few minutes later you have a new site live on the Internet. Except... these folks are using a single public repository for their GitHub pages and basically bouncing code between branches or folders in a single branch. I couldn't find anyone doing the same thing as me -- publishing the Hugo "Source" to a private repository, and then publishing just the static site to the Public GitHub Pages repository. I set out to figure this out and save myself some time.

Publishing Hugo Site from a Private Repo to Public GitHub Pages

I managed to cobble together a workflow using pieces from a couple different actions in the GitHub Actions marketplace. A few things to keep in mind:

  1. The "name: Checkout" step in the job pulls down the repository to /home/runner/work/<Repo Name>/<Repo Name> on the Ubuntu Runner.
  2. The "name: Hugo Setup" step downloads and installs the actual hugo binary. You can see I used the "latest" version and skipped the extended version.
  3. When the name: Run Hugo" step happens, this is where Hugo is actually reading the content of the files downloaded from my private GitHub repo, and building the static site in the subdirectory specified in the config.toml file (in my case, ./docs).
  4. Once the site finished building, Github uses a Personal Access Token to connect to the Public repository (e.g. NOT the repository running the workflow) and commit all the new/changed files.

The beauty of this whole setup is that GitHub provides a Free/Personal account up to 2000 minutes of GitHub Actions time per month. For a personal website, this should be more than enough. Also, if you find yourself bumping up against this limit, you can easily reduce your runs by creating a "working" branch to save your work-in-progress. Then you can publish more content at a single time by merging those changes into master and kicking off a new build. Things to think about.

Show me the Code!

Here you go!

 1# This is a basic workflow to help you get started with Actions
 2
 3name: Hugo Build & Deploy - Private to Public
 4
 5# Controls when the action will run. Triggers the workflow on push or pull request
 6# events but only for the master branch
 7on:
 8  push:
 9    branches: [ master ]
10
11# A workflow run is made up of one or more jobs that can run sequentially or in parallel
12jobs:
13  # This workflow contains a single job called "build"
14  build:
15    # The type of runner that the job will run on
16    runs-on: ubuntu-latest
17
18    # Steps represent a sequence of tasks that will be executed as part of the job
19    steps:
20    
21    # Check Out Repository:  Based on https://github.com/marketplace/actions/deploy-to-github-pages
22    - name: Checkout 🛎️
23      uses: actions/checkout@v4
24      with:
25        persist-credentials: false
26        submodules: true
27
28    # Setup Hugo in the Ubuntu Runner
29    - name: Hugo setup
30      uses: peaceiris/[email protected]
31      with:
32        # The Hugo version to download (if necessary) and use. Example: 0.58.2
33        hugo-version: 'latest'
34        # Download (if necessary) and use Hugo extended version. Example: true
35        extended: true
36
37    # Runs Hugo to build the Static Site
38    - name: Run Hugo
39      run: |
40        hugo --minify --verbose        
41
42# Deploy the Static Site to Public Repo (GitHub Pages)
43    - name: Deploy 🚀
44      uses: JamesIves/github-pages-deploy-action@v4
45      with:
46        ssh-key:  ${{ secrets.DEPLOY_KEY }}
47        repository-name: rterakedis/rterakedis.github.io
48        branch: master # The branch the action should deploy to.
49        folder: docs # The folder the action should deploy.

What about secrets.access_token?

So you noticed that did you? Yes, this is basically a way to allow the runner/action to authenticate to a different repository. If you read the documentation, it turns out the GitHub action executes in the permission/scope of the resposity to which it s permitted to do so. In my specific use-case, the runner needs to commit and push files to an entirely separate GitHub repository. By setting up the Personal Access Token (PAT) in my Account's Developer Settings, I'm able to add that PAT in the repository's "secrets" stash so that the action can authenticate to the Public repository.

Lessons Learned

In my site, I wanted to make sure that any files I changed were sure to get published. As such, I cleared out the /docs folder in my private repository so that each time hugo runs on the runner that folder contains all new files. This prevents a situation whereby hugo doesn't replace files that have already been generated. It also means I don't have to do a heavy-handed wipe on the folder downloaded to the runner before running the hugo build. All-in-all, this just seems cleaner.

I also modified the .gitignore file at the root of my project. I did this so that I could continually test locally (using hugo server --buildDrafts) without having to worry about git trying to track the static site files used by the hugo server. Here's what my .gitignore file contains:

1.DS_Store
2public
3docs

Another lesson learned is that YAML is VERY particular about indentation and spacing. As this was my first experience with YAML, I went through a few bombed GitHub Actions deployments until I figured out that one of the lines in my yaml file was not spaced correctly.

Thoughts?

This is my first foray into Github Actions, and it works pretty reliably. I've also found that using the GitHub Actions I'll now be able to blog on my iPad using Working Copy and iA Writer (in addition to Visual Studios Code on my Mac).