Zola Workflow: Branches, PRs, and a Build Gate

I’ve been running a Zola static site for a while, deploying via Cloudflare Pages. The workflow was simple: write something, git push, Cloudflare rebuilds. Fine, until I started fat-fingering TOML frontmatter and pushing a broken site to prod.

This post covers the small amount of work I did to fix that.

The bash functions

I had a few helper functions in .bashrc for managing Zola content. They worked but had a rough edge: zola-edit with no arguments would error out instead of just showing you what posts exist. Small thing, annoying in practice.

Fixed it to list available posts when called with no arguments:

zola-edit() {
    local SLUG="$1"
    if [ -z "$SLUG" ]; then
        echo "Available posts:"
        ls "$ZOLA_ROOT/content/posts/"
        return 0
    fi
    local TARGET="$ZOLA_ROOT/content/posts/$SLUG/index.md"
    if [ -f "$TARGET" ]; then
        $ZOLA_EDITOR "$TARGET"
    else
        echo "Error: Could not find $TARGET"
        echo "Available posts:"
        ls "$ZOLA_ROOT/content/posts/"
    fi
}

The bigger change was zola-pub. Previously it just committed and pushed to main directly. Now it creates a dated branch, pushes it, and opens a PR:

zola-pub() {
    local MSG="$1"
    if [ -z "$MSG" ]; then
        echo "Usage: zola-pub \"commit message\""
        return 1
    fi
    local BRANCH="publish/$(date +%Y%m%d-%H%M%S)"
    (
        cd "$ZOLA_ROOT" || exit 1
        echo "Creating branch $BRANCH..."
        git checkout -b "$BRANCH"
        git add .
        git commit -m "$MSG"
        git push -u origin "$BRANCH"
        echo "Opening PR..."
        gh pr create --title "$MSG" --body "Automated PR from zola-pub" --base main
    )
}

This requires the gh CLI, covered below.

The GitHub Actions workflow

The point of the branch/PR flow is to get a build check before anything touches main. Cloudflare rebuilds on every push to main, so a broken merge means a broken site. The workflow runs zola build --drafts on every PR and comments with a list of changed content files.

Create .github/workflows/build-check.yml in your repo:

name: Zola Build Check
on:
  pull_request:
    branches:
      - main
jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      contents: read
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: true
          fetch-depth: 0

      - name: Get changed content files
        id: changed
        run: |
          git fetch origin main
          CHANGED=$(git diff --name-only origin/main...HEAD -- 'content/' | grep '\.md$' || true)
          if [ -z "$CHANGED" ]; then
            SUMMARY="No content files changed."
          else
            SUMMARY=$(echo "$CHANGED" | sed 's/^/- /')
          fi
          echo "summary<<EOF" >> $GITHUB_OUTPUT
          echo "$SUMMARY" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

      - name: Install Zola
        run: |
          wget -q https://github.com/getzola/zola/releases/download/v0.21.0/zola-v0.21.0-x86_64-unknown-linux-gnu.tar.gz
          tar -xzf zola-v0.21.0-x86_64-unknown-linux-gnu.tar.gz
          sudo mv zola /usr/local/bin/

      - name: Build site (including drafts)
        run: zola build --drafts

      - name: Comment on PR
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const summary = `${{ steps.changed.outputs.summary }}`;
            const body = [
              '## ✅ Zola build passed',
              '',
              '**Changed content files:**',
              summary,
            ].join('\n');
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: body
            });

A couple of things worth noting: the permissions block is required — without pull-requests: write the comment step will fail silently on repos with restrictive default settings. And fetch-depth: 0 is needed so the diff against origin/main has enough history to work correctly.

The --drafts flag on the build step means draft posts get validated too, not just published ones. That’s the whole point — catching frontmatter errors wherever they are.

The gh CLI

gh is GitHub’s official CLI tool. Install it once:

sudo apt install gh

Authenticate once:

gh auth login
# Choose: GitHub.com → HTTPS → Login with a web browser

It writes a token to ~/.config/gh/hosts.yml. You won’t be prompted again.

From there, the full publish flow from the terminal is:

zola-pub "my commit message"          # branch, commit, push, open PR
gh run watch                           # stream the Actions output live
gh pr merge --squash --delete-branch  # merge and clean up

Cloudflare picks up the merge to main and does the rest.

It’s a bit more ceremony than a direct push to main, but the build gate is worth it. I’ve already caught a couple of frontmatter mistakes that would have broken the site.