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



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
 3name: Hugo Build & Deploy - Private to Public
 5# Controls when the action will run. Triggers the workflow on push or pull request
 6# events but only for the master branch
 8  push:
 9    branches: [ master ]
11# A workflow run is made up of one or more jobs that can run sequentially or in parallel
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
18    # Steps represent a sequence of tasks that will be executed as part of the job
19    steps:
21    # Check Out Repository:  Based on
22    - name: Checkout 🛎️
23      uses: actions/[email protected] 
24      with:
25        persist-credentials: false
27    # Setup Hugo in the Ubuntu Runner
28    - name: Hugo setup
29      uses: peaceiris/[email protected]
30      with:
31        # The Hugo version to download (if necessary) and use. Example: 0.58.2
32        hugo-version: 'latest'
33        # Download (if necessary) and use Hugo extended version. Example: true
34        extended: false
36    # Runs Hugo to build the Static Site
37    - name: Run Hugo
38      run: |
39                hugo --minify --verbose
41# Deploy the Static Site to Public Repo (GitHub Pages)
42    - name: Deploy 🚀
43      uses: JamesIves/[email protected]/v3
44      with:
45        ACCESS_TOKEN:  ${{ secrets.ACCESS_TOKEN }}
46        REPOSITORY_NAME: rterakedis/
47        BRANCH: master # The branch the action should deploy to.
48        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.

Another lesson learned is that GitHub Actions are VERY particular about spacing and layout in the yaml file. I went through a few bombed deployments until I figured out that one of the lines in my yaml file was not spaced correctly.


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).