Publish the site with GitHub Actions

This how-to guide wires your Antora playbook repository into GitHub Actions so that every merge to main rebuilds the site and publishes it to GitHub Pages — with broken cross-references failing the build instead of shipping. "The published docs are stale" stops being a possible state of the world.

It assumes you have a working playbook repository (the tutorial builds one) hosted on GitHub, with content sources reachable over https://.

Before you start

  • Your playbook's content.sources entries must use https:// git URLs, not local paths. CI has no ../widget to read.
  • In the repository's Settings → Pages, set the source to GitHub Actions.

1. Pin the Antora version

CI must not float to whatever Antora published last night. In the playbook repository, make the dependencies explicit in package.json:

npm install --save-exact @antora/cli@3.1.14 @antora/site-generator@3.1.14

Commit both package.json and package-lock.json. The workflow will use npm ci, which installs exactly the locked versions: same input, same site, every run.

2. Make broken references fail the build

By default Antora logs unresolved xrefs and missing includes as errors but still exits successfully. For CI you want the honest version. Add this to antora-playbook.yml:

runtime:
log:
failure_level: error

With failure_level: error, any logged error (a typo'd xref, a missing partial, a dangling nav entry) makes antora exit non-zero, which fails the workflow, which blocks the merge. This single key is most of the quality story of docs as code; set it before anything else depends on the pipeline.

Also set the site's public URL, which Antora needs to generate the sitemap and canonical links:

site:
url: https://your-org.github.io/docs-site

3. Add the workflow

Create .github/workflows/publish.yml in the playbook repository:

name: Publish docs
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install Antora
run: npm ci
- name: Build the site
run: npx antora --fetch antora-playbook.yml
- uses: actions/upload-pages-artifact@v3
with:
path: build/site
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4

The pieces worth understanding rather than pasting:

  • --fetch tells Antora to refresh its git cache instead of trusting a previous clone. On a throwaway CI runner there usually is no cache, but the flag also protects you on self-hosted runners where there is.
  • workflow_dispatch gives you a manual "rebuild now" button, useful when a content repository merged a docs change and you want the site to pick it up without waiting for a playbook commit (see the note below).
  • The permissions block is the Pages deployment contract: pages: write and id-token: write are what deploy-pages needs.

4. Rebuild when content changes

The workflow above triggers on pushes to the playbook repository, but most docs changes land in content repositories. Close the loop by having each content repository poke the playbook repo when docs change. In the content repository:

name: Trigger docs publish
on:
push:
branches: [main]
paths: ["docs/**"]
jobs:
trigger:
runs-on: ubuntu-latest
steps:
- run: gh workflow run publish.yml --repo your-org/docs-site
env:
GH_TOKEN: ${{ secrets.DOCS_SITE_TRIGGER_TOKEN }}

DOCS_SITE_TRIGGER_TOKEN is a fine-grained personal access token (or a GitHub App token) with permission to run workflows on the playbook repository. The paths filter keeps code-only merges from rebuilding the docs site for nothing.

5. Authenticate against private content repositories

If a content source is private, the CI build needs credentials to clone it. Antora reads them from the GIT_CREDENTIALS environment variable. Add a token with read access to the content repositories as a secret on the playbook repo, then extend the build step:

- name: Build the site
run: npx antora --fetch antora-playbook.yml
env:
GIT_CREDENTIALS: https://token:${{ secrets.CONTENT_READ_TOKEN }}@github.com

Keep the playbook's content.sources URLs clean; credentials belong in the environment, never committed to the playbook.

Troubleshooting

  • The build fails fetching the UI bundle. The default-UI URL serves a snapshot artifact, which is why the playbook marks it snapshot: true. If your network policy blocks gitlab.com, download the bundle once, commit it to the playbook repo (or publish it as a release asset), and point ui.bundle.url at that instead.
  • The site builds but styles are missing on Pages. Your site.url is wrong or absent. Project pages live under a subpath (/docs-site/), and the UI resolves assets relative to the site URL.
  • A content change merged but the site didn't update. The trigger workflow in step 4 isn't installed in that content repository, or its paths filter didn't match. Run the publish workflow manually (workflow_dispatch) while you fix the trigger.
  • The build fails on an xref you just removed the target of. That's the pipeline working. Fix the reference — don't lower failure_level.