diff --git a/.github/workflows/build-asciidoc.yml b/.github/workflows/build-asciidoc.yml index 2cb7ff3b3bc..48c81fa017e 100644 --- a/.github/workflows/build-asciidoc.yml +++ b/.github/workflows/build-asciidoc.yml @@ -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]+).+|\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-$PRpr-$PR> $GITHUB_ENV - name: Install lychee @@ -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 diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index dfe10e639b5..6686d48480e 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -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<> $GITHUB_OUTPUT echo "$CHANGED_FILES" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT diff --git a/.lycheeignore b/.lycheeignore index cbb2446e6b3..39443d7c8a6 100644 --- a/.lycheeignore +++ b/.lycheeignore @@ -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 diff --git a/build/scripts/build-cqa.sh b/build/scripts/build-cqa.sh deleted file mode 100755 index e1b101cd0f8..00000000000 --- a/build/scripts/build-cqa.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright (c) Red Hat, Inc. -# This program and the accompanying materials are made -# available under the terms of the Eclipse Public License 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0/ -# -# SPDX-License-Identifier: EPL-2.0 -# -# Requires: Node.js - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -pushd "${SCRIPT_DIR}" >/dev/null || exit -node "cqa/index.js" --all # "$@" -popd >/dev/null || exit diff --git a/build/scripts/build-orchestrator.js b/build/scripts/build-orchestrator.js index 4c8ca1f7312..ce3a47d7b9b 100755 --- a/build/scripts/build-orchestrator.js +++ b/build/scripts/build-orchestrator.js @@ -9,6 +9,7 @@ * 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'; @@ -16,7 +17,6 @@ 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); @@ -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; @@ -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, ''); - } - - 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 = `
  • ${branch}
  • `; - const updated = content.replace('', `${entry}\n`); - writeFileSync(indexPath, updated); - } -} - // ── Summary output ─────────────────────────────────────────────────────────── function printFailedTitle(r) { @@ -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 diff --git a/build/scripts/build.sh b/build/scripts/build.sh deleted file mode 100755 index d0315b21ddd..00000000000 --- a/build/scripts/build.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright (c) Red Hat, Inc. -# This program and the accompanying materials are made -# available under the terms of the Eclipse Public License 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0/ -# -# SPDX-License-Identifier: EPL-2.0 -# -# Utility script build html previews with referenced images -# Requires: asciidoctor - see https://docs.asciidoctor.org/asciidoctor/latest/install/linux-packaging/ -# input: titles/ -# output: titles-generated/ and titles-generated/$BRANCH/ - -# grep regex for title folders to exclude from processing below -EXCLUDED_TITLES="rhdh-plugins-reference" -BRANCH="main" - -while [[ "$#" -gt 0 ]]; do - case $1 in - '-b') BRANCH="$2"; shift 1;; - esac - shift 1 -done - -rm -fr titles-generated/; -mkdir -p titles-generated/"${BRANCH}"; -echo "Red Hat Developer Hub Documentation Preview - ${BRANCH}" >> titles-generated/"${BRANCH}"/index.html - -# shellcheck disable=SC2143 -if [[ $BRANCH == "pr-"* ]]; then - # fetch the existing https://redhat-developer.github.io/red-hat-developers-documentation-rhdh/index.html to add prs and branches - curl -sSL https://redhat-developer.github.io/red-hat-developers-documentation-rhdh/pulls.html -o titles-generated/pulls.html - if [[ -z $(grep "./${BRANCH}/index.html" titles-generated/pulls.html) ]]; then - echo "Building root index for $BRANCH in titles-generated/pulls.html ..."; - echo "
  • ${BRANCH}
  • " >> titles-generated/pulls.html - fi -else - # fetch the existing https://redhat-developer.github.io/red-hat-developers-documentation-rhdh/index.html to add prs and branches - curl -sSL https://redhat-developer.github.io/red-hat-developers-documentation-rhdh/index.html -o titles-generated/index.html - if [[ -z $(grep "./${BRANCH}/index.html" titles-generated/index.html) ]]; then - echo "Building root index for $BRANCH in titles-generated/index.html ..."; - echo "
  • ${BRANCH}
  • " >> titles-generated/index.html - fi -fi diff --git a/build/scripts/cqa/checks/cqa-14-no-broken-links.js b/build/scripts/cqa/checks/cqa-14-no-broken-links.js index c30e32e673b..c39a7071c8d 100644 --- a/build/scripts/cqa/checks/cqa-14-no-broken-links.js +++ b/build/scripts/cqa/checks/cqa-14-no-broken-links.js @@ -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'); diff --git a/build/scripts/deploy-gh-pages.sh b/build/scripts/deploy-gh-pages.sh index aa8839e9388..2037ada969f 100755 --- a/build/scripts/deploy-gh-pages.sh +++ b/build/scripts/deploy-gh-pages.sh @@ -7,8 +7,18 @@ # # SPDX-License-Identifier: EPL-2.0 # -# Deploy files to the gh-pages branch with retry on push rejection. -# Replaces peaceiris/actions-gh-pages to handle concurrent builds. +# Deploy build output (titles-generated/) to the gh-pages branch. +# +# Flow: +# 1. Create a temp git repo, fetch gh-pages (shallow) +# 2. Copy --publish-dir content into the working tree +# 3. For branch deploys: clean up stale PR and branch directories +# 4. Regenerate index.html (branch list) and pulls.html (PR list) +# 5. Commit everything (content + cleanup + indexes) and push +# 6. On push rejection: rebase and retry (max 3 attempts) +# +# Branch deploys clean up merged/closed PR dirs and deleted branch dirs. +# PR deploys only update content and pulls.html — no cleanup. # # Usage: deploy-gh-pages.sh [--message ] # @@ -16,6 +26,11 @@ set -euo pipefail +MAX_RETRIES=3 +RELEASE_NOTES_BASE="https://red-hat-developers-documentation.pages.redhat.com/red-hat-developer-hub-release-notes" + +# ── Parse arguments ────────────────────────────────────────────────────────── + PUBLISH_DIR="${1:?Usage: deploy-gh-pages.sh [--message ]}" shift @@ -28,73 +43,244 @@ while [[ $# -gt 0 ]]; do done PUBLISH_DIR="$(cd "$PUBLISH_DIR" && pwd)" +: "${GITHUB_TOKEN:?GITHUB_TOKEN is required}" +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" -: "${GITHUB_TOKEN:?GITHUB_TOKEN is required (set by GitHub Actions)}" -: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required (set by GitHub Actions)}" +# Detect branch directory (first non-hidden top-level dir in publish dir) +BRANCH_DIR="" +for d in "$PUBLISH_DIR"/*/; do + [[ -d "$d" ]] || continue + name="$(basename "$d")" + [[ "$name" == .* ]] && continue + BRANCH_DIR="$name" + break +done + +if [[ -z "$BRANCH_DIR" ]]; then + echo "No top-level directory found in publish dir" >&2 + exit 1 +fi -# ── Diagnostics: log PUBLISH_DIR contents before deploying ── echo "PUBLISH_DIR: $PUBLISH_DIR" -echo "Top-level entries in PUBLISH_DIR:" -find "$PUBLISH_DIR" -maxdepth 1 -not -path "$PUBLISH_DIR" -printf '%f\n' +echo "Branch directory: $BRANCH_DIR" + +# ── Set up temp deploy repo ────────────────────────────────────────────────── -MAX_RETRIES=3 DEPLOY_DIR="$(mktemp -d)" trap 'rm -rf "$DEPLOY_DIR"' EXIT -cd "$DEPLOY_DIR" -git init -q -git config user.name "github-actions[bot]" -git config user.email "github-actions[bot]@users.noreply.github.com" -git remote add origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" - -# ── Fetch gh-pages and prepare working tree ── -fetch_output=$(git fetch origin gh-pages --depth=1 2>&1) && fetch_ok=true || fetch_ok=false -if [[ "$fetch_ok" == "true" ]]; then - git checkout -B gh-pages FETCH_HEAD -elif echo "$fetch_output" | grep -qi "not found\|couldn't find\|no such remote ref"; then - echo "gh-pages branch does not exist, creating orphan" - git checkout --orphan gh-pages - git rm -rf . 2>/dev/null || true -else - echo "ERROR: Failed to fetch gh-pages: $fetch_output" >&2 - exit 1 -fi +git -C "$DEPLOY_DIR" init -q +git -C "$DEPLOY_DIR" config user.name "github-actions[bot]" +git -C "$DEPLOY_DIR" config user.email "github-actions[bot]@users.noreply.github.com" -# ── Copy content and stage ── -cp -a "$PUBLISH_DIR"/. . +REPO_URL="https://github.com/${GITHUB_REPOSITORY}.git" +git -C "$DEPLOY_DIR" remote add origin "$REPO_URL" +# Auth via http.extraHeader keeps the token out of the remote URL (avoids leaking in logs) +CREDENTIALS="$(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 -w0)" +git -C "$DEPLOY_DIR" config "http.${REPO_URL}.extraHeader" "Authorization: Basic ${CREDENTIALS}" -# Force-add only the files from PUBLISH_DIR (bypasses .gitignore) -publish_entries=() -while IFS= read -r entry; do - publish_entries+=("$entry") -done < <(find "$PUBLISH_DIR" -maxdepth 1 -not -path "$PUBLISH_DIR" -printf '%f\n') +# ── Core functions ─────────────────────────────────────────────────────────── -echo "Staging ${#publish_entries[@]} entries from PUBLISH_DIR..." -git add --force -- "${publish_entries[@]}" +# gh-pages exists in practice; orphan path is a bootstrap safety net +fetch_gh_pages() { + if git -C "$DEPLOY_DIR" fetch origin gh-pages --depth=1 2>/dev/null; then + git -C "$DEPLOY_DIR" checkout -B gh-pages FETCH_HEAD + else + echo "gh-pages branch does not exist, creating orphan" + git -C "$DEPLOY_DIR" checkout --orphan gh-pages + git -C "$DEPLOY_DIR" rm -rf . 2>/dev/null || true + fi +} -if git diff --cached --quiet; then - echo "No changes to deploy" - exit 0 -fi +apply_content() { + cp -a "$PUBLISH_DIR"/. "$DEPLOY_DIR"/ +} + +# ── Cleanup (branch deploys only) ──────────────────────────────────────────── + +get_pr_state() { + local pr_number="$1" + local owner="${GITHUB_REPOSITORY%%/*}" + local repo="${GITHUB_REPOSITORY##*/}" + local response status merged + + response="$(curl -sf \ + -H "Authorization: Bearer $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/repos/${owner}/${repo}/pulls/${pr_number}" 2>/dev/null)" || { echo "unknown"; return; } + + status="$(printf '%s' "$response" | grep -o '"state": *"[^"]*"' | head -1 | grep -o '"[^"]*"$' | tr -d '"')" + merged="$(printf '%s' "$response" | grep -o '"merged": *[a-z]*' | head -1 | grep -o '[a-z]*$')" + + if [[ "$status" == "closed" ]]; then + [[ "$merged" == "true" ]] && echo "merged" || echo "closed" + else + echo "${status:-unknown}" + fi +} + +cleanup() { + # PR cleanup: remove directories for merged/closed PRs + for d in "$DEPLOY_DIR"/pr-*/; do + [[ -d "$d" ]] || continue + local dir_name pr_number state + dir_name="$(basename "$d")" + pr_number="${dir_name#pr-}" + [[ "$pr_number" =~ ^[0-9]+$ ]] || continue + + state="$(get_pr_state "$pr_number")" + if [[ "$state" == "merged" || "$state" == "closed" ]]; then + echo "Removing $dir_name (PR $state)" + rm -rf "$d" + fi + done + + # Branch cleanup: remove directories for deleted remote branches (rhdh-*, 1.*.x, etc.) + # No pattern list needed — we check if each branch still exists on the remote + for d in "$DEPLOY_DIR"/*/; do + [[ -d "$d" ]] || continue + local dir_name + dir_name="$(basename "$d")" + [[ "$dir_name" == pr-* || "$dir_name" == .* ]] && continue + + if ! git -C "$DEPLOY_DIR" ls-remote --heads origin "$dir_name" 2>/dev/null | grep -q .; then + echo "Removing $dir_name (branch no longer exists on remote)" + rm -rf "$d" + fi + done +} -echo "Staged files:" -git diff --cached --stat +# ── Index generation ───────────────────────────────────────────────────────── -git commit -q -m "$COMMIT_MSG" +release_notes_url() { + local branch="$1" + if [[ "$branch" == "main" ]]; then + echo "${RELEASE_NOTES_BASE}/main/index.html" + elif [[ "$branch" =~ ^release-([0-9]+)\.([0-9]+)$ ]]; then + local major="${BASH_REMATCH[1]}" minor="${BASH_REMATCH[2]}" + if (( major > 1 || minor >= 9 )); then + echo "${RELEASE_NOTES_BASE}/release-${major}-${minor}/index.html" + fi + fi +} + +regenerate_indexes() { + local branch_items="" pr_items="" + + for d in "$DEPLOY_DIR"/*/; do + [[ -d "$d" ]] || continue + local name + name="$(basename "$d")" + [[ "$name" == .* ]] && continue + + if [[ "$name" == pr-* ]]; then + pr_items+="
  • ${name}
  • "$'\n' + else + local entry="
  • ${name}" + local rn_url + rn_url="$(release_notes_url "$name")" + [[ -n "$rn_url" ]] && entry+=" | Release Notes" + branch_items+="${entry}
  • "$'\n' + fi + done + + # Branch deploys regenerate both; PR deploys regenerate pulls.html only + if [[ "$BRANCH_DIR" != pr-* ]]; then + cat > "$DEPLOY_DIR/index.html" <RHDH Documentation - Documentation Branches + +
      +${branch_items}
    + +EOF + fi + + cat > "$DEPLOY_DIR/pulls.html" <RHDH Documentation - PR Previews + +
      +${pr_items}
    + +EOF +} + +# ── Stage, commit, push ───────────────────────────────────────────────────── + +stage_and_commit() { + regenerate_indexes + + # Force-add publish entries and indexes (.gitignore may exclude them) + local to_stage=() + for e in "$PUBLISH_DIR"/*/; do + [[ -d "$e" ]] || continue + local name + name="$(basename "$e")" + [[ "$name" == .* ]] && continue + to_stage+=("$name") + done + [[ -f "$DEPLOY_DIR/index.html" ]] && to_stage+=("index.html") + [[ -f "$DEPLOY_DIR/pulls.html" ]] && to_stage+=("pulls.html") + + git -C "$DEPLOY_DIR" add --force -- "${to_stage[@]}" + git -C "$DEPLOY_DIR" add -A + + if git -C "$DEPLOY_DIR" diff --cached --quiet; then + echo "No changes to deploy" + return 1 + fi + + echo "Staged files:" + git -C "$DEPLOY_DIR" diff --cached --stat || true + git -C "$DEPLOY_DIR" commit -q -m "$COMMIT_MSG" +} + +# On push rejection (concurrent deploy), try rebase first. +# If rebase conflicts (e.g. both touched index.html), reset and rebuild. +try_rebase_and_push() { + local attempt="$1" + if git -C "$DEPLOY_DIR" pull --rebase origin gh-pages 2>/dev/null; then + if git -C "$DEPLOY_DIR" push origin gh-pages 2>/dev/null; then + echo "Deployed successfully (attempt $attempt, after rebase)" + return 0 + fi + echo "Push failed after rebase, will rebuild" + else + echo "Rebase conflict — resetting to remote" + git -C "$DEPLOY_DIR" rebase --abort 2>/dev/null || true + fi + fetch_gh_pages + return 1 +} + +# ── Main ───────────────────────────────────────────────────────────────────── + +fetch_gh_pages +apply_content + +# Cleanup runs once before retries (avoids redundant API calls) +if [[ "$BRANCH_DIR" != pr-* ]]; then + cleanup +fi -# ── Push with pull-before-push retry ── for attempt in $(seq 1 "$MAX_RETRIES"); do - if git push origin gh-pages; then + if [[ "$attempt" -gt 1 ]]; then + apply_content + fi + + if ! stage_and_commit; then + exit 0 + fi + + if git -C "$DEPLOY_DIR" push origin gh-pages 2>/dev/null; then echo "Deployed successfully (attempt $attempt)" exit 0 fi echo "Push rejected (attempt $attempt/$MAX_RETRIES)" - if [[ $attempt -lt $MAX_RETRIES ]]; then - echo "Pulling remote changes before retrying..." - git pull --rebase origin gh-pages + if [[ "$attempt" -lt "$MAX_RETRIES" ]]; then + try_rebase_and_push "$attempt" && exit 0 fi done -echo "ERROR: Deploy failed after $MAX_RETRIES attempts" +echo "Deploy failed after $MAX_RETRIES attempts" >&2 exit 1 diff --git a/build/scripts/lint-scripts.sh b/build/scripts/lint-scripts.sh deleted file mode 100755 index 62e25b91257..00000000000 --- a/build/scripts/lint-scripts.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash -# -# Copyright (c) Red Hat, Inc. -# This program and the accompanying materials are made -# available under the terms of the Eclipse Public License 2.0 -# which is available at https://www.eclipse.org/legal/epl-2.0/ -# -# SPDX-License-Identifier: EPL-2.0 -# -# lint-scripts.sh — Run shellcheck on CQA scripts -# -# Usage: ./build/scripts/lint-scripts.sh - -set -e - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -echo "## Lint: CQA scripts" -echo "" - -errors=0 -for script in "$SCRIPT_DIR"/*.sh; do - if ! shellcheck -S warning -e SC2034,SC2329,SC1091 "$script" 2>/dev/null; then - errors=$((errors + 1)) - fi -done - -echo "" -if [[ $errors -eq 0 ]]; then - echo "All scripts pass shellcheck." -else - echo "$errors script(s) have shellcheck warnings." -fi - -[[ $errors -gt 0 ]] && exit 1 || exit 0