Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e23884a
[RHDHBUGS-3010]: Fix CQA-14 PR build destruction, rewrite deploy as N…
themr0c Apr 24, 2026
36d11c8
chore: remove redundant CQA directory copy in pr.yml
themr0c Apr 24, 2026
71c6f6e
chore: remove unused build/scripts/build.sh
themr0c Apr 24, 2026
facbfcc
chore: remove unused build-cqa.sh and lint-scripts.sh
themr0c Apr 24, 2026
485abaa
docs: update CLAUDE.md workflow description for integrated cleanup
themr0c Apr 24, 2026
a54e5a1
docs: add README for build/scripts
themr0c Apr 24, 2026
3383cd8
docs: update README.adoc for removed build.sh and new deploy script
themr0c Apr 24, 2026
6b94dcf
docs: update README.adoc for current build tooling
themr0c Apr 24, 2026
56c1458
fix: resolve shellcheck and SonarCloud CI failures
themr0c Apr 24, 2026
ed11d44
Merge branch 'main' into fix/gh-pages-workflow
themr0c Apr 24, 2026
839bfac
fix: address PR review comments
themr0c Apr 24, 2026
9b3af2c
Merge branch 'main' into fix/gh-pages-workflow
themr0c Apr 24, 2026
aa4370c
fix: suppress SonarCloud S4036 on git rev-parse in CQA-14
themr0c Apr 24, 2026
238a361
refactor: simplify deploy-gh-pages.js
themr0c Apr 24, 2026
d728190
refactor: rewrite deploy-gh-pages back to bash
themr0c Apr 24, 2026
3ad2609
fix: address reviewer comments on deploy script
themr0c Apr 24, 2026
ba54154
fix: restore positional argument for publish_dir
themr0c Apr 24, 2026
d973368
perf: cache git ls-remote in cleanup to avoid N+1 remote calls
themr0c Apr 24, 2026
171f4a3
fix: remove dead orphan branch fallback from fetch_gh_pages
themr0c Apr 24, 2026
d632148
fix: add explicit return statements to bash functions (S7682)
themr0c Apr 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ When creating PRs, follow `.github/pull_request_template.md`:

| Workflow | Trigger | Purpose |
|---|---|---|
| `build-asciidoc.yml` | Push to main/release | Builds AsciiDoc docs and deploys to GitHub Pages. Cleans up merged PR preview branches. |
| `build-asciidoc.yml` | Push to main/release | Builds AsciiDoc docs and deploys to GitHub Pages (deploy includes cleanup of merged/closed PRs and deleted branches). |
| `pr.yml` | PR | Builds HTML preview, runs CQA checks, deploys to `gh-pages`, posts preview URL and CQA checklist as PR comments. Build scripts sourced from base branch. |
| `style-guide.yml` | PR | Runs Vale linter on `assemblies/` for style guide compliance. |
| `shellcheck.yml` | PR (`*.sh`) | Runs shellcheck on changed shell scripts via reviewdog. |
Expand Down
31 changes: 1 addition & 30 deletions .github/workflows/build-asciidoc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,39 +68,10 @@ jobs:
run: |
echo "Building branch ${{ env.GIT_BRANCH }}"
touch .lycheecache
build/scripts/build-ccutil.sh -b ${{ env.GIT_BRANCH }}
node build/scripts/build-orchestrator.js -b ${{ env.GIT_BRANCH }} --no-cqa

- name: Deploy to the gh-pages branch
env:
GITHUB_TOKEN: ${{ secrets.RHDH_BOT_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: bash build/scripts/deploy-gh-pages.sh ./titles-generated --message "Deploy ${{ env.GIT_BRANCH }}"

- name: Cleanup merged PR branches
run: |
PULL_URL="https://api.github.com/repos/redhat-developer/red-hat-developers-documentation-rhdh/pulls"
GITHUB_TOKEN="${{ secrets.RHDH_BOT_TOKEN }}"
git config user.name "rhdh-bot service account"
git config user.email "rhdh-bot@redhat.com"

git checkout gh-pages; git pull || true
dirs=$(find . -maxdepth 1 -name "pr-*" -type d | sed -r -e "s|^\./pr-||")
refs=$(cat pulls.html | grep pr- | sed -r -e "s|.+.html>pr-([0-9]+)</a>.+|\1|")
for d in $(echo -e "$dirs\n$refs" | sort -uV); do
PR="${d}"
echo -n "Check merge status of PR $PR ... "
PR_JSON=$(curl -sSL -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $GITHUB_TOKEN" "$PULL_URL/$PR")
if [[ $(echo "$PR_JSON" | grep merged\") == *"merged\": true"* ]]; then
echo "merged, can delete from pulls.html and remove folder $d"
git rm -fr --quiet "pr-${d}" || rm -fr "pr-${d}"
sed -r -e "/pr-$PR\/index.html>pr-$PR</d" -i pulls.html
elif [[ $(echo "$PR_JSON" | grep \"state\") == *"state\": \"closed\""* ]]; then
echo "closed, can delete from pulls.html and remove folder pr-${d}"
git rm -fr --quiet "pr-${d}" || rm -fr "pr-${d}"
sed -r -e "/pr-$PR\/index.html>pr-$PR</d" -i pulls.html
else
echo "PR is not closed or merged (or could not read API)"
fi
done
git commit -s -m "remove merged PR branches" . || true # don't fail if there's nothing to do
git push origin gh-pages || true # don't fail if there's nothing to do
4 changes: 0 additions & 4 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,6 @@ jobs:
for f in trusted-scripts/.lycheeignore trusted-scripts/lychee.toml; do
if [[ -f $f ]]; then rsync -az $f pr-content/; fi
done
if [[ -d "trusted-scripts/build/scripts/cqa" ]]; then
rm -rf pr-content/build/scripts/cqa
cp -r trusted-scripts/build/scripts/cqa pr-content/build/scripts/cqa
fi
touch pr-content/.lycheecache
cd pr-content
# Add base branch as remote so CQA checks can diff PR content against it
Expand Down
72 changes: 45 additions & 27 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ Before submitting a pull request (PR), create a Jira issue associated with your

There are currently three Jira projects related to RHDH:

* link:https://issues.redhat.com/projects/RHDHBUGS[RHDHBUGS] - For issues related to existing content.
* link:https://issues.redhat.com/projects/RHIDP[RHIDP] - For new features related to the core platform, including security mechanisms.
* https://issues.redhat.com/projects/RHDHPAI[RHDHPAI] - For new features related to plugins and AI.
* link:https://redhat.atlassian.net/jira/software/c/projects/RHDHBUGS/issues[RHDHBUGS] - Red Hat Developer Hub Bugs.
* link:https://redhat.atlassian.net/jira/software/c/projects/RHIDP/issues[RHIDP] - Red Hat Internal Developer Platform.
* link:https://redhat.atlassian.net/jira/software/c/projects/RHDHPLAN/issues[RHDHPLAN] - RH Developer Hub Planning.

Choose a Jira project that matches your contribution and create a new issue:

Expand All @@ -23,9 +23,8 @@ Choose a Jira project that matches your contribution and create a new issue:

[IMPORTANT]
====
RHDH release notes are single-sourced from the *Release Notes Text* field in the Jira Epic.
If you want to make changes to release notes, you must make them in Jira.
Check the link:https://docs.google.com/document/d/1X2CrrU9K3ZqbxN7DXmU6P1mY7inLD32qs3rpOUHz6Vw/edit?tab=t.0#heading=h.yqxd252ryhua[RHDH Release Notes process document] for more information.
RHDH release notes are single-sourced from Jira.
See the link:https://docs.google.com/document/d/1X2CrrU9K3ZqbxN7DXmU6P1mY7inLD32qs3rpOUHz6Vw/edit?tab=t.0#heading=h.yqxd252ryhua[RHDH Release Notes process document] and the link:https://gitlab.cee.redhat.com/red-hat-developers-documentation/red-hat-developer-hub-release-notes[release notes repository] (VPN required) for details.
====

## Style and formatting
Expand All @@ -46,47 +45,64 @@ to modularize your content.

## Building locally

. Install `asciidoctor` - see https://docs.asciidoctor.org/asciidoctor/latest/install/linux-packaging/
. Install Podman - see https://podman.io.
. Run the following command to generate html with images in titles-generated/ folders by using `ccutil` in a container.
The `ccutil` CLI tool reproduces the behavior of the production publication chain (Pantheon), including its limitations, and is therefore preferred over using a more recent `asciidoctor` CLI version.
.Prerequisites

* link:https://nodejs.org[Node.js]
* link:https://podman.io[Podman]
* link:https://github.com/errata-ai/vale[Vale]
* link:https://github.com/lycheeverse/lychee[Lychee] (downloaded automatically by the build script if not installed)
* Optional for local builds: A `GITHUB_TOKEN` environment variable to avoid GitHub API rate limiting during lychee link validation. Use link:https://cli.github.com[GitHub CLI] (`gh`) to get and refresh the token: `export GITHUB_TOKEN=$(gh auth token)`. This token is required in CI for deployment to `gh-pages`.

.Procedure

. Sync the Vale style rules:
+
[source,terminal]
----
$ build/scripts/build-ccutil.sh
$ vale sync
----

[NOTE]
====
* To build with `asciidoctor` rather than `ccutil`, run the following script.
The output will look differently and you might miss issues with content that cannot build in production.
. Build HTML with images in `titles-generated/` using `ccutil` in a container, then run lychee link validation and CQA content quality checks:
+
[source,terminal]
----
$ build/scripts/build.sh
$ ./build/scripts/build-ccutil.sh
----
====
+
The `ccutil` CLI tool reproduces the behavior of the production publication chain (Pantheon), including its limitations, and is therefore preferred over `asciidoctor`.

Results are written to `build-report.json`.

## Checking for broken links

You can run `lychee` locally to check for broken links in your PR.
The build command above includes lychee link validation automatically.
To run lychee separately on already-built HTML:

.Prerequisites

. Build the documentation first (see above).
. Run lychee on the generated HTML:
* link:https://github.com/lycheeverse/lychee[Lychee]
* Optional: A `GITHUB_TOKEN` environment variable to avoid GitHub API rate limiting. Use link:https://cli.github.com[GitHub CLI] (`gh`) to get and refresh the token: `export GITHUB_TOKEN=$(gh auth token)`

.Procedure

. Run lychee on the built HTML:
+
[source,terminal]
----
$ GITHUB_TOKEN=$(gh auth token) lychee titles-generated/
$ lychee titles-generated/
----
+
Install lychee from https://github.com/lycheeverse/lychee or the build orchestrator downloads it automatically.

You can run `linkchecker` to check for broken links in the published RHDH docs.
To check for broken links in the published RHDH docs, use `linkchecker`:

.Prerequisites

* link:https://github.com/linkchecker/linkchecker[linkchecker]

.Procedure

. Install `linkchecker` from https://github.com/linkchecker/linkchecker.
. Run the following command to create a .txt file with a list of detected broken links:
+
[code,terminal]
[source,terminal]
----
$ linkchecker --check-extern --output failures --file-output failures/broken-links.txt https://docs.redhat.com/en/documentation/red_hat_developer_hub/<version>
----
Expand All @@ -101,9 +117,11 @@ PRs have a link to the generated HTML attached as a comment.

The publication workflow has two stages:

. The link:.github/workflows/pr.yml[PR workflow] and link:.github/workflows/build-asciidoc.yml[GitHub Pages workflow] build HTML from AsciiDoc sources and push the output to the `gh-pages` branch using `build/scripts/deploy-gh-pages.sh`, which handles concurrent pushes with automatic retry.
. The link:.github/workflows/pr.yml[PR workflow] and link:.github/workflows/build-asciidoc.yml[GitHub Pages workflow] build HTML from AsciiDoc sources and push the output to the `gh-pages` branch using `build/scripts/deploy-gh-pages.js`, which handles concurrent pushes with automatic retry, cleanup of stale PR/branch directories, and index regeneration.
. The link:https://github.com/redhat-developer/red-hat-developers-documentation-rhdh/actions/workflows/pages/pages-build-deployment[GitHub Pages build and deployment] workflow, managed by GitHub, detects pushes to the `gh-pages` branch and publishes the content to GitHub Pages.

See link:docs/github-publication-workflow.md[GitHub Publication Workflow Architecture] for the full technical reference.

## Reviews

All PRs are reviewed for technical accuracy by an SME and writing quality by another tech writer.
Expand Down
35 changes: 35 additions & 0 deletions build/scripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Build Scripts

Build, deploy, and content quality tooling for the RHDH documentation project.

## Scripts

| Script | Purpose |
|---|---|
| `build-ccutil.sh` | Wrapper that delegates to `build-orchestrator.js`. Used as a fallback in `pr.yml` on older branches and for local builds. |
| `build-orchestrator.js` | Parallel documentation build orchestrator. Runs ccutil title builds, lychee link validation, and CQA assessment. Produces `build-report.json`. Supports `--no-cqa` and `--no-lychee` flags to skip phases. |
| `deploy-gh-pages.sh` | Deploys build output to the `gh-pages` branch. Handles cleanup of stale PR/branch directories, index regeneration with release notes links, and retry with rebase on push conflicts. |
| `error-patterns.json` | Regex patterns for classifying ccutil build errors into structured messages with cause and fix fields. |
| `update-cqa-resources.sh` | Fetches upstream Red Hat style guide resources into `.claude/resources/`. |

## CQA (`cqa/`)

Content Quality Assessment framework with 19 checks (CQA-00a through CQA-17).

```bash
node build/scripts/cqa/index.js titles/<title>/master.adoc # report
node build/scripts/cqa/index.js --fix titles/<title>/master.adoc # auto-fix
node build/scripts/cqa/index.js --check 14 titles/<title>/master.adoc # single check
node build/scripts/cqa/index.js --all # all titles
```

See `.claude/plugins/project-cqa/resources/cqa-spec.md` for the full specification.

## Workflows

These scripts are called by GitHub Actions workflows in `.github/workflows/`:

- **`build-asciidoc.yml`** (push to main/release) -- `build-orchestrator.js --no-cqa` + `deploy-gh-pages.sh`
- **`pr.yml`** (pull requests) -- `build-orchestrator.js` (or `build-ccutil.sh` on older branches) + `deploy-gh-pages.sh`

See `docs/github-publication-workflow.md` for the full architecture documentation.
15 changes: 0 additions & 15 deletions build/scripts/build-cqa.sh

This file was deleted.

95 changes: 30 additions & 65 deletions build/scripts/build-orchestrator.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
* node build/scripts/build-orchestrator.js -b main
* 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-lychee
*/

import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync, renameSync, copyFileSync } from 'node:fs';
import { resolve, dirname, join } from 'node:path';
import { spawn } from 'node:child_process';
import { cpus } from 'node:os';
import { fileURLToPath } from 'node:url';
import { get as httpsGet } from 'node:https';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

Expand All @@ -32,12 +32,14 @@ const SAFE_PATH = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
// ── Argument parsing ─────────────────────────────────────────────────────────

function parseArgs(argv) {
const args = { branch: 'main', verbose: false, jobs: cpus().length };
const args = { branch: 'main', verbose: false, jobs: cpus().length, lychee: true, cqa: true };
for (let i = 2; i < argv.length; i++) {
switch (argv[i]) {
case '-b': args.branch = argv[++i]; break;
case '--verbose': args.verbose = true; break;
case '--jobs': args.jobs = Number.parseInt(argv[++i], 10); break;
case '--no-lychee': args.lychee = false; break;
case '--no-cqa': args.cqa = false; break;
}
}
return args;
Expand Down Expand Up @@ -437,51 +439,6 @@ function generateBranchIndex(branch, results, repoRoot) {
writeFileSync(join(indexDir, 'index.html'), html);
}

function fetchUrl(url) {
return new Promise((resolve, reject) => {
httpsGet(url, (res) => {
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
fetchUrl(res.headers.location).then(resolve, reject);
return;
}
if (res.statusCode !== 200) {
res.resume();
reject(new Error(`HTTP ${res.statusCode}`));
return;
}
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => resolve(data));
res.on('error', reject);
}).on('error', reject);
});
}

async function updateRootIndex(branch, repoRoot) {
const isPR = branch.startsWith('pr-');
const indexFile = isPR ? 'pulls.html' : 'index.html';
const indexPath = join(repoRoot, 'titles-generated', indexFile);
const url = `${PAGES_BASE}/${indexFile}`;

// Fetch existing index from GitHub Pages
try {
const data = await fetchUrl(url);
writeFileSync(indexPath, data);
} catch {
// If fetch fails, create a minimal file
writeFileSync(indexPath, '<html><body><ul>\n</ul></body></html>');
}

const content = readFileSync(indexPath, 'utf8');
const link = `./${branch}/index.html`;
if (!content.includes(link)) {
console.log(`Building root index for ${branch} in titles-generated/${indexFile} ...`);
const entry = `<li><a href=${link}>${branch}</a></li>`;
const updated = content.replace('</ul>', `${entry}\n</ul>`);
writeFileSync(indexPath, updated);
}
}

// ── Summary output ───────────────────────────────────────────────────────────

function printFailedTitle(r) {
Expand Down Expand Up @@ -653,31 +610,39 @@ async function main() {
// Generate branch index HTML (only for passed titles)
generateBranchIndex(args.branch, buildResults, repoRoot);

// Update root index
await updateRootIndex(args.branch, repoRoot);

// Run lychee link validation
console.log('\nRunning link validation (lychee)...');
const lycheeResult = await runLychee(repoRoot, args.branch, args.verbose);
if (lycheeResult.errors.length === 0) {
lycheeResult.errors = classifyErrors(lycheeResult.output, patterns);
const skippedResult = { status: 'skipped', duration: 0, output: '', stats: { total: 0, successful: 0, errors: 0, excludes: 0, timeouts: 0 }, errors: [] };
let lycheeResult;
if (args.lychee) {
console.log('\nRunning link validation (lychee)...');
lycheeResult = await runLychee(repoRoot, args.branch, args.verbose);
if (lycheeResult.errors.length === 0) {
lycheeResult.errors = classifyErrors(lycheeResult.output, patterns);
}
} else {
console.log('\nSkipping link validation (--no-lychee)');
lycheeResult = { ...skippedResult };
}

// Run CQA content quality assessment
// Skip when CQA_RUNNING env is set (CQA-14 recursion guard)
const cqaResult = (process.env.CQA_RUNNING)
? { status: 'skipped', duration: 0, output: '', stats: { total: 0, pass: 0, fail: 0 } }
: await (async () => {
console.log('\nRunning CQA content quality assessment...');
return runCqa(repoRoot, args.verbose);
})();
const skippedCqa = { status: 'skipped', duration: 0, output: '', stats: { total: 0, pass: 0, fail: 0 } };
let cqaResult;
if (!args.cqa || process.env.CQA_RUNNING) {
if (!args.cqa) console.log('\nSkipping CQA (--no-cqa)');
cqaResult = skippedCqa;
} else {
// Write preliminary report so CQA-14 can read lychee results without rebuilding
const pendingCqa = { status: 'pending', duration: 0, output: '', stats: { total: 0, pass: 0, fail: 0 } };
writeReport(args.branch, buildResults, lycheeResult, pendingCqa, args.jobs, 0, repoRoot);

console.log('\nRunning CQA content quality assessment...');
process.env.CQA_RUNNING = '1';
cqaResult = await runCqa(repoRoot, args.verbose);
delete process.env.CQA_RUNNING;
}

const totalDuration = Math.round((Date.now() - totalStart) / 1000);

// Print summary
printSummary(buildResults, lycheeResult, cqaResult, patterns, totalDuration);

// Write JSON report
writeReport(args.branch, buildResults, lycheeResult, cqaResult, args.jobs, totalDuration, repoRoot);

// Exit with error if any builds, lychee, or CQA failed
Expand Down
Loading
Loading