The RHDH documentation project uses GitHub Actions to build AsciiDoc documentation and deploy HTML previews to GitHub Pages via the gh-pages branch. Two workflows handle this:
build-asciidoc.yml-- triggered on branch pushes, builds and deploys production documentation.pr.yml-- triggered on pull requests, builds preview HTML and posts a PR comment with a preview link and CQA checklist.
Both workflows produce HTML output under titles-generated/<branch>/, then push the result to the gh-pages branch using deploy-gh-pages.sh.
| Workflow | Event | Branches | Build Script |
|---|---|---|---|
build-asciidoc.yml |
push |
main, release-1.**, rhdh-1.**, 1.**.x |
build-orchestrator.js --no-cqa |
pr.yml |
pull_request_target |
main, release-1.**, release-2.** |
release-1.9+/main: build-orchestrator.js; release-1.8: build-ccutil.sh (base branch scripts) |
The build-asciidoc.yml workflow calls build-orchestrator.js --no-cqa (CQA results aren't surfaced in branch builds, only in PR comments). The pr.yml workflow detects whether build-orchestrator.js exists on the base branch and uses it when available (release-1.9+, main), falling back to build-ccutil.sh on older branches (release-1.8). The orchestrator wraps ccutil with parallel execution, lychee link validation, CQA assessment, and JSON reporting.
The pr.yml workflow uses pull_request_target instead of pull_request so it can access repository secrets (needed for RHDH_BOT_TOKEN to push to gh-pages and post PR comments). This event runs workflow code from the base branch, not the PR, which avoids exfiltration of secrets from untrusted PRs.
To separate trusted code from untrusted content:
- Trusted checkout -- checks out
build/scriptsfrom the base branch (sparse-checkout: build/scripts) intotrusted-scripts/. - Content checkout -- checks out the full PR head into
pr-content/. - Merge -- the workflow replaces
pr-content/build/scriptswithtrusted-scripts/build/scriptsviarsync, then runs the build frompr-content/.
Build scripts are always sourced from the base branch, never from the PR. This prevents a malicious PR from modifying build scripts to exfiltrate secrets.
The workflow enforces team-based authorization before running the build:
check-commit-author-- uses a GitHub App token to check if the PR author is a member of therhdhteam in theredhat-developerorganization.authorize-- selects theinternalorexternalenvironment:- Internal: PR author is in the
rhdhteam, or the PR is from the same repository (not a fork). Runs immediately. - External: fork PRs from non-team members. The
externalenvironment requires manual approval from therhdh-contentteam before the build proceeds.
- Internal: PR author is in the
adoc_build-- depends onauthorize, so it only runs after the gate passes.
The orchestrator replaces the sequential build-ccutil.sh with parallel title builds, structured error reporting, and a JSON report.
Phases:
- Title discovery -- scans
titles/for directories containingmaster.adoc. - Parallel builds -- runs
podman run ... ccutil compilefor each title, limited by a semaphore (--jobs, defaults to CPU count). Each title produces HTML undertitles-generated/<branch>/<title>/. - Image copy -- parses each generated
index.htmlto find image references and copies them into the output directory. - Branch index -- generates
titles-generated/<branch>/index.htmllisting all successfully built titles, with an optional release notes link. - Lychee link validation -- runs
lycheeagainsttitles-generated/with cross-title link remapping (rewritesdocs.redhat.comlinks to local file paths). Broken links are traced back to.adocsource files viagrep. - Preliminary report -- writes
build-report.jsonwith lychee results and CQA status "pending". This allows CQA-14 to read lychee results without triggering a rebuild. - CQA assessment -- sets
CQA_RUNNING=1inprocess.env, then runsnode build/scripts/cqa/index.js --all. The env var propagates to CQA-14, which skips its internal orchestrator call and reads the preliminary report instead. - Final report -- overwrites
build-report.jsonwith completed CQA results.
Error classification: the orchestrator loads build/scripts/error-patterns.json, which maps regex patterns to structured error messages with cause and fix fields. These appear in the JSON report and PR comment.
CQA-14 recursion guard: CQA-14 (lychee link validation check) can trigger the orchestrator internally. To prevent infinite recursion, the orchestrator sets CQA_RUNNING=1 when invoking CQA, so CQA-14 reads existing lychee results from the report instead of triggering a full rebuild.
CLI usage:
node build/scripts/build-orchestrator.js -b <branch>
node build/scripts/build-orchestrator.js -b pr-123 --verbose
node build/scripts/build-orchestrator.js -b main --jobs 4
node build/scripts/build-orchestrator.js -b main --no-cqa --no-lycheeThe -b flag determines the output directory name under titles-generated/. --no-cqa and --no-lychee skip CQA and lychee respectively (used by build-asciidoc.yml where CQA results aren't surfaced). The orchestrator exits with code 1 if any enabled phase fails.
Handles deployment of built content to the gh-pages branch, including cleanup and index regeneration in a single commit.
Sequence:
- Detect the branch directory from
<publish_dir>(single top-level directory, e.g.,main/,pr-123/). - Create a temporary git repo with
github-actions[bot]identity. - Fetch
gh-pages(shallow, depth=1). - Copy
<publish_dir>contents into the working tree. - For branch deploys: run cleanup (see Cleanup section below).
- Regenerate indexes from current directories on
gh-pages(see below). - Stage all changes (content + cleanup deletions + indexes), commit, and push.
Index regeneration: rebuilds HTML indexes from directories present on gh-pages:
index.html-- lists all non-pr-*directories with optional release notes links (forrelease-1.9+andmain).pulls.html-- lists allpr-*directories.
Branch deploys regenerate both indexes. PR deploys regenerate pulls.html only.
Retry logic: on push rejection, the script attempts git pull --rebase. If rebase succeeds, it pushes immediately. If rebase conflicts, it aborts, re-fetches gh-pages, re-applies content and cleanup, and retries. Maximum 3 attempts.
Invocation:
# Branch deploy
bash build/scripts/deploy-gh-pages.sh ./titles-generated --message "Deploy main"
# PR deploy (from pr.yml, using trusted scripts)
bash trusted-scripts/build/scripts/deploy-gh-pages.sh ./pr-content/titles-generated --message "Deploy PR 123 preview"- Branch deploys (
build-asciidoc.yml): deploy content + cleanup stale PRs/branches + regenerate bothindex.htmlandpulls.html→ single commit. - PR deploys (
pr.yml): deploy content underpr-<N>/+ regeneratepulls.htmlonly → single commit. No cleanup runs.
gh-pages/
|-- index.html # Links to branch builds + release notes
|-- pulls.html # Links to PR preview builds
|-- main/ # Main branch build
| |-- index.html # Per-branch title listing
| +-- <title>/ # Individual title HTML
| +-- index.html
|-- release-1.9/ # Release branch build
|-- release-1.8/ # Legacy release branch
+-- pr-123/ # PR preview build
|-- index.html
+-- <title>/
Preview URL pattern:
https://redhat-developer.github.io/red-hat-developers-documentation-rhdh/<branch-or-pr>/
For PR previews:
https://redhat-developer.github.io/red-hat-developers-documentation-rhdh/pr-<N>/
- PR opened, synchronized, reopened, or marked ready for review --
pr.ymltriggers. - Authorization gate checks team membership. Fork PRs from non-team members require manual approval via the
externalenvironment. - Trusted build scripts are checked out from the base branch. PR content is checked out separately.
- Build scripts from the base replace
pr-content/build/scripts. The orchestrator (orbuild-ccutil.shon older branches) runs with-b pr-<N>. - If HTML was successfully generated (checked via
build-report.json),deploy-gh-pages.shpushes the output togh-pagesunderpr-<N>/. - A consolidated PR comment is posted (or updated) with:
- Build status (passed/failed) with title counts and duration.
- Preview link (marked stale if title build failed).
- Build error details with classified causes and fixes.
- CQA checklist with pass/fail counts (when available).
- Link to full CI logs.
- Old standalone CQA comments (from the previous two-comment format) are cleaned up.
- When the PR is merged or closed, the next branch deploy cleans up the
pr-<N>/directory fromgh-pages.
Concurrency: the workflow uses concurrency groups keyed on the PR number. If a new push arrives while a build is in progress, the in-progress run is cancelled.
Cleanup is integrated into deploy-gh-pages.sh and runs during branch deploys only, not during PR deploys. It executes before index regeneration so indexes reflect the cleaned-up state. Cleanup, content deployment, and index regeneration are committed together in a single commit.
- List
pr-*directories ongh-pages. - For each, query the GitHub API (
GET /repos/{owner}/{repo}/pulls/{number}). - If the PR is merged or closed, remove the
pr-<N>/directory.
- List non-
pr-*directories ongh-pages. - For each, run
git ls-remote --heads origin <branch>. - If the remote branch no longer exists, remove the directory. This cleans up directories for deleted branches (e.g.,
release-1.9-post-cqa).
node build/scripts/build-orchestrator.js -b mainRequires Podman. Builds all titles in parallel, runs lychee link validation, runs CQA, and writes build-report.json.
# All checks on a single title
node build/scripts/cqa/index.js titles/<title>/master.adoc
# Auto-fix issues
node build/scripts/cqa/index.js --fix titles/<title>/master.adoc
# Run a specific check
node build/scripts/cqa/index.js --check NN titles/<title>/master.adoc
# All checks on all titles
node build/scripts/cqa/index.js --allCQA-14 (lychee link validation) in standalone mode runs the orchestrator internally. It sets CQA_RUNNING=1 to prevent recursion -- the orchestrator skips CQA when this variable is set, so CQA-14 reads the lychee results from the existing build-report.json instead of triggering another full build.
build/scripts/build-ccutil.sh -b <branch>Used on release-1.8 and as a fallback on branches where build-orchestrator.js does not exist. Runs title builds sequentially without lychee or CQA.