Featured image of post Building a Continuous Integration Pipeline for Hugo with GitHub Actions

Building a Continuous Integration Pipeline for Hugo with GitHub Actions

A practical guide to implementing a CI pipeline using GitHub Actions for a Hugo-based static site. Learn how to automate validation and testing steps each time content is pushed, ensuring quality before merging to production.

This post is part of the DevSecOps Principles series.

Introduction

In modern development workflows, automation is essential. Whether you’re managing documentation, technical blogs, or production-ready websites, Continuous Integration (CI) helps maintain consistency and reliability across changes.

In this article, we’ll walk through building a basic CI pipeline for a Hugo static site using GitHub Actions that will simulate a team workflow. Each time a contributor pushes content to a feature branch, our pipeline will:

  • Validate the Markdown files

  • Simulate a Hugo site preview in a container

  • Perform a quick smoke test on the home page

  • Automatically merge the changes into main if everything passes

By the end of this guide, you’ll have a solid foundation to expand into more advanced CI/CD (Continuous Delivery/Deployment) setups tailored to your team or personal project.

Preriquisites

Before you start, make sure your environment meets the following requirements:

1. GitHub Repository

You should already have:

  • A GitHub repository containing your Hugo-based website.

  • A main branch and a feature branch.

To check:

1
git branch

2. Hugo Installed Locally (Optional but Useful)

You may want to preview changes locally before pushing.

To check:

1
hugo version

If not installed, follow instructions at gohugo.io

3. GitHub Actions Enabled

Nothing to install, GitHub Actions is built-in. You just need to add a workflow under:

1
.github/workflows/ci.yml

To do so:

  1. From your repo root, create the folders:
1
mkdir -p .github/workflows
  1. Create your CI workflow file:
1
vi .github/workflows/ci.yml

(or use any editor you like)

  1. Save and exit (we will paste our GitHub Actions workflow YAML content inside it later)

Don’t leave the file empty or it won’t save itself, add some commentaries like #CI Configuration File)

If everything is fine, you should have this folder setup:

1
2
3
.github/
└── workflows/
    └── ci.yml   ← this is the file you'll create

4. Node.js and npm (for Markdown linting)

If not already installed, run the following lines get the latest LTS version officially provided by NodeSource:

1
2
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs

Then check:

1
2
node -v #Should print "v22.17.1".
npm -v #Should print "10.9.2".

5. markdownlint-cli

Used in the CI to lint (running the program that will analyse code for potential errors) .md files.

To install globally (for testing locally):

1
sudo npm install -g markdownlint-cli

To check:

1
markdownlint --version

6. Docker

Used in the pipeline to simulate Hugo preview with an Nginx container.

To check:

1
docker --version

You should also be able to run:

1
docker run hello-world

7. HTTPie (for local testing)

To check:

1
http --version

If not installed:

1
sudo apt install httpie  # on Ubuntu/Debian

Pipeline Stages Breakdown

Stage Description
Stage 1 — Markdown Linting Ensures all .md files follow formatting standards (style, spacing, headers, etc.).
Stage 2 — Hugo Server in Container Launches a container running a local Hugo preview server to simulate how the site would render in production.
Stage 3 — HTTPie Smoke Test Sends a request to the homepage using HTTPie and checks that expected content is present (e.g., <title> or specific text).
Stage 4 — Auto Merge to main If all stages pass, the pipeline merges dev-feature-1 into main, following a “merge if green” principle common in modern CI/CD workflows.

CI Configuration File (ci.yml)

We’re now going to edit the Github Actions workflow file.

Step 1 — Markdown Linting

This step checks the syntax of all your .md. files.

Go to your .github/workflows/ci.yml and paste this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
name: CI for Hugo Blog

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  lint-markdown:
    name: Lint Markdown files
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Install Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 18

      - name: Install markdownlint-cli
        run: npm install -g markdownlint-cli

      - name: Run markdownlint
        run: markdownlint **/*.md

You can ignore certain files via .markdownlintignore if required..

Step 2 — Hugo Server in Container

Add this second job in the file after the lint-markdown one:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
  hugo-preview:
    name: Build Hugo Site in Container
    runs-on: ubuntu-latest
    needs: lint-markdown

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Docker
        uses: docker/setup-buildx-action@v3

      - name: Run Hugo server container
        run: |
          docker run --rm \
            -v ${{ github.workspace }}:/src \
            klakegg/hugo:ext \
            --minify
  • klakegg/hugo:ext is an official Docker image with Hugo + extended modules

  • --minify checks that site generation works even with optimization

✅ This job will automatically fail if Hugo detects a build error in your content.

Step 3 — HTTPie Smoke Test

This test verifies that the generated site contains the expected elements. To do this :

Add this third job inside ci.yml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
http-smoke-test:
  name: Smoke Test with HTTPie
  runs-on: ubuntu-latest
  needs: hugo-preview

  steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Install Hugo Extended
      run: |
        HUGO_VERSION="0.111.3"
        wget -q https://github.com/gohugoio/hugo/releases/download/v${HUGO_VERSION}/hugo_extended_${HUGO_VERSION}_Linux-64bit.tar.gz
        tar -xzf hugo_extended_${HUGO_VERSION}_Linux-64bit.tar.gz
        sudo mv hugo /usr/local/bin/hugo
        hugo version

    - name: Build site
      run: hugo -D

    - name: Serve site in background
      run: |
        python3 -m http.server 8080 --directory public &
        sleep 3

    - name: Install HTTPie
      run: sudo apt-get install -y httpie

    - name: Run HTTPie test
      run: |
        http --check-status GET http://localhost:8080 | grep -i "<title>"

This test simulates an HTTP request on the generated index.html file to ensure that everything has been done correctly on the content side.

  • Starts Hugo’s dev server on http://localhost:8080 in the background
  • Waits for it to boot
  • Sends an HTTP GET request to the homepage using HTTPie
  • Searches for <title> in the response content to validate output

Step 4 — Auto-Merge to main

Preriquisites

First of all, make sure the “Allow auto-merge” option is enabled on your GitHub repository:

  1. Go to GitHub > Settings.

  2. Scroll down until reaching “Pull Requests”

  3. Check ✅ “Allow auto-merge”.


Then add this job at the end of your file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  auto-merge:
    name: Auto-merge PR if CI passes
    runs-on: ubuntu-latest
    needs: [lint-markdown, hugo-preview, http-smoke-test]

    if: github.event_name == 'pull_request'

    permissions:
      pull-requests: write
      contents: write

    steps:
      - name: Enable auto-merge
        uses: "peter-evans/enable-pull-request-automerge@v3"
        with:
          pull-request-number: ${{ github.event.pull_request.number }}
          merge-method: squash

Summary of behavior :

  • When a contributor pushes a PR (e.g. dev-article-1), the 3 previous jobs (lint, build, test) run.

  • If everything passes, the auto-merge job activates the automatic merge.

  • GitHub then merges the PR into main without manual intervention

Now that we finished editing the ci.yml, the next step is to commit and push it so that GitHub Actions can start running the workflow.

Simulating a Content Contribution

1. Commit the ci.yml file

If you haven’t committed your updated workflow file yet:

1
2
3
git add .github/workflows/ci.yml
git commit -m "Add full CI pipeline with lint, Hugo preview, HTTP test, and auto-merge"
git push origin main

Note: The pipeline only runs on mainand pull_request events targeting main. So you’ll need to work in a feature branch next.

⚠️ Also don’t forget to generate a new token with workflow scope, else GitHub will refuse the push.

2. Create a Feature Branch for a New Article

Run this line to create a new branch in your repo:

1
git checkout -b dev-test-article

Then make a small change (e.g., add a commentary to a the index.md file of a post):

1
2
3
4
echo "# Test Article" >> content/post/<your-post>/index.md
git add content/post/<your-post>/index.md
git commit -m "Add test article"
git push origin dev-test-article

3. Open a Pull Request

Go to GitHub and create a PR from dev-test-article into main.

The workflow will now trigger:

  • Markdown linting
  • Hugo preview build
  • HTTPie smoke test
  • Auto-merge, if all pass

Once green, the PR will auto-merge, without any manual approval needed.

Looking good here ;)

CI pipeline Final Look

Troubleshooting

Common CI Error: can't evaluate field Lastmod in type page.Site

If you’re using hugo-theme-stack, you might hit a build error when Hugo tries to sort or access .Site.Lastmod.

This happens because Lastmod isn’t directly defined on page.Siteobjects.

Fix: Replace this line in your theme’s OpenGraph partial (generally located at themes/<your-theme>/layouts/partials/head/opengraph/provider/base.html):

1
{{ .Site.Lastmod }}

with:

1
2
3
4
5
{{ with (first 1 (sort .Site.RegularPages "Lastmod" "desc")) }}
  {{ with index . 0 }}
    <meta property="og:updated_time" content='{{ .Lastmod.Format "2006-01-02T15:04:05-07:00" | safeHTML }}' />
  {{ end }}
{{ end }}

⚠️ This bug won’t appear locally unless you’re running hugo --minify or building inside a container, like in CI.

Conclusion

I really encourage you to give this GitHub Actions CI setup a try for your Hugo site. It’s simple to configure, and once it’s running, it gives you a smooth and reliable publishing workflow out of the box.

With automated builds, linting, smoke tests, and even auto-merging, you’ll spend less time worrying about mistakes and more time writing great content.

Thanks for reading! I hope this helps make your publishing process cleaner, faster, and more enjoyable.

In this series:

  1. Enforcing Policy-as-Code Compliance in CI with OPA and Conftest
  2. Enforcing Dynamic Security Testing in CI with OWASP ZAP
  3. Building a Continuous Integration Pipeline for Hugo with GitHub Actions