Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
16 changes: 7 additions & 9 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ jobs:
with:
ref: ${{ github.event.pull_request.base.ref }}
path: trusted-scripts
sparse-checkout: build/scripts

- name: Checkout PR branch for content to build
uses: actions/checkout@v4
Expand All @@ -104,7 +105,7 @@ jobs:
# update
sudo apt-get update -y || true
# install
sudo apt-get -y -q install podman && podman --version
sudo apt-get -y -q install podman rsync && podman --version
echo "GIT_BRANCH=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> $GITHUB_ENV

- name: Install lychee
Expand Down Expand Up @@ -143,15 +144,12 @@ jobs:
CQA_BASE_REF: base/${{ github.event.pull_request.base.ref }}
run: |
echo "Building PR ${{ github.event.pull_request.number }}"
# Copy trusted build scripts from base branch (not content)
# Guard each file — older branches may not have them
for f in build/scripts/build-ccutil.sh build/scripts/build-orchestrator.js build/scripts/error-patterns.json lychee.toml .lycheeignore; do
if [[ -f "trusted-scripts/$f" ]]; then cp "trusted-scripts/$f" "pr-content/$f"; fi
rm -rf pr-content/build/scripts
rsync -az trusted-scripts/build/scripts pr-content/build/
# some files are new in main/1.10, so check if they exist before copying
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
11 changes: 9 additions & 2 deletions .github/workflows/shellcheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,16 @@ jobs:
- name: Get changed shell scripts
id: changed-files
run: |
# Get list of changed .sh files
# Get list of changed .sh files that still exist (exclude deletions)
git fetch origin ${{ github.base_ref }}
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep '\.sh$' || echo "")
ALL_CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep '\.sh$' || echo "")
CHANGED_FILES=""
while IFS= read -r file; do
if [ -n "$file" ] && [ -f "$file" ]; then
CHANGED_FILES="${CHANGED_FILES:+$CHANGED_FILES
}$file"
fi
done <<< "$ALL_CHANGED"
echo "changed_files<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGED_FILES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
Expand Down
3 changes: 3 additions & 0 deletions .lycheeignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ file://.*titles-generated/index\.html$

# External sites that block automated requests
^https://fonts\.google\.com/icons

# Release notes site is behind Red Hat VPN, unreachable from GitHub Actions
^https://red-hat-developers-documentation\.pages\.redhat\.com/red-hat-developer-hub-release-notes
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
80 changes: 0 additions & 80 deletions build/scripts/build.sh

This file was deleted.

29 changes: 18 additions & 11 deletions build/scripts/cqa/checks/cqa-14-no-broken-links.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,18 +88,25 @@ function getLycheeIssues(root) {
if (_lycheeIssuesCache !== null) return _lycheeIssuesCache;
_lycheeIssuesCache = [];

try {
// Run build orchestrator (builds fresh HTML + runs lychee with remapping)
// Set CQA_RUNNING to prevent build-orchestrator from running CQA again (recursion)
execFileSync('node', ['build/scripts/build-orchestrator.js', '-b', 'main'], { // NOSONAR — fixed args, no user input
cwd: root,
stdio: 'pipe',
timeout: 600000, // 10 minutes
env: { ...process.env, CQA_RUNNING: '1' },
});
} catch {
// Build may exit non-zero if lychee finds broken links — that's expected
if (!process.env.CQA_RUNNING) {
// Standalone mode: build current state to get lychee results.
// Detect current branch for correct output directory naming and link remapping.
let branch = 'main';
try {
branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: root, encoding: 'utf8' }).trim(); // NOSONAR
} catch { /* fall back to main */ }
try {
execFileSync('node', ['build/scripts/build-orchestrator.js', '-b', branch, '--no-cqa'], { // NOSONAR — fixed args, no user input
cwd: root,
stdio: 'pipe',
timeout: 600000,
env: { ...process.env, CQA_RUNNING: '1' },
});
} catch {
// Orchestrator exits non-zero when lychee finds broken links; the report is still written
}
}
// When CQA_RUNNING is set: preliminary report already exists with lychee results

// Read the build report
const reportPath = join(root, 'build-report.json');
Expand Down
Loading
Loading