I found a ton of posts that describe how to use Hugo and GitLab CI to create and host a static site on GitLab Pages.

However, I had a different requirement: I want to use a local GitLab CE instance to store my content, use GitLab CI (locally) to build the static output and then push that output to GitHub to publish on GitHub Pages.

High-level workflow:

  • spin up a Docker container
  • check out the latest GitHub Pages content commit from GitHub
  • run Hugo on the latest source content commit from GitLab CE
  • if the content has been updated, push the changes to GitHub

The Docker container

I tried a bunch of the existing Hugo containers, but none of them had all the components required (hugo+git+openssh) to achieve my goal. So, I shaved a few yaks and created my own hugo-builder image that has all the bits I need.

Secret stuff

In order to check out content (and later push it back) to GitHub, I need to be able to use SSH and have a keypair configured.

Using GitLab Secret Variables

I recommend creating a specific keypair for your GitLab CI instance so that it is kept separate from your main SSH keypair. This is required to pull and push from GitHub.

Also, I don’t want to store the private key anywhere visible or in a repo, so I’m using GitLab Secret Variables instead. Create a variable named PRIVATE_KEY and store the content of ~/.ssh/id_rsa in that variable.

I also created a GITHUB_KEY variable that stores the public key of GitHub so I can automatically add it to ~/.ssh/authorized_keys in the build container.

Run Hugo

I had a bunch of problems with GitLab CI not properly pulling the Hugo theme I use which is configured as a git submodule. So, I do it manually as part of the before_script process in the CI pipeline.

Hugo stores its static output in the public/ folder. This folder is included in .gitignore for the source content so it is never stored on the local GitLab instance.

GitLab CI automatically checks out the latest commit from the repo when it starts a build, so before I run Hugo, I pull the current content from GitHub to create the public/ directory:

git clone git@github.com:username/username.github.io.git public

Setup and clone from GitLab is done in the before_script block.

Then, when Hugo runs, it will update files in that same location, so I can use git to check if the content has actually changed. This is done using the after_script block.

.gitlab-ci.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
image: avmiller/hugo-builder

build:
  stage: build
  before_script:
    - git submodule update --init
    - mkdir -p ~/.ssh
    - echo "$PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa && chmod 600 ~/.ssh/id_rsa
    - echo "$GITHUB_KEY" | tr -d '\r' > ~/.ssh/known_hosts
    - git config --global user.name "Your Name" && git config --global user.email your@email.com
    - git clone git@github.com:username/username.github.io.git public
  script:
    - hugo
  after_script:
    - export GIT_LOG_MESSAGE=`git log --format=%B -n 1 $CI_BUILD_REF`
    - cd public
    - if ! git diff --no-ext-diff --quiet --exit-code; then git add --all && git commit -sam "$GIT_LOG_MESSAGE" && git push -u origin master; fi
  only:
    - master

If all goes well, this will be the first new post that is actually published using this automated workflow.

Obviously, if it doesn’t, I’ll have to fix it and republish this page and you won’t know anyway. Magic!