diff --git a/rockydocs.sh b/rockydocs.sh new file mode 100755 index 0000000000..33a8a08cd8 --- /dev/null +++ b/rockydocs.sh @@ -0,0 +1,460 @@ +#!/bin/bash + +set -e + +# Rocky Linux Documentation - Master Contributor Script +# Production-ready script with NEVRA versioning and modular architecture +# +# Author: Wale Soyinka +# Changelog: tools/CHANGELOG.md +# + +VERSION="1.0.0" +RELEASE="13.el10" +AUTHOR="Wale Soyinka" +FULL_VERSION="rockydocs-${VERSION}-${RELEASE}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CONTENT_DIR="$SCRIPT_DIR" +REPO_NAME="$(basename "$CONTENT_DIR")" +INITIAL_PWD="$(pwd)" # Preserve the directory where script was invoked + +# Configuration management +CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/rockydocs" +CONFIG_FILE="$CONFIG_DIR/config" + +# Source function library +source "$(dirname "${BASH_SOURCE[0]}")/tools/rockydocs-functions.sh" + +# Load saved workspace configuration +load_workspace_config + +# Initialize workspace directories +WORKSPACE_BASE_DIR="${SAVED_WORKSPACE_BASE_DIR:-$(dirname "$CONTENT_DIR")/rockydocs-workspaces}" +APP_DIR="$WORKSPACE_BASE_DIR/docs.rockylinux.org" +APP_COMPAT_LINK="$WORKSPACE_BASE_DIR/app" + +# Dependency check (NEW v12) +check_dependencies() { + local missing_deps=() + local required_commands=("git" "python3" "lsof" "pgrep" "jq" "curl") + + for cmd in "${required_commands[@]}"; do + if ! command -v "$cmd" >/dev/null 2>&1; then + missing_deps+=("$cmd") + fi + done + + if [ ${#missing_deps[@]} -gt 0 ]; then + print_error "Missing required dependencies: ${missing_deps[*]}" + print_info "Please install the missing dependencies and try again." + exit 1 + fi +} + +# Consolidated git operations (NEW v12) +update_repositories() { + print_info "Updating all repositories with consolidated git operations..." + + # Update app repository if it exists + if [ -d "$APP_DIR/.git" ]; then + print_info "Updating app repository..." + cd "$APP_DIR" + run_cmd "git fetch origin main" || print_warning "Failed to fetch app repository updates" + run_cmd "git pull origin main" || print_warning "Failed to pull app repository updates" + fi + + # Update cached worktrees if they exist + local worktree_base="$APP_DIR/worktrees" + if [ -d "$worktree_base/main/.git" ]; then + print_info "Updating cached content repositories..." + cd "$worktree_base/main" + run_cmd "git fetch origin rocky-8:rocky-8 rocky-9:rocky-9 main:main" || print_warning "Failed to update cached repositories" + fi + + cd "$CONTENT_DIR" + print_success "Repository updates completed" +} + +# Optimized web root override (NEW v12) +apply_web_root_override_local() { + print_info "Applying optimized local web root override..." + + local original_branch + original_branch=$(git rev-parse --abbrev-ref HEAD) + + # Use low-level git commands to avoid expensive checkouts + if ! git show-ref --verify --quiet refs/heads/gh-pages; then + print_error "No gh-pages branch found for web root override" + return 1 + fi + + # Find the source for web root content (latest symlink or version 10 directory) + local content_source="" + if git ls-tree gh-pages | grep -q "latest"; then + # Check if latest is a symlink and resolve it + local latest_target=$(git show gh-pages:latest 2>/dev/null || echo "") + if [ -n "$latest_target" ] && git ls-tree "gh-pages:$latest_target" >/dev/null 2>&1; then + content_source="$latest_target" + print_info "Found latest symlink pointing to version $latest_target" + elif git ls-tree gh-pages:latest >/dev/null 2>&1; then + content_source="latest" + print_info "Found latest directory" + fi + fi + + # Fallback to version 10 if latest not found + if [ -z "$content_source" ] && git ls-tree gh-pages:10 >/dev/null 2>&1; then + content_source="10" + print_info "Using version 10 as content source" + fi + + if [ -z "$content_source" ]; then + print_error "No suitable content source found (latest or version 10) on gh-pages branch" + return 1 + fi + + print_info "Creating optimized commit for web root override using content from: $content_source" + + # Create a merged tree that preserves versioned directories AND adds root content + local parent_commit=$(git rev-parse gh-pages) + local parent_tree=$(git rev-parse "gh-pages^{tree}") + local source_tree=$(git rev-parse "gh-pages:$content_source") + + # Use a temporary worktree approach to create merged tree + # This preserves existing versioned structure while adding root content + local temp_worktree=".git/temp-merge-$(date +%s)" + run_cmd "git worktree add $temp_worktree gh-pages" + + # In the temporary worktree, copy the latest content to root, preserving versioned dirs + ( + cd "$temp_worktree" + # Remove existing root content (but keep versioned directories and versions.json) + for item in *; do + if [[ "$item" != "8" && "$item" != "9" && "$item" != "10" && "$item" != "latest" && "$item" != "versions.json" && "$item" != ".nojekyll" ]]; then + rm -rf "$item" + fi + done + + # Copy the latest version content to root + if [ -L latest ]; then + local target=$(readlink latest) + cp -r "${target}"/* ./ + elif [ -d "10" ]; then + cp -r 10/* ./ + fi + + # Stage all changes + git add -A + local merged_tree=$(git write-tree) + echo "$merged_tree" > /tmp/merged_tree_sha + ) + + # Clean up the temporary worktree (force removal since it contains our changes) + run_cmd "git worktree remove --force $temp_worktree" + local merged_tree=$(cat /tmp/merged_tree_sha) + rm -f /tmp/merged_tree_sha + + # Create new commit with the merged tree + local new_commit=$(git commit-tree $merged_tree -p $parent_commit -m "feat: Apply optimized local-only web root override from $content_source") + + # Update gh-pages branch to point to new commit + run_cmd "git update-ref refs/heads/gh-pages $new_commit" + + print_success "Optimized web root override applied successfully from $content_source" +} + +# Streamlined static serving (NEW v12) +serve_static() { + + print_info "🔧 STREAMLINED STATIC MODE: Fast static file extraction and serving" + + # Basic validation + if [ ! -d "$APP_DIR" ]; then + print_error "App directory not found: $APP_DIR" + return 1 + fi + + cd "$APP_DIR" + + # Only extract static site if not already done or if deployment is newer + if ! extract_static_site; then + print_error "Failed to extract static site" + return 1 + fi + + # Start static server immediately + print_success "🚀 Starting STREAMLINED static server" + print_info " • Instant startup - no rebuild needed" + print_info " • Production-identical behavior" + print_info " • Access: http://localhost:8000" + + # Check for port conflicts and resolve them + local port=8000 + if ! check_and_resolve_port_conflict $port true; then + print_error "Could not resolve port conflict on port $port" + return 1 + fi + + # Start static server with PID tracking + cd "$APP_DIR/site-static" + python3 -m http.server 8000 & + local server_pid=$! + add_cleanup_resource "pid:$server_pid" + cd - > /dev/null + wait $server_pid +} + +# Docker serving function +serve_docker() { + local static_mode="$1" + + print_info "🐳 DOCKER SERVING MODE: Container-based documentation serving" + + # Validate Docker setup + if [ ! -d "$APP_DIR" ]; then + print_error "App directory not found: $APP_DIR" + print_info "Run setup first: $0 --setup --docker" + return 1 + fi + + # Check if Docker image exists + if ! docker image inspect rockydocs-dev >/dev/null 2>&1; then + print_error "Docker image 'rockydocs-dev' not found" + print_info "Run setup first: $0 --setup --docker" + return 1 + fi + + cd "$APP_DIR" + + # Get container name and manage lifecycle + local container_name=$(get_docker_container_name "serve") + stop_docker_container "$container_name" + + # Find available port + local port=$(find_available_port 8000 8001 8002) + if [ -z "$port" ]; then + print_error "No available ports (8000, 8001, 8002). Kill existing processes." + return 1 + fi + + print_success "🚀 Starting Docker container: $container_name" + print_info " • Port: $port" + print_info " • Access: http://localhost:$port" + print_info " • Static mode: $static_mode" + + # Start container based on mode + if [ "$static_mode" = "true" ]; then + # Static mode: serve pre-built content + print_info " • Mode: Static (production-like)" + docker run -d --name "$container_name" \ + -p "$port:8000" \ + -v "$APP_DIR:/app" \ + -v "$CONTENT_DIR/docs:/app/content" \ + --workdir /app \ + rockydocs-dev \ + python3 -m http.server 8000 -d /app/site-static + else + # Live mode: MkDocs development server with proper environment + print_info " • Mode: Live (development with auto-reload)" + docker run -d --name "$container_name" \ + -p "$port:8000" \ + -v "$APP_DIR:/app" \ + -v "$CONTENT_DIR/docs:/app/content" \ + --workdir /app \ + rockydocs-dev \ + bash -c "source venv/bin/activate && mkdocs serve -a 0.0.0.0:8000" + fi + + # Add container to cleanup + add_cleanup_resource "container:$container_name" + + # Health check with longer timeout for mkdocs build + print_info "Waiting for container to be ready (mkdocs build can take 1-2 minutes)..." + if check_docker_health "$container_name" "$port" 120; then + print_success "✅ Docker container ready!" + print_info "Access your documentation at: http://localhost:$port" + + # Show container logs briefly + print_info "" + print_info "Container logs (last 10 lines):" + docker logs --tail 10 "$container_name" 2>/dev/null || true + print_info "" + print_warning "Press Ctrl+C to stop the container" + + # Follow container logs + docker logs -f "$container_name" + else + print_error "Container failed to start properly" + docker logs "$container_name" 2>/dev/null || true + stop_docker_container "$container_name" + return 1 + fi +} + +# Parse arguments +COMMAND="" +ENVIRONMENT="venv" +BUILD_TYPE="minimal" +STATIC_MODE=false +LIST_MODE=false +CUSTOM_WORKSPACE="" + +while [[ $# -gt 0 ]]; do + case $1 in + --setup) + COMMAND="setup" + shift + ;; + --serve) + COMMAND="serve" + shift + ;; + --serve-dual) + COMMAND="serve-dual" + shift + ;; + --deploy) + COMMAND="deploy" + shift + ;; + --clean) + COMMAND="clean" + shift + ;; + --reset) + COMMAND="reset" + shift + ;; + --status) + COMMAND="status" + shift + ;; + --version) + COMMAND="version" + shift + ;; + --venv|--docker|--podman) + ENVIRONMENT="${1#--}" + shift + ;; + --minimal) + BUILD_TYPE="minimal" + shift + ;; + --full) + BUILD_TYPE="full" + shift + ;; + --static) + STATIC_MODE=true + shift + ;; + --list) + LIST_MODE=true + shift + ;; + --workspace) + if [ -n "$2" ]; then + CUSTOM_WORKSPACE="$2" + # Create directory if it doesn't exist, then get realpath + mkdir -p "$2" + WORKSPACE_BASE_DIR="$(realpath "$2")" + APP_DIR="$WORKSPACE_BASE_DIR/docs.rockylinux.org" + APP_COMPAT_LINK="$WORKSPACE_BASE_DIR/app" + print_info "Using custom workspace: $WORKSPACE_BASE_DIR" + + # Save new workspace configuration + print_info "Saving new workspace configuration..." + save_workspace_config "$WORKSPACE_BASE_DIR" + shift 2 + else + print_error "--workspace requires a path" + exit 1 + fi + ;; + -h|--help) + if [ -n "$COMMAND" ]; then + case "$COMMAND" in + setup) show_setup_help ;; + serve) show_serve_help ;; + serve-dual) show_serve_dual_help ;; + deploy) show_deploy_help ;; + *) show_help ;; + esac + else + show_help + fi + exit 0 + ;; + *) + print_error "Unknown option: $1" + show_help + exit 1 + ;; + esac +done + +# Handle subcommand help +if [[ "$2" == "-h" ]]; then + case "$1" in + --setup) show_setup_help ;; + --serve) show_serve_help ;; + --serve-dual) show_serve_dual_help ;; + --deploy) show_deploy_help ;; + *) show_help ;; + esac + exit 0 +fi + +# Check dependencies before any operation +check_dependencies + +# Execute command +case "$COMMAND" in + setup) + update_repositories + setup_environment "$ENVIRONMENT" "$BUILD_TYPE" + ;; + serve) + if [ "$ENVIRONMENT" = "docker" ]; then + serve_docker "$STATIC_MODE" + elif [ "$STATIC_MODE" = "true" ]; then + serve_static + else + serve_site "" "$STATIC_MODE" + fi + ;; + serve-dual) + serve_dual + ;; + deploy) + if [ "$LIST_MODE" = "true" ]; then + list_versions + elif [ "$ENVIRONMENT" = "docker" ]; then + update_repositories + deploy_docker + else + update_repositories + deploy_site + fi + ;; + clean) + clean_workspace + ;; + reset) + reset_configuration + ;; + status) + show_status + ;; + version) + echo "$FULL_VERSION" + echo "Author: $AUTHOR" + echo "Rocky Linux Documentation Script" + ;; + *) + print_error "No command specified" + show_help + exit 1 + ;; +esac \ No newline at end of file diff --git a/tools/CHANGELOG.md b/tools/CHANGELOG.md new file mode 100644 index 0000000000..38d2f9a4e8 --- /dev/null +++ b/tools/CHANGELOG.md @@ -0,0 +1,8 @@ +# Rocky Linux Documentation Script Changelog + +All notable changes to the `rockydocs.sh` script will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to NEVRA versioning (Name-Version-Release). + +No versioned commits found yet. diff --git a/tools/docker-compose.yml b/tools/docker-compose.yml new file mode 100644 index 0000000000..4b8bff355f --- /dev/null +++ b/tools/docker-compose.yml @@ -0,0 +1,127 @@ +version: '3.8' + +services: + # Rocky Linux Documentation Development Server + rockydocs-dev: + build: + context: . + dockerfile: Dockerfile.dev + container_name: rockydocs-dev-serve + ports: + - "8000:8000" + volumes: + # Mount app directory for live editing + - ./:/app:rw + # Mount content directory for live content editing + - ../content/docs:/app/content:rw + # Persist git data for proper timestamps + - ../.git:/app/.git:ro + working_dir: /app + command: mkdocs serve -a 0.0.0.0:8000 + environment: + - PYTHONPATH=/app + - MKDOCS_CONFIG_FILE=mkdocs.docker.yml + - GIT_AUTHOR_NAME=Rocky Linux Documentation + - GIT_AUTHOR_EMAIL=docs@rockylinux.org + - GIT_COMMITTER_NAME=Rocky Linux Documentation + - GIT_COMMITTER_EMAIL=docs@rockylinux.org + networks: + - rockydocs-network + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # Static file server for production-like testing + rockydocs-static: + build: + context: . + dockerfile: Dockerfile.dev + container_name: rockydocs-static-serve + ports: + - "8001:8000" + volumes: + - ./site-static:/app/static:ro + working_dir: /app + command: python3 -m http.server 8000 -d /app/static + networks: + - rockydocs-network + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + profiles: + - static + + # Build service for deployment testing + rockydocs-build: + build: + context: . + dockerfile: Dockerfile.dev + container_name: rockydocs-build + volumes: + - ./:/app:rw + - ../content/docs:/app/content:rw + - ../.git:/app/.git:ro + working_dir: /app + environment: + - PYTHONPATH=/app + - MKDOCS_CONFIG_FILE=mkdocs.docker.yml + - GIT_AUTHOR_NAME=Rocky Linux Documentation + - GIT_AUTHOR_EMAIL=docs@rockylinux.org + - GIT_COMMITTER_NAME=Rocky Linux Documentation + - GIT_COMMITTER_EMAIL=docs@rockylinux.org + command: /bin/bash -c "echo 'Build service ready. Run: docker-compose exec rockydocs-build mike deploy --push --update-aliases 10 latest'" + networks: + - rockydocs-network + profiles: + - build + + # Database for development analytics (optional) + rockydocs-analytics: + image: postgres:15-alpine + container_name: rockydocs-analytics + environment: + - POSTGRES_DB=rockydocs + - POSTGRES_USER=rockydocs + - POSTGRES_PASSWORD=development + volumes: + - postgres_data:/var/lib/postgresql/data + ports: + - "5432:5432" + networks: + - rockydocs-network + restart: unless-stopped + profiles: + - analytics + + # Redis for caching (optional) + rockydocs-cache: + image: redis:7-alpine + container_name: rockydocs-cache + ports: + - "6379:6379" + volumes: + - redis_data:/data + networks: + - rockydocs-network + restart: unless-stopped + command: redis-server --appendonly yes + profiles: + - cache + +networks: + rockydocs-network: + driver: bridge + +volumes: + postgres_data: + driver: local + redis_data: + driver: local \ No newline at end of file diff --git a/tools/mkdocs-docker.yml b/tools/mkdocs-docker.yml new file mode 100644 index 0000000000..9ee1892111 --- /dev/null +++ b/tools/mkdocs-docker.yml @@ -0,0 +1,110 @@ +--- +site_name: "Documentation" +site_url: "https://docs.rockylinux.org/" +docs_dir: "content" +repo_url: https://github.com/rocky-linux/documentation +repo_name: rocky-linux/documentation +edit_uri: "edit/main/docs/" + +theme: + name: material + custom_dir: theme + icon: + edit: material/pencil + features: + - content.action.edit + - content.code.copy + - navigation.tabs + - navigation.tabs.sticky + - navigation.top + - search.suggest + - search.highlight + - search.share + logo: assets/logo.png + favicon: assets/logo.png + palette: + - scheme: default + media: "(prefers-color-scheme: light)" + toggle: + icon: material/weather-night + name: Switch to dark mode + primary: black + - scheme: slate + media: "(prefers-color-scheme: dark)" + toggle: + icon: material/weather-sunny + name: Switch to light mode + primary: black + +markdown_extensions: + - abbr + - attr_list + - admonition + - toc: + permalink: true + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.highlight + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.keys + - pymdownx.caret + - pymdownx.mark + - pymdownx.tilde + - pymdownx.tabbed: + alternate_style: true + - pymdownx.details + - pymdownx.tasklist + - footnotes + - def_list + - meta + +plugins: + - mike: + version_selector: true + - search + - awesome-pages + - i18n: + docs_structure: suffix + fallback_to_default: true + reconfigure_material: true + reconfigure_search: true + languages: + - build: true + default: true + locale: en + name: English + - build: true + default: false + locale: uk + name: Ukrainian + - git-revision-date-localized: + fallback_to_build_date: true + type: date + - redirects: + redirect_maps: + "guides/add_mirror_manager.md": "guides/mirror_management/add_mirror_manager.md" + - tags + - privacy: + enabled: false + +extra: + version: + provider: mike + default: latest + alias: true + social: + - icon: fontawesome/brands/twitter + link: https://twitter.com/rocky_linux + - icon: fontawesome/brands/github + link: https://github.com/rocky-linux + - icon: fontawesome/brands/gitlab + link: https://git.rockylinux.org + - icon: material/home + link: https://rockylinux.org + +copyright: Copyright © 2026 The Rocky Enterprise Software Foundation \ No newline at end of file diff --git a/tools/requirements-dev.txt b/tools/requirements-dev.txt new file mode 100644 index 0000000000..cc3ed914d8 --- /dev/null +++ b/tools/requirements-dev.txt @@ -0,0 +1,74 @@ +# Rocky Linux Documentation - Development Requirements +# Additional development tools beyond production requirements + +# Core MkDocs and Material theme (production requirements) +mkdocs>=1.5.0 +mkdocs-material>=9.0.0 +mike>=2.0.0 + +# Language and internationalization support +mkdocs-static-i18n>=1.2.0 + +# Content and navigation plugins +mkdocs-awesome-pages-plugin>=2.9.0 +mkdocs-redirects>=1.2.0 + +# Git integration and timestamps +mkdocs-git-revision-date-localized-plugin>=1.2.0 + +# Search and content features +mkdocs-minify-plugin>=0.8.0 +mkdocs-macros-plugin>=1.0.0 + +# Privacy and social features +mkdocs-material[recommended]>=9.0.0 + +# Development and testing tools +pytest>=7.0.0 +pytest-cov>=4.0.0 +black>=23.0.0 +flake8>=6.0.0 +mypy>=1.0.0 + +# Container development support +docker>=6.0.0 +docker-compose>=2.0.0 + +# Git and repository tools +GitPython>=3.1.0 +pre-commit>=3.0.0 + +# JSON and YAML processing +pyyaml>=6.0 +jsonschema>=4.0.0 + +# HTTP and web development +requests>=2.28.0 +httpx>=0.24.0 + +# Build and deployment tools +wheel>=0.40.0 +build>=0.10.0 +twine>=4.0.0 + +# Documentation generation +sphinx>=6.0.0 +sphinx-rtd-theme>=1.2.0 + +# Performance and monitoring +psutil>=5.9.0 +watchdog>=3.0.0 + +# Quality assurance +bandit>=1.7.0 +safety>=2.3.0 +pipreqs>=0.4.0 + +# Development utilities +ipython>=8.0.0 +jupyter>=1.0.0 +notebook>=6.5.0 + +# File and path utilities +pathspec>=0.11.0 +platformdirs>=3.0.0 \ No newline at end of file diff --git a/tools/rockydocs-changelog.sh b/tools/rockydocs-changelog.sh new file mode 100755 index 0000000000..4f641858f4 --- /dev/null +++ b/tools/rockydocs-changelog.sh @@ -0,0 +1,375 @@ +#!/bin/bash + +# rockydocs-changelog.sh - Automated Changelog Management Tool +# Manages tools/CHANGELOG.md using git commit messages and script version parsing +# +# Usage: +# ./tools/rockydocs-changelog.sh [COMMAND] [OPTIONS] +# +# Commands: +# generate - Generate complete changelog from git history +# update - Update changelog with recent commits +# commit - Enhanced git commit with changelog update +# show - Show recent rockydocs commits +# version - Show current script version info +# +# Author: Wale Soyinka +# Contributors: +# Ai-Contributors: Claude (claude-sonnet-4-20250514), Gemini (gemini-2.5-pro) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(dirname "$SCRIPT_DIR")" +CHANGELOG_FILE="$SCRIPT_DIR/CHANGELOG.md" +ROCKYDOCS_SCRIPT="$REPO_ROOT/rockydocs.sh" + +# Source functions if available +if [ -f "$SCRIPT_DIR/rockydocs-functions.sh" ]; then + source "$SCRIPT_DIR/rockydocs-functions.sh" +else + # Minimal print functions if not available + print_info() { echo "ℹ️ $1"; } + print_success() { echo "✅ $1"; } + print_warning() { echo "⚠️ $1"; } + print_error() { echo "❌ $1"; } +fi + +# Extract current version from script +get_current_version() { + if [ ! -f "$ROCKYDOCS_SCRIPT" ]; then + echo "unknown-unknown" + return + fi + + local version=$(grep '^VERSION=' "$ROCKYDOCS_SCRIPT" | cut -d'"' -f2 2>/dev/null || echo "unknown") + local release=$(grep '^RELEASE=' "$ROCKYDOCS_SCRIPT" | cut -d'"' -f2 2>/dev/null || echo "unknown") + echo "$version-$release" +} + +# Parse commit message for changelog info +parse_commit_message() { + local commit_msg="$1" + local commit_hash="$2" + local commit_date="$3" + + # Extract action and description + # Format: "rockydocs: (bump to )" + if [[ "$commit_msg" =~ ^rockydocs:\ ([^[:space:]]+)\ (.+)\ \(bump\ to\ ([^\)]+)\)$ ]]; then + local action="${BASH_REMATCH[1]}" + local description="${BASH_REMATCH[2]}" + local version="${BASH_REMATCH[3]}" + + # Categorize action + local category="Changed" + case "$action" in + add|new) category="Added" ;; + fix|resolve) category="Fixed" ;; + remove|delete) category="Removed" ;; + update|improve|enhance) category="Changed" ;; + security) category="Security" ;; + deprecate) category="Deprecated" ;; + esac + + echo "$version|$category|$description|$commit_hash|$commit_date" + return 0 + fi + + return 1 +} + +# Generate changelog entry for a version +generate_version_entry() { + local version="$1" + local date="$2" + + echo "" + echo "## [$version] - $date" + echo "" + + # Group changes by category + declare -A categories + categories["Added"]="" + categories["Changed"]="" + categories["Fixed"]="" + categories["Security"]="" + categories["Deprecated"]="" + categories["Removed"]="" + + # Parse commits for this version + while IFS='|' read -r commit_version category description commit_hash commit_date; do + if [ "$commit_version" = "$version" ]; then + if [ -n "${categories[$category]}" ]; then + categories[$category]="${categories[$category]}\n- $description" + else + categories[$category]="- $description" + fi + fi + done < <(get_rockydocs_commits | while read -r hash date message; do + if parse_commit_message "$message" "$hash" "$date"; then + true + fi + done) + + # Output categories with content + for category in "Added" "Changed" "Fixed" "Security" "Deprecated" "Removed"; do + if [ -n "${categories[$category]}" ]; then + echo "### $category" + echo -e "${categories[$category]}" + echo "" + fi + done +} + +# Get rockydocs-related commits +get_rockydocs_commits() { + git log --oneline --grep="^rockydocs:" --format="%H %ci %s" -- rockydocs.sh tools/ 2>/dev/null || true +} + +# Show recent commits +show_commits() { + local since="${1:-1 week ago}" + + print_info "Recent rockydocs commits (since: $since)" + echo "" + + git log --oneline --grep="^rockydocs:" --since="$since" --format="%C(yellow)%h%C(reset) %C(green)%ci%C(reset) %s" -- rockydocs.sh tools/ 2>/dev/null || { + print_warning "No rockydocs commits found since $since" + return + } +} + +# Generate complete changelog +generate_changelog() { + local backup_existing="$1" + + if [ -f "$CHANGELOG_FILE" ] && [ "$backup_existing" = "true" ]; then + cp "$CHANGELOG_FILE" "$CHANGELOG_FILE.backup" + print_info "Existing changelog backed up to: $CHANGELOG_FILE.backup" + fi + + print_info "Generating complete changelog from git history..." + + # Create changelog header + cat > "$CHANGELOG_FILE" << 'EOF' +# Rocky Linux Documentation Script Changelog + +All notable changes to the `rockydocs.sh` script will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to NEVRA versioning (Name-Version-Release). + +EOF + + # Get unique versions from commits, sorted by date (newest first) + local versions=$(get_rockydocs_commits | while read -r hash date message; do + if parse_commit_message "$message" "$hash" "$date"; then + echo "$date|$(parse_commit_message "$message" "$hash" "$date" | cut -d'|' -f1)" + fi + done | sort -r | cut -d'|' -f2 | sort -u -r) + + if [ -z "$versions" ]; then + print_warning "No versioned rockydocs commits found" + echo "No versioned commits found yet." >> "$CHANGELOG_FILE" + return + fi + + # Generate entries for each version + while read -r version; do + if [ -n "$version" ]; then + # Get the date of the first commit for this version + local version_date=$(get_rockydocs_commits | while read -r hash date message; do + if parse_commit_message "$message" "$hash" "$date" >/dev/null; then + local commit_version=$(parse_commit_message "$message" "$hash" "$date" | cut -d'|' -f1) + if [ "$commit_version" = "$version" ]; then + echo "$date" + break + fi + fi + done | head -1 | cut -d' ' -f1) + + generate_version_entry "$version" "$version_date" >> "$CHANGELOG_FILE" + fi + done <<< "$versions" + + print_success "Changelog generated: $CHANGELOG_FILE" +} + +# Update changelog with recent commits +update_changelog() { + local since="${1:-1 week ago}" + + if [ ! -f "$CHANGELOG_FILE" ]; then + print_warning "Changelog doesn't exist. Use 'generate' command first." + return 1 + fi + + print_info "Updating changelog with commits since: $since" + + # Get recent commits + local recent_commits=$(git log --oneline --grep="^rockydocs:" --since="$since" --format="%H %ci %s" -- rockydocs.sh tools/ 2>/dev/null || true) + + if [ -z "$recent_commits" ]; then + print_info "No recent rockydocs commits to add" + return + fi + + # For simplicity, append recent commits to a temporary section + local temp_file=$(mktemp) + + # Add header for recent changes + echo "" >> "$temp_file" + echo "## Recent Changes ($(date +%Y-%m-%d))" >> "$temp_file" + echo "" >> "$temp_file" + echo "### Added" >> "$temp_file" + + # Parse and add recent commits + echo "$recent_commits" | while read -r hash date message; do + if parse_commit_message "$message" "$hash" "$date" >/dev/null; then + local description=$(parse_commit_message "$message" "$hash" "$date" | cut -d'|' -f3) + echo "- $description" >> "$temp_file" + fi + done + + # Prepend to existing changelog (after header) + local header_lines=$(grep -n "^# Rocky Linux" "$CHANGELOG_FILE" | head -1 | cut -d':' -f1) + local insert_line=$((header_lines + 5)) # After header block + + head -$insert_line "$CHANGELOG_FILE" > "${temp_file}.full" + cat "$temp_file" >> "${temp_file}.full" + tail -n +$((insert_line + 1)) "$CHANGELOG_FILE" >> "${temp_file}.full" + + mv "${temp_file}.full" "$CHANGELOG_FILE" + rm -f "$temp_file" + + print_success "Changelog updated with recent commits" +} + +# Enhanced git commit with automatic changelog update +enhanced_commit() { + local message="$1" + + if [ -z "$message" ]; then + print_error "Commit message required" + echo "Usage: $0 commit 'rockydocs: action description (bump to version-release)'" + return 1 + fi + + # Validate commit message format + if [[ ! "$message" =~ ^rockydocs:\ [^[:space:]]+\ .+\ \(bump\ to\ [^\)]+\)$ ]]; then + print_error "Invalid commit message format" + echo "Required format: 'rockydocs: (bump to -)'" + echo "Example: 'rockydocs: add --feature-x option (bump to 1.1.0-1.el10)'" + return 1 + fi + + # Extract version from commit message + local version=$(echo "$message" | grep -o '(bump to [^)]*)' | sed 's/(bump to \(.*\))/\1/') + + # Check if version matches script + local current_version=$(get_current_version) + if [ "$version" != "$current_version" ]; then + print_warning "Version mismatch:" + echo " Commit message: $version" + echo " Script version: $current_version" + echo " Update script version first or fix commit message" + fi + + # Stage rockydocs-related files + git add rockydocs.sh tools/ 2>/dev/null || true + + # Commit with message + git commit -m "$message" + + # Update changelog if it exists + if [ -f "$CHANGELOG_FILE" ]; then + print_info "Updating changelog..." + update_changelog "1 hour ago" + fi + + print_success "Commit completed with changelog update" +} + +# Show current version info +show_version() { + local current_version=$(get_current_version) + local latest_commit=$(git log -1 --oneline --grep="^rockydocs:" --format="%h %s" -- rockydocs.sh tools/ 2>/dev/null || echo "No commits found") + + print_info "Version Information" + echo " Current script version: $current_version" + echo " Latest rockydocs commit: $latest_commit" + echo " Changelog file: $CHANGELOG_FILE" + echo " Repository root: $REPO_ROOT" +} + +# Show help +show_help() { + cat << EOF +rockydocs-changelog.sh - Automated Changelog Management Tool + +USAGE: + $0 COMMAND [OPTIONS] + +COMMANDS: + generate [--backup] Generate complete changelog from git history + update [SINCE] Update changelog with recent commits (default: 1 week ago) + commit MESSAGE Enhanced git commit with changelog update + show [SINCE] Show recent rockydocs commits (default: 1 week ago) + version Show current script version information + help Show this help message + +EXAMPLES: + # Generate complete changelog + $0 generate + + # Update with recent commits + $0 update "2 weeks ago" + + # Enhanced commit (auto-updates changelog) + $0 commit "rockydocs: add --feature-x option (bump to 1.1.0-1.el10)" + + # Show recent commits + $0 show "1 month ago" + +COMMIT MESSAGE FORMAT: + rockydocs: (bump to -) + + Actions: add, fix, update, remove, security, deprecate + Example: rockydocs: add --version option (bump to 1.0.0-13.el10) + +NOTES: + - Requires standardized commit message format + - Automatically categorizes changes based on action keywords + - Updates changelog using git history and version parsing + - Integrates with existing rockydocs-functions.sh if available + +EOF +} + +# Main command processing +case "${1:-help}" in + generate) + generate_changelog "${2:-false}" + ;; + update) + update_changelog "${2:-1 week ago}" + ;; + commit) + enhanced_commit "$2" + ;; + show) + show_commits "${2:-1 week ago}" + ;; + version) + show_version + ;; + help|--help|-h) + show_help + ;; + *) + print_error "Unknown command: $1" + echo "" + show_help + exit 1 + ;; +esac \ No newline at end of file diff --git a/tools/rockydocs-functions.sh b/tools/rockydocs-functions.sh new file mode 100755 index 0000000000..75296e194d --- /dev/null +++ b/tools/rockydocs-functions.sh @@ -0,0 +1,1819 @@ +#!/bin/bash + +# Rocky Linux Documentation - Function Library +# Modular function library for rockydocs.sh +# Contains all utility functions, setup logic, and serving modes +# +# Author: Wale Soyinka +# Contributors: +# Ai-Contributors: Claude (claude-sonnet-4-20250514), Gemini (gemini-2.5-pro) + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# Print functions +print_success() { echo -e "${GREEN}✅ $1${NC}"; } +print_info() { echo -e "${BLUE}ℹ️ $1${NC}"; } +print_warning() { echo -e "${YELLOW}⚠️ $1${NC}"; } +print_error() { echo -e "${RED}❌ $1${NC}"; } +print_command() { echo -e "${YELLOW}Running: $1${NC}"; } + +# Execute command with echo +run_cmd() { + print_command "$1" + eval "$1" +} + +# === UTILITY FUNCTIONS === + +# Resource cleanup handler +CLEANUP_RESOURCES=() +cleanup_on_exit() { + local exit_code=$? + if [ ${#CLEANUP_RESOURCES[@]} -gt 0 ]; then + print_warning "Cleaning up resources..." + for resource in "${CLEANUP_RESOURCES[@]}"; do + if [[ "$resource" =~ ^pid: ]]; then + local pid="${resource#pid:}" + kill "$pid" 2>/dev/null || true + print_info "Terminated PID $pid" + elif [[ "$resource" =~ ^dir: ]]; then + local dir="${resource#dir:}" + rm -rf "$dir" 2>/dev/null || true + print_info "Cleaned directory $dir" + elif [[ "$resource" =~ ^file: ]]; then + local file="${resource#file:}" + rm -f "$file" 2>/dev/null || true + print_info "Cleaned file $file" + elif [[ "$resource" =~ ^container: ]]; then + local container="${resource#container:}" + stop_docker_container "$container" + print_info "Stopped container $container" + fi + done + fi + exit $exit_code +} +trap cleanup_on_exit EXIT INT TERM + +# Add resource to cleanup list +add_cleanup_resource() { + CLEANUP_RESOURCES+=("$1") +} + +# Load saved workspace configuration +load_workspace_config() { + if [ -f "$CONFIG_FILE" ]; then + source "$CONFIG_FILE" + fi +} + +# Save workspace configuration +save_workspace_config() { + local workspace_path="$1" + mkdir -p "$CONFIG_DIR" + cat > "$CONFIG_FILE" << EOF +# Rocky Linux Documentation Workspace Configuration +# This file is automatically generated and managed +SAVED_WORKSPACE_BASE_DIR="$workspace_path" +EOF + print_success "Workspace configuration saved to: $CONFIG_FILE" +} + +# Setup mkdocs configuration +setup_mkdocs_config() { + local build_type="$1" + local working_dir="${2:-$PWD}" + + cd "$working_dir" + + if [ "$build_type" = "full" ]; then + run_cmd "cp -f configs/mkdocs.full.yml mkdocs.yml" + print_info "Using full config (all languages)" + else + run_cmd "cp -f configs/mkdocs.minimal.yml mkdocs.yml" + print_info "Using minimal config (English + Ukrainian)" + fi +} + +# Manage content symlink with proper cleanup +manage_content_symlink() { + local action="$1" + local target="${2:-$CONTENT_DIR/docs}" + local link_name="${3:-content}" + + case "$action" in + "create") + run_cmd "rm -rf $link_name" + run_cmd "ln -sf $target $link_name" + add_cleanup_resource "file:$PWD/$link_name" + ;; + "backup") + if [ -e "$link_name" ]; then + run_cmd "rm -f ${link_name}-backup-current" + run_cmd "mv $link_name ${link_name}-backup-current" + fi + ;; + "restore") + run_cmd "rm -f $link_name" + if [ -e "${link_name}-backup-current" ]; then + run_cmd "mv ${link_name}-backup-current $link_name" + fi + ;; + "clean") + run_cmd "rm -f $link_name ${link_name}-backup-current" + ;; + esac +} + +# Activate virtual environment with error handling +activate_venv() { + local venv_path="${1:-venv}" + if [ ! -d "$venv_path" ]; then + print_error "Virtual environment not found: $venv_path" + return 1 + fi + source "$venv_path/bin/activate" || { + print_error "Failed to activate virtual environment" + return 1 + } +} + +# Improved error handling for commands +run_cmd_with_rollback() { + local cmd="$1" + local rollback_cmd="$2" + local error_msg="${3:-Command failed}" + + print_command "$cmd" + if ! eval "$cmd"; then + print_error "$error_msg" + if [ -n "$rollback_cmd" ]; then + print_warning "Attempting rollback: $rollback_cmd" + eval "$rollback_cmd" 2>/dev/null || true + fi + return 1 + fi + return 0 +} + +# Check and resolve port conflicts +check_and_resolve_port_conflict() { + local port="$1" + local force_kill="${2:-false}" + + if lsof -i ":$port" >/dev/null 2>&1; then + local process_info=$(lsof -i ":$port" 2>/dev/null | tail -n 1 | awk '{print $1 " (PID " $2 ")"}') + local pid=$(lsof -ti ":$port" 2>/dev/null) + + print_warning "Port $port is already in use by $process_info" + + if [ "$force_kill" = "true" ]; then + print_info "Automatically terminating conflicting process..." + if kill "$pid" 2>/dev/null; then + print_success "Terminated PID $pid (was using port $port)" + sleep 2 + return 0 + else + print_error "Failed to terminate PID $pid" + return 1 + fi + else + print_info "Options:" + print_info " 1. Terminate the conflicting process automatically" + print_info " 2. Use a different port" + print_info " 3. Cancel and resolve manually" + read -p "Choose (1-3): " choice + + case "$choice" in + 1) + if kill "$pid" 2>/dev/null; then + print_success "Terminated PID $pid (was using port $port)" + sleep 2 + return 0 + else + print_error "Failed to terminate PID $pid" + return 1 + fi + ;; + 2) + print_info "Using alternative port 8002" + return 2 # Signal to use alternative port + ;; + 3) + print_info "Please manually terminate the process: kill $pid" + return 1 + ;; + *) + print_error "Invalid choice" + return 1 + ;; + esac + fi + fi + + return 0 # Port is available +} + +# Kill all documentation-related processes +kill_all_doc_processes() { + print_info "Cleaning up all documentation server processes..." + + # Kill mike serve processes + local mike_pids=$(pgrep -f "mike serve" 2>/dev/null || true) + if [ -n "$mike_pids" ]; then + echo "$mike_pids" | xargs kill 2>/dev/null || true + print_info "Terminated mike serve processes: $mike_pids" + fi + + # Kill Python HTTP servers on documentation ports + for port in 8000 8001 8002; do + if lsof -i ":$port" >/dev/null 2>&1; then + local pid=$(lsof -ti ":$port" 2>/dev/null) + local process_name=$(lsof -i ":$port" 2>/dev/null | tail -n 1 | awk '{print $1}') + if [[ "$process_name" == "Python" ]] || [[ "$process_name" == "python"* ]]; then + kill "$pid" 2>/dev/null || true + print_info "Terminated $process_name (PID $pid) on port $port" + fi + fi + done + + # Kill mkdocs serve processes + local mkdocs_pids=$(pgrep -f "mkdocs serve" 2>/dev/null || true) + if [ -n "$mkdocs_pids" ]; then + echo "$mkdocs_pids" | xargs kill 2>/dev/null || true + print_info "Terminated mkdocs serve processes: $mkdocs_pids" + fi + + sleep 2 + print_success "All documentation server processes cleaned up" +} + +# Detect current Rocky version from git branch +detect_version() { + local branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "main") + case "$branch" in + rocky-8) echo "8" ;; + rocky-9) echo "9" ;; + main) echo "10" ;; + *) echo "10" ;; + esac +} + +# Utility function: Find available port with fallback +find_available_port() { + local port + for port in "$@"; do + if ! lsof -i ":$port" >/dev/null 2>&1; then + echo "$port" + return 0 + fi + done + return 1 +} + +# Docker container utility functions +get_docker_container_name() { + local service_type="$1" # serve, deploy, etc. + echo "rockydocs-${service_type}-${USER}" +} + +stop_docker_container() { + local container_name="$1" + if docker ps -q -f name="$container_name" >/dev/null 2>&1; then + print_info "Stopping existing container: $container_name" + docker stop "$container_name" >/dev/null 2>&1 || true + fi + if docker ps -a -q -f name="$container_name" >/dev/null 2>&1; then + docker rm "$container_name" >/dev/null 2>&1 || true + fi +} + +get_docker_container_status() { + local container_name="$1" + if docker ps -q -f name="$container_name" >/dev/null 2>&1; then + echo "running" + elif docker ps -a -q -f name="$container_name" >/dev/null 2>&1; then + echo "stopped" + else + echo "not_found" + fi +} + +check_docker_health() { + local container_name="$1" + local port="$2" + local max_attempts="${3:-30}" + local attempt=0 + + while [ $attempt -lt $max_attempts ]; do + # First check if container is still running + if ! docker ps --format "table {{.Names}}" | grep -q "^$container_name$"; then + print_error "Container $container_name stopped running" + return 1 + fi + + # Then check if service is responding + if curl -s "http://localhost:$port" >/dev/null 2>&1; then + return 0 + fi + sleep 1 + attempt=$((attempt + 1)) + done + return 1 +} + +# Docker volume management functions +create_docker_volumes() { + local workspace_volume="rockydocs-workspace-${USER}" + local content_volume="rockydocs-content-${USER}" + + # Create workspace volume if it doesn't exist + if ! docker volume inspect "$workspace_volume" >/dev/null 2>&1; then + print_info "Creating Docker volume for workspace: $workspace_volume" + docker volume create "$workspace_volume" >/dev/null 2>&1 + fi + + # Create content volume if it doesn't exist + if ! docker volume inspect "$content_volume" >/dev/null 2>&1; then + print_info "Creating Docker volume for content: $content_volume" + docker volume create "$content_volume" >/dev/null 2>&1 + fi + + echo "$workspace_volume $content_volume" +} + +# Docker volume cleanup function +cleanup_docker_volumes() { + local workspace_volume="rockydocs-workspace-${USER}" + local content_volume="rockydocs-content-${USER}" + + print_warning "This will remove Docker volumes containing your workspace data" + read -p "Remove Docker volumes? (y/N): " confirm + if [[ "$confirm" =~ ^[Yy]$ ]]; then + print_info "Removing Docker volumes..." + docker volume rm "$workspace_volume" >/dev/null 2>&1 || true + docker volume rm "$content_volume" >/dev/null 2>&1 || true + print_success "Docker volumes removed" + else + print_info "Docker volume cleanup cancelled" + fi +} + +# Docker volume status check +show_docker_volume_status() { + local workspace_volume="rockydocs-workspace-${USER}" + local content_volume="rockydocs-content-${USER}" + + echo " • Docker volumes:" + + if docker volume inspect "$workspace_volume" >/dev/null 2>&1; then + local volume_size=$(docker system df -v 2>/dev/null | grep "$workspace_volume" | awk '{print $3}' || echo "unknown") + echo " - $workspace_volume (size: $volume_size)" + else + echo " - $workspace_volume (not created)" + fi + + if docker volume inspect "$content_volume" >/dev/null 2>&1; then + local volume_size=$(docker system df -v 2>/dev/null | grep "$content_volume" | awk '{print $3}' || echo "unknown") + echo " - $content_volume (size: $volume_size)" + else + echo " - $content_volume (not created)" + fi +} + +cleanup_docker_containers() { + local user_containers=$(docker ps -a -q -f name="rockydocs-.*-${USER}" 2>/dev/null || true) + if [ -n "$user_containers" ]; then + print_info "Cleaning up Docker containers..." + echo "$user_containers" | xargs docker rm -f >/dev/null 2>&1 || true + fi +} + +# === HELP FUNCTIONS === + +# Main help +show_help() { + cat << EOF +Rocky Linux Documentation - Master Contributor Script v$FULL_VERSION + +DESCRIPTION: + Recreates the exact look and feel of https://docs.rockylinux.org locally + for contributors to preview their changes before pushing. + +USAGE: + $0 [COMMAND] [OPTIONS] + +COMMANDS: + --setup Setup local development environment + --serve Serve existing deployed versions (fast) + --serve-dual Dual server mode: mike serve + mkdocs live reload + --deploy Build and deploy all versions locally (slow) + --clean Clean workspace and build artifacts + --reset Reset saved configuration + --status Show system status + +GLOBAL OPTIONS: + --minimal Use English + Ukrainian only (default, faster) - setup only + --full Use all languages (slower, complete testing) - setup only + --workspace PATH Custom workspace location + --help, -h Show this help + +EXAMPLES: + ./rockydocs-dev-12.sh --setup --venv # Setup Python venv environment + ./rockydocs-dev-12.sh --deploy # Build and deploy versions locally + ./rockydocs-dev-12.sh --serve # Fast serve (after deploy) + ./rockydocs-dev-12.sh --serve --static # Static production-like serve + ./rockydocs-dev-12.sh --serve-dual # Dual server with live reload + ./rockydocs-dev-12.sh --setup --full # Setup with all languages (config set once) + ./rockydocs-dev-12.sh --deploy # Build using setup's language config + +SUBCOMMAND HELP: + ./rockydocs-dev-12.sh --setup -h # Detailed setup help + ./rockydocs-dev-12.sh --serve -h # Detailed serve help + ./rockydocs-dev-12.sh --serve-dual -h # Dual server help + ./rockydocs-dev-12.sh --deploy -h # Detailed deploy help + +WORKFLOW: + 1. git checkout rocky-9 # Choose your target version + 2. ./rockydocs-dev-12.sh --setup --venv # Setup environment (once) + 3. ./rockydocs-dev-12.sh --deploy # Build/deploy versions to local repo + 4. ./rockydocs-dev-12.sh --serve # Fast serve for editing + 5. git add . && git commit && git push # Push your source changes to origin + +Current Rocky Linux version: $(detect_version) (branch: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")) +EOF +} + +# Setup command help +show_setup_help() { + cat << EOF +Rocky Linux Documentation - Setup Environment + +DESCRIPTION: + Sets up local development environment that recreates the exact look and feel + of https://docs.rockylinux.org on your local machine. + +USAGE: + $0 --setup [ENVIRONMENT] [OPTIONS] + +ENVIRONMENTS: + --venv Use Python virtual environment (recommended) + --docker Use Docker container (containerized environment) + --podman Use Podman container + +OPTIONS: + --minimal Setup for English + Ukrainian only (default, faster) + --full Setup for all languages (complete testing) + --workspace PATH Custom workspace location + +THIS COMMAND RUNS: + - mkdir -p $WORKSPACE_BASE_DIR + - Searches for existing docs.rockylinux.org repository to reuse OR + - git clone https://github.com/rocky-linux/docs.rockylinux.org.git $APP_DIR + - ln -sf docs.rockylinux.org $WORKSPACE_BASE_DIR/app (compatibility symlink) + - cd $APP_DIR + + For --venv (default): + - python3 -m venv venv + - source venv/bin/activate + - pip install -r requirements.txt + + For --docker: + - docker build -t rockydocs-dev . + - Creates Docker volumes for data persistence + - ln -sf $CONTENT_DIR/docs $APP_DIR/docs + - cp configs/mkdocs.minimal.yml ./mkdocs.yml (if --minimal) + - cp configs/mkdocs.full.yml ./mkdocs.yml (if --full) + - Saves workspace configuration to ~/.config/rockydocs/config + +MANUAL ALTERNATIVE: + You can run these commands yourself: + mkdir -p $WORKSPACE_BASE_DIR + git clone https://github.com/rocky-linux/docs.rockylinux.org.git $APP_DIR + cd $APP_DIR && python3 -m venv venv && source venv/bin/activate + pip install -r requirements.txt + +AFTER SETUP: + - Content editing happens in: $CONTENT_DIR/docs/ + - App environment located at: $APP_DIR + - You can cd to app directory and run mkdocs commands directly + +WORKSPACE CONFIGURATION: + First run saves your workspace preference to ~/.config/rockydocs/config + Subsequent runs automatically use your saved workspace location + Use --workspace to change location (gets saved for future use) + Script intelligently finds and reuses existing docs.rockylinux.org repositories + +SIMPLIFIED STRUCTURE: + workspace/ + ├── docs.rockylinux.org/ # App repo (build environment) + ├── app -> docs.rockylinux.org # Compatibility symlink + └── (your content repo is wherever you cloned it) + +EOF +} + +# Serve command help +show_serve_help() { + cat << EOF +Rocky Linux Documentation - Fast Serve (No Rebuild) + +DESCRIPTION: + Starts a FAST local development server using previously deployed versions. + Serves existing mike-deployed content from gh-pages branch without rebuilding. + Use --deploy first to create/update versions if needed. + +FEATURES: + - FAST startup (no rebuilding) + - Multi-version support with version selector + - Serves pre-built content from gh-pages branch + - Root + versioned access (/, /8/, /9/, /10/, /latest/) + - Port fallback (8000 → 8001 → 8002 if ports busy) + +USAGE: + $0 --serve [ENVIRONMENT] [OPTIONS] + +ENVIRONMENTS: + (default) Use Python virtual environment setup + --docker Use Docker container for serving + +OPTIONS: + --static Serve static files (exact production behavior) + +NOTE: --minimal/--full options only apply to --setup command. Deploy uses setup's configuration. + Serve modes use whatever content was already deployed to gh-pages. + +THIS COMMAND RUNS: + For default (venv): + - cd $APP_DIR + - source venv/bin/activate + - mike serve -a localhost:PORT --config-file mkdocs.yml (if not --static) + - python3 -m http.server PORT -d site-static (if --static) + + For --docker: + - docker run -d --name rockydocs-serve-$USER -p PORT:8000 \ + -v $APP_DIR:/app -v $CONTENT_DIR/docs:/app/content \ + rockydocs-dev mkdocs serve -a 0.0.0.0:8000 (if not --static) + - docker run -d --name rockydocs-serve-$USER -p PORT:8000 \ + -v $APP_DIR:/app rockydocs-dev \ + python3 -m http.server 8000 -d /app/site-static (if --static) + +PREREQUISITES: + Run --deploy first to build/deploy versions: + $0 --deploy + +MANUAL ALTERNATIVE: + cd $APP_DIR + source venv/bin/activate + mike serve -a localhost:8000 + +ACCESS: + Local site will be available at: http://localhost:8000 (or 8001/8002) + Live reload enabled - changes appear automatically + +Current version being served: Rocky Linux $(detect_version) + +EOF +} + +# Help function for dual server mode +show_serve_dual_help() { + cat << EOF +Rocky Linux Documentation - Dual Server Mode (Live Reload) + +DESCRIPTION: + Starts DUAL servers for optimal development experience: + - Port 8000: Mike serve (multi-version with version selector) + - Port 8001: MkDocs serve (live reload for current content) + +FEATURES: + - Mike serve (8000): Multi-version support, root + versioned access + - MkDocs serve (8001): Live reload, --watch-theme support + - Simultaneous operation for different use cases + - Smart port conflict resolution + - Proper cleanup on exit + +USAGE: + $0 --serve-dual [OPTIONS] + +OPTIONS: + --minimal Use minimal config (English + Ukrainian) + --venv Use Python virtual environment + --workspace DIR Set workspace directory + +ACCESS PATTERNS: + Multi-version (Port 8000): + • http://localhost:8000/ → Rocky Linux 10 (latest) + • http://localhost:8000/10/ → Rocky Linux 10 (versioned) + • http://localhost:8000/9/ → Rocky Linux 9 + • http://localhost:8000/8/ → Rocky Linux 8 + + Live Reload (Port 8001): + • http://localhost:8001/ → Current content with live reload + • File changes automatically refresh browser + • Theme changes supported with --watch-theme + +NOTES: + - Use port 8000 for testing multi-version functionality + - Use port 8001 for active content editing with live reload + - Both servers run simultaneously in background + - Ctrl+C cleanly stops both servers + +EOF +} + +# Deploy command help +show_deploy_help() { + cat << EOF +Rocky Linux Documentation - Build and Deploy All Versions Locally + +DESCRIPTION: + Builds and deploys ALL Rocky Linux versions (8, 9, 10) locally for testing. + This is a ONE-TIME operation that creates the complete versioned site in your + local build repository. It does NOT push to any remote. + +USAGE: + $0 --deploy [ENVIRONMENT] [OPTIONS] + +ENVIRONMENTS: + (default) Use Python virtual environment setup + --docker Use Docker container for deployment + +OPTIONS: + (none) Uses language configuration set during --setup + +THIS COMMAND RUNS: + For default (venv): + - cd $APP_DIR + - Uses existing mkdocs.yml configuration from setup + - Deletes and recreates the local 'gh-pages' branch + - Setup cached repositories for each version + - source venv/bin/activate + - mike deploy 8 --title 'Rocky Linux 8' (from rocky-8 branch) + - mike deploy 9 --title 'Rocky Linux 9' (from rocky-9 branch) + - mike deploy 10 latest --title 'Rocky Linux 10' (from main branch) + - mike set-default latest + - Applies local-only commit to serve 'latest' content from web root + + For --docker: + - Uses existing mkdocs.yml configuration from setup + - Creates temporary deployment script in container + - docker run --name rockydocs-deploy-$USER \ + -v $APP_DIR:/app -v $CONTENT_DIR/docs:/app/content \ + rockydocs-dev bash /app/temp_deploy.sh + - Extracts site-static artifacts from container + - Applies web root override for production-identical serving + +MANUAL ALTERNATIVE: + cd $APP_DIR + source venv/bin/activate + mike deploy 8 9 10 latest + +OUTPUT: + Deployed versions available for serving with --serve + Use $0 --serve to start fast development server + +Current version being deployed: Rocky Linux $(detect_version) + +EOF +} + +# === SETUP FUNCTIONS === + +# Smart repository discovery and reuse +find_existing_app_repo() { + # Check if there's already a docs.rockylinux.org repo at workspace root level + local potential_paths=( + "$WORKSPACE_BASE_DIR/docs.rockylinux.org" + "$WORKSPACE_BASE_DIR/../*/docs.rockylinux.org" # Check sibling workspace directories + ) + + for path in "${potential_paths[@]}"; do + if [ -d "$path/.git" ]; then + local remote_url=$(cd "$path" && git remote get-url origin 2>/dev/null || echo "") + if [[ "$remote_url" == *"docs.rockylinux.org"* ]]; then + echo "$path" + return 0 + fi + fi + done + return 1 +} + +# Setup function +setup_environment() { + local env_type="$1" + local build_type="$2" + + print_info "Setting up Rocky Linux documentation environment..." + print_info "Target: Recreate https://docs.rockylinux.org locally" + print_info "Version: Rocky Linux $(detect_version)" + print_info "Workspace: $WORKSPACE_BASE_DIR" + + # Save workspace configuration if this is first time or different from saved + if [ ! -f "$CONFIG_FILE" ] || [ "$WORKSPACE_BASE_DIR" != "${SAVED_WORKSPACE_BASE_DIR:-}" ]; then + save_workspace_config "$WORKSPACE_BASE_DIR" + fi + + # Create workspace + run_cmd "mkdir -p $WORKSPACE_BASE_DIR" + + # Smart repository handling + if [ ! -d "$APP_DIR" ]; then + # Check if we can reuse an existing repo + local existing_repo=$(find_existing_app_repo) + if [ $? -eq 0 ] && [ -n "$existing_repo" ]; then + print_info "Found existing docs.rockylinux.org repository at: $existing_repo" + print_info "Creating symlink to reuse existing repository..." + run_cmd "ln -sf $existing_repo $APP_DIR" + else + print_info "Cloning fresh docs.rockylinux.org repository..." + run_cmd "git clone https://github.com/rocky-linux/docs.rockylinux.org.git $APP_DIR" + fi + fi + + # Create compatibility symlink for backward compatibility + if [ ! -e "$APP_COMPAT_LINK" ]; then + run_cmd "ln -sf docs.rockylinux.org $APP_COMPAT_LINK" + print_info "Created compatibility symlink: app -> docs.rockylinux.org" + fi + + # Setup environment based on type + case "$env_type" in + "venv") + setup_venv "$build_type" + ;; + "docker") + setup_docker "$build_type" + ;; + "podman") + setup_podman "$build_type" + ;; + *) + print_error "Unknown environment type: $env_type" + exit 1 + ;; + esac + + print_success "Setup complete!" + print_info "Content editing: $CONTENT_DIR/docs/" + print_info "App environment: $APP_DIR" + print_info "Next: $0 --serve" +} + +# Setup Python virtual environment +setup_venv() { + local build_type="$1" + + print_info "Setting up Python virtual environment..." + + run_cmd "cd $APP_DIR" + + if [ ! -d "venv" ]; then + run_cmd_with_rollback "python3 -m venv venv" "rm -rf venv" "Failed to create virtual environment" + fi + + # Install requirements with error handling + if ! run_cmd_with_rollback "source venv/bin/activate && pip install -r requirements.txt" "" "Failed to install requirements"; then + return 1 + fi + + # Setup content symlink using utility function + manage_content_symlink "create" "$CONTENT_DIR/docs" "content" + + # Setup config using utility function + setup_mkdocs_config "$build_type" "$APP_DIR" +} + +# Setup Docker environment +setup_docker() { + local build_type="$1" + + print_info "Setting up Docker environment..." + + # Create Dockerfile if not exists + if [ ! -f "$APP_DIR/Dockerfile.dev" ]; then + cat > "$APP_DIR/Dockerfile.dev" << 'EOF' +FROM python:3.9-slim + +# Install git (required for mkdocs-git-revision-date-localized-plugin) +RUN apt-get update && apt-get install -y git && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY requirements.txt . +RUN pip install -r requirements.txt + +COPY . . +EXPOSE 8000 + +CMD ["mkdocs", "serve", "-a", "0.0.0.0:8000"] +EOF + fi + + + run_cmd "cd $APP_DIR" + run_cmd "rm -rf content && ln -sf $CONTENT_DIR/docs content" + + if [ "$build_type" = "minimal" ]; then + run_cmd "cp -f configs/mkdocs.minimal.yml mkdocs.yml" + else + run_cmd "cp -f configs/mkdocs.full.yml mkdocs.yml" + fi + + run_cmd "docker build -f Dockerfile.dev -t rockydocs-dev ." + + print_info "Docker image built: rockydocs-dev" + print_info "To run: docker run -p 8000:8000 -v $CONTENT_DIR/docs:/app/content rockydocs-dev" + print_info "Or use: ./rockydocs-dev-12.sh --serve --docker (when implemented)" +} + +# Setup Podman environment +setup_podman() { + local build_type="$1" + + print_info "Setting up Podman environment..." + + # Similar to Docker but using podman commands + setup_docker "$build_type" # Reuse Docker setup logic + + run_cmd "cd $APP_DIR" + run_cmd "podman build -f Dockerfile.dev -t rockydocs-dev ." + + print_info "Podman image built: rockydocs-dev" + print_info "To run: podman run -p 8000:8000 -v $CONTENT_DIR/docs:/app/docs rockydocs-dev" +} + +# === SERVING FUNCTIONS === + +# Setup git worktrees for efficient multi-version deployment +setup_cached_repos() { + local worktree_base="$APP_DIR/worktrees" + local repo_url="https://github.com/rocky-linux/documentation.git" + + print_info "Setting up git worktrees for efficient deployment..." + + # Create worktree base directory if it doesn't exist + if [ ! -d "$worktree_base" ]; then + run_cmd "mkdir -p $worktree_base" + fi + + # Check if we have the main repository + if [ -d "$worktree_base/main/.git" ]; then + print_info "Found existing worktree setup, using cached repositories..." + cd "$worktree_base/main" + cd "$APP_DIR" + else + # Clone main repository if we don't have it + print_info "Creating main repository for git worktrees (one-time setup)..." + cd "$worktree_base" + + # Use --reference optimization if user's content repo exists and has .git + if [ -d "$CONTENT_DIR/.git" ]; then + print_info "Optimizing clone using local repository reference (saves bandwidth)..." + run_cmd "git clone --reference $CONTENT_DIR $repo_url main" + else + run_cmd "git clone $repo_url main" + fi + + cd main + # Fetch all branches we need + run_cmd "git fetch origin rocky-8:rocky-8 rocky-9:rocky-9" 2>/dev/null || true + cd "$APP_DIR" + fi + + # Create worktrees for each version if they don't exist + cd "$worktree_base/main" + + # Rocky Linux 8 worktree + if [ ! -d "$worktree_base/rocky-8" ]; then + print_info "Creating Rocky Linux 8 worktree..." + run_cmd "git worktree add ../rocky-8 rocky-8" + add_cleanup_resource "worktree:$worktree_base/rocky-8" + fi + + # Rocky Linux 9 worktree + if [ ! -d "$worktree_base/rocky-9" ]; then + print_info "Creating Rocky Linux 9 worktree..." + run_cmd "git worktree add ../rocky-9 rocky-9" + add_cleanup_resource "worktree:$worktree_base/rocky-9" + fi + + cd "$APP_DIR" + print_success "Git worktrees ready for efficient deployment" +} + +# Deploy specific version locally +deploy_version_local() { + local version=$1 + local branch=$2 + local alias=$3 + local title=$4 + local worktree_base="$APP_DIR/worktrees" + local current_version=$(detect_version) + + print_info "Deploying Rocky Linux $version (optimized)..." + + if [ "$version" = "$current_version" ]; then + # Deploy current branch content (your edits) + print_info "Using your current content for Rocky Linux $version" + + # Ensure content symlink points to current docs (clean user docs first to avoid loops) + run_cmd "rm -f $CONTENT_DIR/docs/content" # Remove any stale content symlinks from user docs + manage_content_symlink "create" "$CONTENT_DIR/docs" "content" + + if [ -n "$alias" ]; then + run_cmd_with_rollback "source venv/bin/activate && mike deploy $version $alias --title '$title'" "" "Build had warnings for Rocky Linux $version but continuing with deployment" || { + print_warning "Build had warnings for Rocky Linux $version but continuing with deployment" + } + else + run_cmd_with_rollback "source venv/bin/activate && mike deploy $version --title '$title'" "" "Build had warnings for Rocky Linux $version but continuing with deployment" || { + print_warning "Build had warnings for Rocky Linux $version but continuing with deployment" + } + fi + else + # Use git worktrees for efficient deployment + local worktree_path="$worktree_base/$branch" + if [ -d "$worktree_path" ]; then + print_info "Using git worktree for Rocky Linux $version ($branch branch)..." + + # Temporarily backup current docs and use worktree content + manage_content_symlink "backup" "$CONTENT_DIR/docs" "content" + run_cmd "ln -sf $worktree_path/docs content" + + # Deploy this version + if [ -n "$alias" ]; then + run_cmd "source venv/bin/activate && mike deploy $version $alias --title '$title'" + else + run_cmd "source venv/bin/activate && mike deploy $version --title '$title'" + fi + + # Restore current docs + manage_content_symlink "restore" "$CONTENT_DIR/docs" "content" + + print_success "Rocky Linux $version deployed successfully (worktree)" + else + print_warning "No worktree found for $branch branch - skipping Rocky Linux $version" + print_info "Run setup again to create git worktrees" + fi + fi +} + +# Deploy all Rocky Linux versions +deploy_all_versions() { + local current_version="$1" + + # Initialize git repo for mike if not exists + if [ ! -d ".git" ]; then + print_info "Initializing git repository for mike versioning..." + run_cmd "git init" + run_cmd "git config user.name 'Local Dev'" + run_cmd "git config user.email 'dev@local.dev'" + run_cmd "git add mkdocs.yml" + run_cmd "git commit -m 'Initial commit for local development'" + fi + + # Deploy all versions like production + print_info "Building complete multi-version site like production..." + deploy_version_local "8" "rocky-8" "" "Rocky Linux 8" + deploy_version_local "9" "rocky-9" "" "Rocky Linux 9" + deploy_version_local "10" "main" "latest" "Rocky Linux 10" + + # Set default to latest + print_info "Setting latest version as default for root access..." + if ! run_cmd "source venv/bin/activate && mike set-default --allow-empty latest"; then + print_error "Failed to set default version to 'latest'. Aborting." + return 1 + fi +} + +# Live serving mode with mike serve +serve_live() { + + print_info "🚀 FAST SERVE MODE: Using existing deployed versions from gh-pages" + + # Basic validation + if [ ! -d "$APP_DIR" ]; then + print_error "App directory not found: $APP_DIR" + print_info "Run: $0 --setup --venv --minimal first" + return 1 + fi + + if [ ! -d "$APP_DIR/venv" ]; then + print_error "Virtual environment not found. Run: $0 --setup --venv --minimal" + return 1 + fi + + cd "$APP_DIR" + + # Check if mike has been deployed + if ! git show-ref --verify --quiet refs/heads/gh-pages; then + print_error "No mike deployment found on gh-pages branch. Run: $0 --deploy first" + return 1 + fi + + # Mike serve still needs a valid mkdocs.yml, but will serve from gh-pages branch + # We need to ensure there's a valid config file and docs_dir exists + if [ ! -f "mkdocs.yml" ]; then + if [ -f "configs/mkdocs.minimal.yml" ]; then + run_cmd "cp configs/mkdocs.minimal.yml mkdocs.yml" + print_info "Created minimal mkdocs.yml for mike serve" + else + print_error "No mkdocs config files found" + return 1 + fi + fi + + # Ensure the docs_dir referenced in mkdocs.yml exists (mike serve requires it) + local docs_dir=$(grep "^docs_dir:" mkdocs.yml | cut -d'"' -f2 2>/dev/null || echo "content") + if [ ! -d "$docs_dir" ]; then + run_cmd "mkdir -p $docs_dir" + print_info "Created placeholder $docs_dir directory for mike serve" + fi + + # Find available port with fallback + local port=$(find_available_port 8000 8001 8002) + if [ -z "$port" ]; then + print_error "No available ports (8000, 8001, 8002). Kill existing processes or use different ports." + return 1 + fi + + print_success "🚀 Starting FAST server on port $port" + print_info " • Serving pre-built content from gh-pages branch" + print_info " • Version selector available" + print_info " • No rebuilding - content already deployed" + print_info " • Port fallback: 8000 → 8001 → 8002" + print_info "" + print_info "Access: http://localhost:$port" + print_info "" + + # Mike serve reads directly from gh-pages branch, no content symlink needed + print_command "source venv/bin/activate && mike serve -a localhost:$port" + bash -c "cd $APP_DIR && source venv/bin/activate && mike serve -a localhost:$port" +} + +# Extract static site like Vercel +extract_static_site() { + if ! git show-ref --verify --quiet refs/heads/gh-pages; then + print_error "No gh-pages branch found - mike deployment may have failed" + return 1 + fi + + print_info "Extracting static site from gh-pages branch (like Vercel)..." + + # Clean and create site-static directory + run_cmd "rm -rf site-static" + run_cmd "mkdir -p site-static" + add_cleanup_resource "dir:$PWD/site-static" + + # Extract static files from gh-pages using checkout instead of archive to preserve symlinks + print_info "Using git checkout to preserve symlinks..." + local current_branch=$(git rev-parse --abbrev-ref HEAD) + + # Stash any uncommitted changes + git stash push -m "Temporary stash for static extraction" >/dev/null 2>&1 || true + + # Checkout gh-pages branch temporarily + if ! run_cmd "git checkout gh-pages"; then + print_error "Failed to checkout gh-pages branch" + return 1 + fi + + # Copy all files to site-static directory + run_cmd "cp -r * site-static/ 2>/dev/null || true" + run_cmd "cp -r .[^.]* site-static/ 2>/dev/null || true" # Copy hidden files + + # Return to original branch + run_cmd "git checkout $current_branch" + + # Restore any stashed changes + git stash pop >/dev/null 2>&1 || true + + if [ ! -d "site-static" ] || [ "$(ls -A site-static 2>/dev/null | wc -l)" -eq 0 ]; then + print_error "Static extraction produced no content" + return 1 + fi + + print_success "Static site extracted successfully from gh-pages with symlinks preserved!" + + # Apply Vercel-style root deployment + apply_root_deployment + return $? +} + +# Apply root deployment for backward compatibility +apply_root_deployment() { + print_info "Applying Vercel-style root deployment..." + + # First check if root deployment is already applied + if [ -f "site-static/index.html" ] && [ ! -d "site-static/latest" ] && [ ! -d "site-static/10" ]; then + print_info "Root deployment already applied - content is already at web root" + print_success "Root index.html exists (content already deployed to root)" + return 0 + fi + + # Look for latest content to copy to root (handle symlinks) + local root_source="" + + # Check if latest exists (could be directory or symlink) + if [ -e "site-static/latest" ]; then + if [ -L "site-static/latest" ]; then + # Latest is a symlink, resolve it + local latest_target=$(readlink "site-static/latest" 2>/dev/null || echo "") + if [ -n "$latest_target" ] && [ -d "site-static/$latest_target" ]; then + root_source="site-static/$latest_target" + print_info "Found latest symlink pointing to $latest_target, using as root source..." + fi + elif [ -d "site-static/latest" ]; then + # Latest is a directory + root_source="site-static/latest" + print_info "Found latest directory, using as root source..." + fi + fi + + # Fallback to version 10 if latest not resolved + if [ -z "$root_source" ] && [ -d "site-static/10" ]; then + root_source="site-static/10" + print_info "Using version 10 directory as root source..." + fi + + if [ -z "$root_source" ]; then + print_warning "No suitable version directory found for root deployment" + return 1 + fi + + # Backup versions.json if it exists + if [ -f "site-static/versions.json" ]; then + run_cmd "cp site-static/versions.json site-static/versions.json.backup" + print_info "Backed up versions.json" + fi + + # Copy version content to root + print_info "Copying latest content to root for production behavior..." + run_cmd "cp -r $root_source/* site-static/ 2>/dev/null || true" + + # Restore versions.json to maintain version selector functionality + if [ -f "site-static/versions.json.backup" ]; then + run_cmd "cp site-static/versions.json.backup site-static/versions.json" + run_cmd "rm site-static/versions.json.backup" + print_info "Restored versions.json for version selector" + fi + + # Verify root content + if [ -f "site-static/index.html" ]; then + print_success "Root index.html exists (latest content deployed)" + + # Check what URLs will work + print_info "Verifying URL access patterns..." + if [ -d "site-static/8" ]; then print_info " ✅ /8/ → Rocky Linux 8"; fi + if [ -d "site-static/9" ]; then print_info " ✅ /9/ → Rocky Linux 9"; fi + if [ -d "site-static/10" ]; then print_info " ✅ /10/ → Rocky Linux 10"; fi + if [ -d "site-static/latest" ]; then print_info " ✅ /latest/ → Rocky Linux 10"; fi + print_success " ✅ / → Rocky Linux 10 (DIRECT ACCESS)" + + return 0 + else + print_error "Root index.html missing after root deployment!" + return 1 + fi +} + +# Main serve function - routes to appropriate serving mode +serve_site() { + local build_type="$1" # Ignored for serve modes - they use pre-built content + local static_mode="$2" + + # Route to appropriate serving mode + if [ "$static_mode" = "true" ]; then + serve_static + else + serve_live + fi +} + +# Dual server function +serve_dual() { + + print_info "🚀 DUAL SERVER MODE: Starting mike serve + mkdocs live reload" + print_info "Setting up dual server environment..." + + # Validate basic requirements + if [ ! -d "$APP_DIR" ]; then + print_error "App directory not found: $APP_DIR" + print_info "Run setup first: $0 --setup --minimal" + return 1 + fi + + run_cmd "cd $APP_DIR" + + # Validate environment (same as serve_live) + if [ ! -d "$APP_DIR/venv" ]; then + print_error "Virtual environment not found. Run: $0 --setup --venv --minimal" + return 1 + fi + + # Check if mike has been deployed - dual server requires mike versioning + if ! git show-ref --verify --quiet refs/heads/gh-pages; then + print_error "No mike deployment found on 'gh-pages' branch. Dual server requires mike versioning." + print_info "Run: $0 --deploy first to create the local mike deployment." + return 1 + fi + + # NOTE: No mkdocs config needed - serve_dual serves pre-built content from gh-pages + + # Setup content symlink for current content + manage_content_symlink "create" "$CONTENT_DIR/docs" "content" + + # Activate virtual environment if using it + if [ ! -f "venv/bin/activate" ]; then + print_error "Virtual environment not found. Run: $0 --setup --venv first" + return 1 + fi + print_info "Activating virtual environment..." + source venv/bin/activate + + # Find available ports with conflict resolution + local mike_port=$(find_available_port 8000 8010 8020) + local mkdocs_port=$(find_available_port 8001 8011 8021) + + if [ -z "$mike_port" ] || [ -z "$mkdocs_port" ]; then + print_error "Could not find available ports for dual server mode" + return 1 + fi + + print_success "🔧 DUAL SERVER CONFIGURATION:" + print_info " • Mike serve (multi-version): http://localhost:$mike_port" + print_info " • MkDocs serve (live reload): http://localhost:$mkdocs_port" + print_info "" + print_info "🎯 USE CASES:" + print_info " • Port $mike_port: Test multi-version functionality, version selector, and web root." + print_info " • Port $mkdocs_port: Active content editing with live reload." + print_info "" + + # Start mike serve in background + print_info "Starting mike serve in background (port $mike_port)..." + mike serve -a localhost:$mike_port --config-file mkdocs.yml > /tmp/mike-serve-$$.log 2>&1 & + local mike_pid=$! + add_cleanup_resource "pid:$mike_pid" + add_cleanup_resource "file:/tmp/mike-serve-$$.log" + + # Give mike serve time to start + sleep 2 + + # Check if mike serve started successfully + if ! kill -0 $mike_pid 2>/dev/null; then + print_error "Failed to start mike serve. Check log: /tmp/mike-serve-$$.log" + return 1 + fi + + print_success "✅ Mike serve started successfully (PID: $mike_pid)" + + # Start mkdocs serve in foreground with live reload + print_info "Starting mkdocs serve with live reload (port $mkdocs_port)..." + print_info "📝 LIVE RELOAD FEATURES:" + print_info " • File changes auto-refresh browser" + print_info " • Theme changes supported" + print_info " • Current content only (not versioned)" + print_info "" + print_warning "Press Ctrl+C to stop both servers cleanly" + print_info "" + + # Set up signal handler for clean shutdown + trap 'print_warning "Stopping dual servers..."; kill $mike_pid 2>/dev/null; exit 0' INT TERM + + # Start mkdocs serve in foreground + mkdocs serve -a localhost:$mkdocs_port --watch-theme --config-file mkdocs.yml +} + +# Deploy site function +deploy_site() { + local current_version=$(detect_version) + + print_info "🚀 DEPLOY MODE: Building and deploying ALL Rocky Linux versions locally" + print_info "This creates the complete versioned site that --serve uses" + print_info "Current branch: Rocky Linux $current_version (your edits will be in this version)" + + if [ ! -d "$APP_DIR/venv" ]; then + print_error "Environment not setup. Run: $0 --setup --venv first" + return 1 + fi + + run_cmd "cd $APP_DIR" + + # Delete existing gh-pages branch for a clean LOCAL deployment + if git show-ref --verify --quiet refs/heads/gh-pages; then + print_info "Deleting existing local gh-pages branch for a clean deployment..." + run_cmd "git branch -D gh-pages" + fi + + # Use existing mkdocs.yml config set during setup - no need to reconfigure + + # Setup repository caching and deploy all versions + print_info "Setting up cached repositories and deploying versions..." + setup_cached_repos + deploy_all_versions "$current_version" + + # Apply the web root override directly to the local gh-pages branch + apply_web_root_override_local + + print_success "🎉 Local deploy complete! All versions are ready for '--serve'." + print_info "Available versions: Rocky Linux 8, 9, 10 (latest)" + print_info "" + print_info "Next steps:" + print_info " • Use: $0 --serve --minimal # Fast serve (no rebuild)" + print_info " • Use: $0 --serve --static # Static production-like serve" + print_info "" + print_info "Deployed versions ready for fast serving with mike!" +} + +# Docker deployment function +deploy_docker() { + local current_version=$(detect_version) + + print_info "🐳 DOCKER DEPLOY MODE: Container-based multi-version deployment" + print_info "Building and deploying ALL Rocky Linux versions using Docker containers" + print_info "Current branch: Rocky Linux $current_version (your edits will be in this version)" + + # Validate Docker setup + if [ ! -d "$APP_DIR" ]; then + print_error "App directory not found: $APP_DIR" + print_info "Run setup first: $0 --setup --docker" + return 1 + fi + + # Check if Docker image exists + if ! docker image inspect rockydocs-dev >/dev/null 2>&1; then + print_error "Docker image 'rockydocs-dev' not found" + print_info "Run setup first: $0 --setup --docker" + return 1 + fi + + cd "$APP_DIR" + + # Use existing mkdocs.yml config set during setup - no need to reconfigure + print_info "Using existing mkdocs configuration from setup..." + + # Get container name for deployment + local container_name=$(get_docker_container_name "deploy") + stop_docker_container "$container_name" + + print_info "Setting up cached repositories..." + setup_cached_repos + + print_success "🚀 Starting Docker deployment container..." + + # Create deployment script inside container + local deploy_script="/tmp/deploy_all_versions.sh" + cat > "$deploy_script" << 'EOF' +#!/bin/bash +set -e + +current_version="$1" +echo "🐳 Container deployment starting..." +echo "Current version: $current_version" + +# Configure git user for timestamp preservation +git config --global user.name "Rocky Linux Documentation Bot" +git config --global user.email "noreply@rockylinux.org" + +# No virtual environment needed in Docker - packages installed globally + +# Deploy Rocky Linux 8 +if [ -d "worktrees/rocky-8" ]; then + echo "📦 Deploying Rocky Linux 8..." + rm -rf content && ln -sf worktrees/rocky-8/docs content + mike deploy 8 --config-file mkdocs.yml || echo "Warning: Rocky 8 deployment had issues" +fi + +# Deploy Rocky Linux 9 +if [ -d "worktrees/rocky-9" ]; then + echo "📦 Deploying Rocky Linux 9..." + rm -rf content && ln -sf worktrees/rocky-9/docs content + mike deploy 9 --config-file mkdocs.yml || echo "Warning: Rocky 9 deployment had issues" +fi + +# Deploy Rocky Linux 10 (current content with git history) +echo "📦 Deploying Rocky Linux 10 (current)..." +if [ -d "worktrees/main" ]; then + echo "Using worktree main for Rocky Linux 10 (with git history)..." + rm -rf content && ln -sf worktrees/main/docs content +else + echo "Fallback: Using current content directory..." + rm -rf content && ln -sf /app/content-current content +fi +mike deploy 10 latest --config-file mkdocs.yml +mike set-default latest --config-file mkdocs.yml + +echo "✅ Container deployment completed successfully!" +mike list --config-file mkdocs.yml || true +EOF + chmod +x "$deploy_script" + + # Run deployment in container + print_info "Running deployment inside Docker container..." + docker run --rm --name "$container_name" \ + -v "$APP_DIR:/app" \ + -v "$CONTENT_DIR/docs:/app/content-current" \ + -v "$deploy_script:$deploy_script" \ + --workdir /app \ + rockydocs-dev \ + bash "$deploy_script" "$current_version" + + # Clean up temporary script + rm -f "$deploy_script" + + # Apply web root override (same as venv version) + print_info "Applying web root override for production-like behavior..." + apply_web_root_override_local + + print_success "🎉 Docker deployment complete! All versions are ready for '--serve'." + print_info "Available versions: Rocky Linux 8, 9, 10 (latest)" + print_info "" + print_info "Next steps:" + print_info " • Use: $0 --serve --docker # Docker-based serving" + print_info " • Use: $0 --serve --docker --static # Static production-like serve" + print_info "" + print_info "Deployed versions ready for Docker serving!" +} + +# === STATUS FUNCTIONS === + +show_system_info() { + print_success "System Information:" + echo " • Script version: $FULL_VERSION" + echo " • Current directory: $CONTENT_DIR" + echo " • Rocky Linux version: $(detect_version) (branch: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown"))" + echo " • Platform: $(uname -s) $(uname -m)" +} + +# Merged environment and configuration info +show_config_env_info() { + print_success "Configuration & Environment:" + echo " • Config file: $CONFIG_FILE" + if [ -f "$CONFIG_FILE" ]; then + echo " • Saved workspace: $SAVED_WORKSPACE_BASE_DIR" + fi + echo " • Current workspace (WORKSPACE_BASE_DIR): $WORKSPACE_BASE_DIR" + echo " • Content directory (CONTENT_DIR): $CONTENT_DIR" + echo " • App directory (APP_DIR): $APP_DIR" +} + + +show_repo_status() { + local repo_type="$1" + local repo_dir="$2" + + if [ -d "$repo_dir/.git" ]; then + cd "$repo_dir" + local remote=$(git remote get-url origin 2>/dev/null || echo "unknown") + local commit=$(git rev-parse HEAD 2>/dev/null || echo "unknown") + local upstream_commit="unknown" + if git ls-remote origin HEAD >/dev/null 2>&1; then + upstream_commit=$(git ls-remote origin HEAD | cut -f1 2>/dev/null || echo "unknown") + fi + echo " • Origin: $remote" + echo " • Local commit: ${commit:0:8}" + echo " • Remote commit: ${upstream_commit:0:8}" + + # Show other remotes if they exist + local other_remotes=$(git remote | grep -v "^origin$" 2>/dev/null || true) + if [ -n "$other_remotes" ]; then + for remote_name in $other_remotes; do + local remote_url=$(git remote get-url "$remote_name" 2>/dev/null || echo "unknown") + echo " • $remote_name: $remote_url" + done + fi + + if [ "$repo_type" = "content" ]; then + local git_status=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ') + if [ "$git_status" -gt 0 ]; then + print_warning " • $git_status uncommitted changes" + else + echo " • Working tree clean" + fi + fi + cd "$CONTENT_DIR" + else + echo " • Not a git repository" + fi +} + +show_process_status() { + print_success "Active Processes:" + local mike_pids=$(pgrep -f "mike serve" 2>/dev/null || true) + local python_pids=$(pgrep -f "python.*http\.server" 2>/dev/null || true) + local mkdocs_pids=$(pgrep -f "mkdocs serve" 2>/dev/null || true) + + if [ -n "$mike_pids" ]; then + echo " • Mike serve PIDs: $mike_pids" + fi + if [ -n "$python_pids" ]; then + echo " • Python HTTP server PIDs: $python_pids" + fi + if [ -n "$mkdocs_pids" ]; then + echo " • MkDocs serve PIDs: $mkdocs_pids" + fi + if [ -z "$mike_pids" ] && [ -z "$python_pids" ] && [ -z "$mkdocs_pids" ]; then + echo " • No active documentation servers found" + fi +} + +show_port_status() { + print_success "Port Usage:" + for port in 8000 8001 8002; do + if lsof -i :$port >/dev/null 2>&1; then + local process_info=$(lsof -i :$port 2>/dev/null | tail -n 1 | awk '{print $1 " (PID " $2 ")"}') + echo " • Port $port: In use by $process_info" + else + echo " • Port $port: Available" + fi + done +} + +# Enhanced build artifacts with disk usage +show_build_artifacts() { + print_success "Build Artifacts & Disk Usage:" + + # Helper function for fast disk usage + get_disk_usage_fast() { + local path="$1" + if [ -d "$path" ]; then + du -sh "$path" 2>/dev/null | cut -f1 + else + echo "0B" + fi + } + + # Build artifacts status + if [ -d "$APP_DIR/site" ]; then + local site_size=$(get_disk_usage_fast "$APP_DIR/site") + echo " • Site directory: Found ($site_size)" + else + echo " • Site directory: Not found" + fi + + if [ -d "$APP_DIR/site-static" ]; then + local static_size=$(get_disk_usage_fast "$APP_DIR/site-static") + echo " • Static site directory: Found ($static_size)" + else + echo " • Static site directory: Not found" + fi + + if [ -f "$APP_DIR/versions.json" ]; then + echo " • Versions file: Found" + else + echo " • Versions file: Not found" + fi + + # Simple disk usage totals + local workspace_size=$(get_disk_usage_fast "$WORKSPACE_BASE_DIR") + local worktree_size=$(get_disk_usage_fast "$APP_DIR/worktrees") + echo " • Total workspace: $workspace_size" + echo " • Cached worktrees: $worktree_size" +} + +# List deployed versions (mimics mike list behavior) +list_versions() { + print_info "Listing deployed versions..." + + # Check if app directory exists + if [ ! -d "$APP_DIR" ]; then + print_error "App directory not found: $APP_DIR" + print_info "Run --setup first to create the build environment" + return 1 + fi + + cd "$APP_DIR" + + # Check if gh-pages branch exists (indicates deployment has happened) + if ! git show-ref --verify --quiet refs/heads/gh-pages; then + print_warning "No versions deployed yet" + print_info "Run --deploy first to create version deployments" + return 0 + fi + + # Extract version information from gh-pages branch + local current_branch=$(git rev-parse --abbrev-ref HEAD) + + # Check if versions.json exists on gh-pages branch + if git ls-tree gh-pages | grep -q "versions.json"; then + print_success "Deployed versions:" + # Extract version info directly from gh-pages branch without switching + git show gh-pages:versions.json | jq -r '.[] | "\(.version) [\(.title)] \(.aliases // [] | join(", ") | if . == "" then "" else "(" + . + ")" end)"' 2>/dev/null || { + # Fallback if jq fails - simple extraction + git show gh-pages:versions.json 2>/dev/null | grep -E '"version"|"title"|"aliases"' | paste - - - | sed 's/"version": "\([^"]*\)".*"title": "\([^"]*\)".*/\1 [\2]/' + } + else + print_warning "No versions.json found on gh-pages branch" + print_info "Checking for version directories..." + # List version directories directly from gh-pages + git ls-tree gh-pages | grep "^040000 tree" | awk '{print $4}' | grep -E "^[0-9]+$|^latest$" | sort -V || print_warning "No version directories found" + fi +} + +# Docker status display function +show_docker_status() { + print_success "Docker Environment:" + + # Check if Docker is available + if ! command -v docker >/dev/null 2>&1; then + echo " • Docker: Not available" + return + fi + + # Check Docker daemon status + if ! docker info >/dev/null 2>&1; then + echo " • Docker: Available but daemon not running" + return + fi + + echo " • Docker: Available and running" + + # Check for rockydocs-dev image (faster check) + if docker images -q rockydocs-dev 2>/dev/null | grep -q .; then + echo " • Docker image: rockydocs-dev (available)" + else + echo " • Docker image: rockydocs-dev (not found)" + fi + + # Check for active containers (faster) + local serve_container=$(get_docker_container_name "serve") + local deploy_container=$(get_docker_container_name "deploy") + + local serve_running=$(docker ps -q -f name="$serve_container" 2>/dev/null) + local deploy_running=$(docker ps -q -f name="$deploy_container" 2>/dev/null) + + if [ -n "$serve_running" ] || [ -n "$deploy_running" ]; then + echo " • Active containers: Yes" + if [ -n "$serve_running" ]; then + local port=$(docker port "$serve_container" 8000 2>/dev/null | cut -d: -f2) + echo " - Serving on port ${port:-unknown}" + fi + if [ -n "$deploy_running" ]; then + echo " - Deploy container running" + fi + else + echo " • Active containers: None" + fi +} + + +# Main status function - optimized and streamlined +show_status() { + print_info "Rocky Linux Documentation System Status" + echo "" + + show_system_info + echo "" + + show_config_env_info + echo "" + + print_success "Content Repository Status:" + show_repo_status "content" "$CONTENT_DIR" + echo "" + + print_success "App Repository Status:" + if [ -d "$APP_DIR" ]; then + show_repo_status "app" "$APP_DIR" + else + echo " • App directory not found: $APP_DIR" + fi + echo "" + + show_process_status + echo "" + + show_port_status + echo "" + + show_docker_status + echo "" + + show_build_artifacts +} + +# === CLEANUP FUNCTIONS === + +# Reset configuration function +reset_configuration() { + print_info "Resetting saved configuration..." + + if [ -f "$CONFIG_FILE" ]; then + print_warning "This will remove saved workspace configuration: $CONFIG_FILE" + read -p "Continue? (y/N): " confirm + if [[ "$confirm" =~ ^[Yy]$ ]]; then + run_cmd "rm -f $CONFIG_FILE" + print_success "Configuration reset - next setup will use default workspace" + else + print_info "Reset operation cancelled" + fi + else + print_info "No saved configuration found to reset" + fi +} + +# Clean function with enhanced resource management +clean_workspace() { + print_info "Cleaning workspace and build artifacts..." + + if [ -d "$WORKSPACE_BASE_DIR" ]; then + print_warning "This will remove the entire workspace directory: $WORKSPACE_BASE_DIR" + print_warning "This includes the app repository and all build artifacts" + print_info "Repository cache will be preserved for faster subsequent runs" + read -p "Continue? (y/N): " confirm + if [[ "$confirm" =~ ^[Yy]$ ]]; then + # Clean up git worktrees first to avoid permission issues + if [ -d "$APP_DIR" ]; then + print_info "Cleaning up git worktrees..." + cd "$APP_DIR" 2>/dev/null || true + for worktree in rocky-8 rocky-9 main; do + if [ -d "worktrees/$worktree" ]; then + git worktree remove --force "worktrees/$worktree" 2>/dev/null || true + fi + done + cd - >/dev/null 2>&1 || true + fi + + # Preserve cached repos for performance + if [ -d "$APP_DIR/cached-repos" ]; then + print_info "Preserving repository cache for faster startup..." + run_cmd_with_rollback "mv $APP_DIR/cached-repos /tmp/rockydocs-cache-backup" "" "Failed to backup cache" + fi + + # Fix permissions before removal + if [ -d "$WORKSPACE_BASE_DIR" ]; then + chmod -R 755 "$WORKSPACE_BASE_DIR" 2>/dev/null || true + fi + + run_cmd "rm -rf $WORKSPACE_BASE_DIR" + print_success "Workspace cleaned: $WORKSPACE_BASE_DIR" + + # Restore cache if it existed + if [ -d "/tmp/rockydocs-cache-backup" ]; then + run_cmd "mkdir -p $APP_DIR" + run_cmd "mv /tmp/rockydocs-cache-backup $APP_DIR/cached-repos" + print_info "Repository cache restored for next run" + fi + else + print_info "Clean operation cancelled" + fi + else + print_info "No workspace directory found to clean" + fi + + # Also clean any local build artifacts in current directory + if [ -d "site" ]; then + run_cmd "rm -rf site" + print_success "Local build artifacts cleaned" + fi + + # Option to clean cache completely + if [ -d "$APP_DIR/cached-repos" ]; then + print_info "" + print_warning "Repository cache found: $APP_DIR/cached-repos" + print_info "This cache makes subsequent runs much faster" + read -p "Also remove repository cache? (y/N): " clean_cache + if [[ "$clean_cache" =~ ^[Yy]$ ]]; then + run_cmd "rm -rf $APP_DIR/cached-repos" + # Also remove the empty parent directory to avoid setup issues + if [ -d "$APP_DIR" ] && [ "$(ls -A $APP_DIR 2>/dev/null | wc -l)" -eq 0 ]; then + run_cmd "rm -rf $APP_DIR" + print_info "Removed empty app directory" + fi + print_success "Repository cache cleaned (next run will be slower)" + else + print_info "Repository cache preserved for faster startup" + fi + fi + + # Option to clean Docker volumes + if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then + local workspace_volume="rockydocs-workspace-${USER}" + local content_volume="rockydocs-content-${USER}" + + if docker volume inspect "$workspace_volume" >/dev/null 2>&1 || docker volume inspect "$content_volume" >/dev/null 2>&1; then + print_info "" + print_warning "Docker volumes found for Rocky Linux Documentation" + cleanup_docker_volumes + fi + fi + + # Option to clean saved configuration + if [ -f "$CONFIG_FILE" ]; then + print_info "" + print_warning "Saved configuration found: $CONFIG_FILE" + print_info "This contains your workspace preferences" + read -p "Also remove saved configuration? (y/N): " clean_config + if [[ "$clean_config" =~ ^[Yy]$ ]]; then + run_cmd "rm -f $CONFIG_FILE" + print_success "Saved configuration cleared - next setup will use defaults" + else + print_info "Saved configuration preserved" + fi + fi +} \ No newline at end of file diff --git a/tools/test_rockydocs.sh b/tools/test_rockydocs.sh new file mode 100755 index 0000000000..e3b46e9c3b --- /dev/null +++ b/tools/test_rockydocs.sh @@ -0,0 +1,810 @@ +#!/bin/bash + +# Rocky Linux Documentation - Automated Test Harness +# Tests all features of rockydocs-dev-12.sh +# Developer-only tool for validation and regression testing + +set -e + +VERSION="1.0.0" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROCKYDOCS_SCRIPT="$SCRIPT_DIR/rockydocs-dev-12.sh" +TEST_WORKSPACE="/tmp/rocky_test_harness" +TEST_CONTENT_DIR="$TEST_WORKSPACE/test_documentation" + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' + +# Print functions +print_success() { echo -e "${GREEN}[PASS]${NC} $1"; } +print_fail() { echo -e "${RED}[FAIL]${NC} $1"; } +print_info() { echo -e "${BLUE}[INFO]${NC} $1"; } +print_warning() { echo -e "${YELLOW}[WARN]${NC} $1"; } + +# Test framework variables +TESTS_RUN=0 +TESTS_PASSED=0 +TESTS_FAILED=0 + +# Test case function +test_case() { + local description="$1" + local command="$2" + local expected_exit_code="${3:-0}" + + TESTS_RUN=$((TESTS_RUN + 1)) + print_info "Running test: $description" + + if eval "$command" >/dev/null 2>&1; then + local actual_exit_code=0 + else + local actual_exit_code=$? + fi + + if [ "$actual_exit_code" -eq "$expected_exit_code" ]; then + print_success "$description" + TESTS_PASSED=$((TESTS_PASSED + 1)) + else + print_fail "$description (exit code: $actual_exit_code, expected: $expected_exit_code)" + TESTS_FAILED=$((TESTS_FAILED + 1)) + return 1 + fi +} + +# Assertion functions +assert_http_status() { + local url="$1" + local expected_status="$2" + local timeout="${3:-5}" + + local actual_status=$(curl -s -o /dev/null -w "%{http_code}" --max-time $timeout "$url" 2>/dev/null || echo "000") + + if [ "$actual_status" = "$expected_status" ]; then + return 0 + else + print_fail "HTTP status mismatch for $url: got $actual_status, expected $expected_status" + return 1 + fi +} + +assert_dir_exists() { + local path="$1" + if [ -d "$path" ]; then + return 0 + else + print_fail "Directory does not exist: $path" + return 1 + fi +} + +assert_file_exists() { + local path="$1" + if [ -f "$path" ]; then + return 0 + else + print_fail "File does not exist: $path" + return 1 + fi +} + +assert_git_branch_exists() { + local repo_path="$1" + local branch="$2" + + cd "$repo_path" + if git show-ref --verify --quiet "refs/heads/$branch"; then + return 0 + else + print_fail "Git branch $branch does not exist in $repo_path" + return 1 + fi +} + +assert_git_commit_message() { + local repo_path="$1" + local branch="$2" + local substring="$3" + + cd "$repo_path" + local commit_msg=$(git log --format=%B -n 1 "$branch" 2>/dev/null || echo "") + + if [[ "$commit_msg" == *"$substring"* ]]; then + return 0 + else + print_fail "Git commit message on $branch does not contain '$substring'" + return 1 + fi +} + +assert_process_running() { + local process_pattern="$1" + if pgrep -f "$process_pattern" >/dev/null; then + return 0 + else + print_fail "Process matching '$process_pattern' is not running" + return 1 + fi +} + +assert_port_available() { + local port="$1" + if ! lsof -Pi :$port -sTCP:LISTEN -t >/dev/null; then + return 0 + else + print_fail "Port $port is not available (already in use)" + return 1 + fi +} + +assert_port_in_use() { + local port="$1" + if lsof -Pi :$port -sTCP:LISTEN -t >/dev/null; then + return 0 + else + print_fail "Port $port is not in use" + return 1 + fi +} + +assert_config_contains() { + local config_file="$1" + local pattern="$2" + + if [ -f "$config_file" ] && grep -q "$pattern" "$config_file"; then + return 0 + else + print_fail "Configuration file $config_file does not contain pattern: $pattern" + return 1 + fi +} + +assert_symlink_target() { + local symlink_path="$1" + local expected_target="$2" + + if [ -L "$symlink_path" ]; then + local actual_target=$(readlink "$symlink_path" 2>/dev/null || echo "") + if [[ "$actual_target" == *"$expected_target"* ]]; then + return 0 + else + print_fail "Symlink $symlink_path points to '$actual_target', expected to contain '$expected_target'" + return 1 + fi + else + print_fail "$symlink_path is not a symlink" + return 1 + fi +} + +# Setup test environment +setup_test_environment() { + print_info "Setting up hermetic test environment..." + + # Clean any existing test workspace + if [ -d "$TEST_WORKSPACE" ]; then + rm -rf "$TEST_WORKSPACE" + fi + + # Create test workspace + mkdir -p "$TEST_WORKSPACE" + cd "$TEST_WORKSPACE" + + # Create minimal mock documentation repository + mkdir -p "$TEST_CONTENT_DIR/docs" + cd "$TEST_CONTENT_DIR" + + # Initialize git repository + git init + git config user.name "Test User" + git config user.email "test@example.com" + + # Create basic content structure + cat > "docs/index.md" << 'EOF' +# Rocky Linux Documentation Test + +This is a test documentation site. + +## Features + +- Test content +- Multiple versions +- Version selector + +EOF + + mkdir -p "docs/guides" + cat > "docs/guides/test-guide.md" << 'EOF' +# Test Guide + +This is a test guide for validation. + +EOF + + # Create mkdocs.yml + cat > "mkdocs.yml" << 'EOF' +site_name: Rocky Linux Documentation Test +docs_dir: docs +site_dir: site + +nav: + - Home: index.md + - Guides: + - Test Guide: guides/test-guide.md + +theme: + name: material + +EOF + + # Commit initial content + git add . + git commit -m "Initial test content" + + # Create test branches + git checkout -b rocky-8 + echo "Rocky Linux 8 specific content" >> docs/index.md + git add docs/index.md + git commit -m "Rocky Linux 8 content" + + git checkout -b rocky-9 + echo "Rocky Linux 9 specific content" >> docs/index.md + git add docs/index.md + git commit -m "Rocky Linux 9 content" + + git checkout main + echo "Rocky Linux 10 specific content" >> docs/index.md + git add docs/index.md + git commit -m "Rocky Linux 10 content" + + print_success "Test environment setup complete" +} + +# Cleanup test environment +cleanup_test_environment() { + print_info "Cleaning up test environment..." + + # Kill any test servers + pkill -f "python.*http\.server" 2>/dev/null || true + pkill -f "mike serve" 2>/dev/null || true + pkill -f "mkdocs serve" 2>/dev/null || true + + # Remove test workspace + if [ -d "$TEST_WORKSPACE" ]; then + rm -rf "$TEST_WORKSPACE" + fi + + # Clean any test configuration + if [ -f "$HOME/.config/rockydocs/config" ]; then + if grep -q "$TEST_WORKSPACE" "$HOME/.config/rockydocs/config" 2>/dev/null; then + rm -f "$HOME/.config/rockydocs/config" + fi + fi + + print_success "Test environment cleaned up" +} + +# Test suite: Setup and Clean +test_setup_and_clean() { + print_info "=== Testing Setup and Clean Operations ===" + + cd "$TEST_CONTENT_DIR" + + # Test setup command + test_case "Setup creates workspace and app repository" \ + "$ROCKYDOCS_SCRIPT --setup --venv --minimal --workspace $TEST_WORKSPACE/workspace" + + # Verify setup results + test_case "App directory exists after setup" \ + "assert_dir_exists $TEST_WORKSPACE/workspace/docs.rockylinux.org" + + test_case "Virtual environment exists after setup" \ + "assert_dir_exists $TEST_WORKSPACE/workspace/docs.rockylinux.org/venv" + + test_case "Compatibility symlink exists" \ + "[ -L $TEST_WORKSPACE/workspace/app ]" + + # Test clean command + test_case "Clean removes workspace" \ + "echo 'y' | $ROCKYDOCS_SCRIPT --clean --workspace $TEST_WORKSPACE/workspace" + + test_case "Workspace removed after clean" \ + "[ ! -d $TEST_WORKSPACE/workspace ]" +} + +# Test suite: Deployment +test_deployment() { + print_info "=== Testing Deployment Operations ===" + + cd "$TEST_CONTENT_DIR" + + # Setup for deployment tests + $ROCKYDOCS_SCRIPT --setup --venv --minimal --workspace "$TEST_WORKSPACE/workspace" >/dev/null 2>&1 + + # Test deploy command + test_case "Deploy creates local gh-pages branch" \ + "$ROCKYDOCS_SCRIPT --deploy --minimal --workspace $TEST_WORKSPACE/workspace" + + # Verify deployment results + local app_dir="$TEST_WORKSPACE/workspace/docs.rockylinux.org" + + test_case "gh-pages branch exists after deploy" \ + "assert_git_branch_exists $app_dir gh-pages" + + test_case "versions.json exists in gh-pages" \ + "cd $app_dir && git show gh-pages:versions.json >/dev/null 2>&1" + + test_case "Version 8 directory exists in gh-pages" \ + "cd $app_dir && git ls-tree gh-pages:8 >/dev/null 2>&1" + + test_case "Version 9 directory exists in gh-pages" \ + "cd $app_dir && git ls-tree gh-pages:9 >/dev/null 2>&1" + + test_case "Version 10 directory exists in gh-pages" \ + "cd $app_dir && git ls-tree gh-pages:10 >/dev/null 2>&1" + + test_case "Latest directory exists in gh-pages" \ + "cd $app_dir && git ls-tree gh-pages:latest >/dev/null 2>&1" + + test_case "Web root override commit exists" \ + "assert_git_commit_message $app_dir gh-pages 'web root override'" +} + +# Test suite: Serving modes +test_serving_modes() { + print_info "=== Testing Serving Modes ===" + + cd "$TEST_CONTENT_DIR" + + # Setup for serving tests + $ROCKYDOCS_SCRIPT --setup --venv --minimal --workspace "$TEST_WORKSPACE/workspace" >/dev/null 2>&1 + $ROCKYDOCS_SCRIPT --deploy --minimal --workspace "$TEST_WORKSPACE/workspace" >/dev/null 2>&1 + + local app_dir="$TEST_WORKSPACE/workspace/docs.rockylinux.org" + + # Test mike serve mode (should not fail with config errors) + print_info "Testing mike serve mode..." + + cd "$app_dir" + # Start mike serve in background and capture output + timeout 5 bash -c "$ROCKYDOCS_SCRIPT --serve --workspace $TEST_WORKSPACE/workspace" > /tmp/serve_test.log 2>&1 & + local serve_pid=$! + + # Give server time to start + sleep 3 + + # Check if mike serve started without config errors + if grep -q "Config value.*docs_dir.*isn't an existing directory" /tmp/serve_test.log; then + test_case "Mike serve starts without config errors" "false" + else + test_case "Mike serve starts without config errors" "true" + fi + + # Clean up server + kill $serve_pid 2>/dev/null || true + pkill -f "mike serve" 2>/dev/null || true + sleep 2 + + # Test static serving mode + print_info "Testing static serving mode..." + + # First test if static extraction works without serving + cd "$app_dir" + test_case "Static extraction completes without errors" \ + "$ROCKYDOCS_SCRIPT --serve --static --workspace $TEST_WORKSPACE/workspace 2>&1 | grep -q 'Static site extracted successfully'" + + # Check if symlinks are preserved in extracted content + if [ -L "site-static/latest" ]; then + test_case "Latest symlink preserved in static extraction" "true" + + # Test if symlink target exists + local latest_target=$(readlink "site-static/latest" 2>/dev/null || echo "") + if [ -n "$latest_target" ] && [ -d "site-static/$latest_target" ]; then + test_case "Latest symlink target directory exists" "true" + else + test_case "Latest symlink target directory exists" "false" + fi + else + test_case "Latest symlink preserved in static extraction" "false" + fi + + # Test actual HTTP serving if extraction succeeded + if [ -d "site-static" ] && [ -f "site-static/index.html" ]; then + timeout 10 bash -c "$ROCKYDOCS_SCRIPT --serve --static --workspace $TEST_WORKSPACE/workspace" > /tmp/static_serve_test.log 2>&1 & + local static_serve_pid=$! + + # Give server time to start + sleep 3 + + # Test HTTP responses + test_case "Static server responds to root URL" \ + "assert_http_status http://localhost:8000/ 200" + + test_case "Static server responds to versioned URL" \ + "assert_http_status http://localhost:8000/9/ 200" + + # Clean up server + kill $static_serve_pid 2>/dev/null || true + sleep 2 + else + print_info "Skipping HTTP tests - static extraction failed" + test_case "Static server responds to root URL" "false" + test_case "Static server responds to versioned URL" "false" + fi + + # Test that static extraction works + test_case "Static site directory exists after serving" \ + "assert_dir_exists $app_dir/site-static" + + test_case "Static site contains index.html" \ + "assert_file_exists $app_dir/site-static/index.html" + + # Cleanup test logs + rm -f /tmp/serve_test.log /tmp/static_serve_test.log +} + +# Test suite: Utilities +test_utilities() { + print_info "=== Testing Utility Functions ===" + + cd "$TEST_CONTENT_DIR" + + # Test status command + test_case "Status command runs without error" \ + "$ROCKYDOCS_SCRIPT --status" + + # Test help commands + test_case "Main help displays correctly" \ + "$ROCKYDOCS_SCRIPT --help" + + test_case "Setup help displays correctly" \ + "$ROCKYDOCS_SCRIPT --setup --help" + + test_case "Serve help displays correctly" \ + "$ROCKYDOCS_SCRIPT --serve --help" + + test_case "Deploy help displays correctly" \ + "$ROCKYDOCS_SCRIPT --deploy --help" + + test_case "Dual server help displays correctly" \ + "$ROCKYDOCS_SCRIPT --serve-dual --help" +} + +# Test dependency checking +test_dependencies() { + print_info "=== Testing Dependency Checking ===" + + cd "$TEST_CONTENT_DIR" + + # Test with all dependencies available (should pass) + test_case "Dependencies check passes with all tools available" \ + "$ROCKYDOCS_SCRIPT --status" + + # This test would require temporarily hiding dependencies, which is complex + # For now, we assume the dependency check works if the script runs + print_info "Dependency checking tested indirectly through script execution" +} + +# Test configuration management +test_configuration_management() { + print_info "=== Testing Configuration Management ===" + + cd "$TEST_CONTENT_DIR" + + # Test workspace configuration persistence + local test_config_dir="$TEST_WORKSPACE/test_config" + mkdir -p "$test_config_dir" + + # Test custom workspace configuration + test_case "Custom workspace configuration is saved" \ + "$ROCKYDOCS_SCRIPT --setup --venv --minimal --workspace $test_config_dir/custom_workspace" + + # Test configuration reset + test_case "Configuration reset removes saved config" \ + "$ROCKYDOCS_SCRIPT --reset" + + # Test default workspace fallback + test_case "Default workspace fallback works after reset" \ + "$ROCKYDOCS_SCRIPT --status" +} + +# Test error handling and edge cases +test_error_handling() { + print_info "=== Testing Error Handling and Edge Cases ===" + + cd "$TEST_CONTENT_DIR" + + # Test invalid workspace path + test_case "Invalid workspace path is handled gracefully" \ + "$ROCKYDOCS_SCRIPT --setup --workspace /nonexistent/invalid/path 2>&1 | grep -q 'Error\\|Failed'" 1 + + # Test missing content directory + local temp_dir=$(mktemp -d) + cd "$temp_dir" + test_case "Missing docs directory is detected" \ + "$ROCKYDOCS_SCRIPT --setup --venv --minimal 2>&1 | grep -q 'docs.*not found\\|No docs directory'" 1 + cd "$TEST_CONTENT_DIR" + rm -rf "$temp_dir" + + # Test serve without deployment + test_case "Serve without deployment shows appropriate message" \ + "$ROCKYDOCS_SCRIPT --serve 2>&1 | grep -q 'No gh-pages branch found\\|deploy first'" 1 +} + +# Test workspace and environment variations +test_environment_variations() { + print_info "=== Testing Environment Variations ===" + + cd "$TEST_CONTENT_DIR" + + # Test different environment types + local env_workspace="$TEST_WORKSPACE/env_test" + + # Test venv environment + test_case "Virtual environment setup completes" \ + "$ROCKYDOCS_SCRIPT --setup --venv --minimal --workspace $env_workspace" + + test_case "Virtual environment directory exists" \ + "assert_dir_exists $env_workspace/docs.rockylinux.org/venv" + + test_case "Virtual environment activation script exists" \ + "assert_file_exists $env_workspace/docs.rockylinux.org/venv/bin/activate" + + # Clean for next test + rm -rf "$env_workspace" + + # Test minimal vs full build types + test_case "Minimal build configuration is applied" \ + "$ROCKYDOCS_SCRIPT --setup --venv --minimal --workspace $env_workspace && \ + grep -q 'minimal\\|en.*uk' $env_workspace/docs.rockylinux.org/configs/mkdocs.yml" + + rm -rf "$env_workspace" + + test_case "Full build configuration is applied" \ + "$ROCKYDOCS_SCRIPT --setup --venv --full --workspace $env_workspace && \ + grep -q 'full\\|languages.*all' $env_workspace/docs.rockylinux.org/configs/mkdocs.yml" +} + +# Test port management and conflict resolution +test_port_management() { + print_info "=== Testing Port Management ===" + + cd "$TEST_CONTENT_DIR" + + # Setup for port tests + $ROCKYDOCS_SCRIPT --setup --venv --minimal --workspace "$TEST_WORKSPACE/port_test" >/dev/null 2>&1 + $ROCKYDOCS_SCRIPT --deploy --minimal --workspace "$TEST_WORKSPACE/port_test" >/dev/null 2>&1 + + # Test port conflict detection + # Start a simple server on port 8000 + python3 -m http.server 8000 >/dev/null 2>&1 & + local blocking_server_pid=$! + sleep 2 + + # Test that port conflict is detected and handled + timeout 10 bash -c "$ROCKYDOCS_SCRIPT --serve --static --workspace $TEST_WORKSPACE/port_test" > /tmp/port_conflict_test.log 2>&1 & + local test_server_pid=$! + sleep 3 + + # Check if alternative port was used or conflict was reported + if grep -q "port.*conflict\\|alternative port\\|8001" /tmp/port_conflict_test.log; then + test_case "Port conflict detection and resolution works" "true" + else + test_case "Port conflict detection and resolution works" "false" + fi + + # Cleanup + kill $blocking_server_pid 2>/dev/null || true + kill $test_server_pid 2>/dev/null || true + sleep 2 + rm -f /tmp/port_conflict_test.log +} + +# Test git operations and versioning +test_git_operations() { + print_info "=== Testing Git Operations and Versioning ===" + + cd "$TEST_CONTENT_DIR" + + # Setup for git tests + local git_workspace="$TEST_WORKSPACE/git_test" + $ROCKYDOCS_SCRIPT --setup --venv --minimal --workspace "$git_workspace" >/dev/null 2>&1 + $ROCKYDOCS_SCRIPT --deploy --minimal --workspace "$git_workspace" >/dev/null 2>&1 + + local app_dir="$git_workspace/docs.rockylinux.org" + + # Test git branch operations + test_case "Git repository is properly initialized" \ + "[ -d $app_dir/.git ]" + + test_case "Worktree directory exists for caching" \ + "assert_dir_exists $app_dir/worktrees" + + # Test version detection on different branches + git checkout rocky-8 >/dev/null 2>&1 + test_case "Version detection works for rocky-8 branch" \ + "$ROCKYDOCS_SCRIPT --status | grep -q 'rocky-8\\|version 8'" + + git checkout rocky-9 >/dev/null 2>&1 + test_case "Version detection works for rocky-9 branch" \ + "$ROCKYDOCS_SCRIPT --status | grep -q 'rocky-9\\|version 9'" + + git checkout main >/dev/null 2>&1 + test_case "Version detection works for main branch" \ + "$ROCKYDOCS_SCRIPT --status | grep -q 'main\\|version 10'" + + # Test git operations in app repository + cd "$app_dir" + test_case "Worktree main cache exists" \ + "assert_dir_exists $app_dir/worktrees/main" + + test_case "Content symlink is properly created" \ + "[ -L content ] && [ -d content ]" + + cd "$TEST_CONTENT_DIR" +} + +# Test performance optimizations +test_performance_features() { + print_info "=== Testing Performance Features ===" + + cd "$TEST_CONTENT_DIR" + + # Setup for performance tests + local perf_workspace="$TEST_WORKSPACE/performance_test" + + # Time the setup process + local start_time=$(date +%s) + test_case "Performance-optimized setup completes efficiently" \ + "$ROCKYDOCS_SCRIPT --setup --venv --minimal --workspace $perf_workspace" + local setup_time=$(($(date +%s) - start_time)) + + print_info "Setup completed in ${setup_time}s" + + # Test cached repository reuse + local app_dir="$perf_workspace/docs.rockylinux.org" + test_case "Repository caching is working" \ + "[ -d $app_dir/worktrees/main/.git ]" + + # Test consolidated git operations + start_time=$(date +%s) + test_case "Performance-optimized deployment completes efficiently" \ + "$ROCKYDOCS_SCRIPT --deploy --minimal --workspace $perf_workspace" + local deploy_time=$(($(date +%s) - start_time)) + + print_info "Deployment completed in ${deploy_time}s" + + # Test optimized web root override + cd "$app_dir" + if git show-ref --verify --quiet refs/heads/gh-pages; then + test_case "Optimized web root override creates commit efficiently" \ + "git log --oneline gh-pages | head -1 | grep -q 'optimized.*web root override'" + else + test_case "Optimized web root override creates commit efficiently" "false" + fi + + cd "$TEST_CONTENT_DIR" +} + +# Main test runner +run_all_tests() { + print_info "Rocky Linux Documentation Test Harness v$VERSION" + print_info "Testing rockydocs-dev-12.sh functionality" + echo "" + + # Verify test script exists + if [ ! -f "$ROCKYDOCS_SCRIPT" ]; then + print_fail "Test subject not found: $ROCKYDOCS_SCRIPT" + exit 1 + fi + + # Setup test environment + setup_test_environment + + # Run test suites + test_dependencies + test_setup_and_clean + test_deployment + test_serving_modes + test_utilities + test_configuration_management + test_error_handling + test_environment_variations + test_port_management + test_git_operations + test_performance_features + + # Cleanup + cleanup_test_environment + + # Report results + echo "" + print_info "=== Test Results ===" + echo "Tests run: $TESTS_RUN" + echo "Passed: $TESTS_PASSED" + echo "Failed: $TESTS_FAILED" + + if [ $TESTS_FAILED -eq 0 ]; then + print_success "All tests passed!" + exit 0 + else + print_fail "$TESTS_FAILED test(s) failed" + exit 1 + fi +} + +# Handle command line arguments +case "${1:-run}" in + --help|-h) + cat << EOF +Rocky Linux Documentation Test Harness v$VERSION + +DESCRIPTION: + Comprehensive automated test suite for rockydocs-dev-12.sh functionality. + Runs extensive tests in hermetic environments with full coverage validation. + +USAGE: + $0 [COMMAND] + +COMMANDS: + run Run all tests (default) + setup Setup test environment only + cleanup Cleanup test environment only + --help, -h Show this help + +ENVIRONMENT: + Test workspace: $TEST_WORKSPACE + Subject script: $ROCKYDOCS_SCRIPT + +TEST COVERAGE: + ✅ Setup and Clean Operations - Workspace management validation + ✅ Deployment Operations - Multi-version deployment testing + ✅ Serving Modes - Live and static serving validation + ✅ Utility Functions - Status, help, and configuration commands + ✅ Configuration Management - Workspace configuration persistence + ✅ Error Handling & Edge Cases - Graceful failure and error detection + ✅ Environment Variations - Virtual environment and build types + ✅ Port Management - Conflict detection and resolution + ✅ Git Operations & Versioning - Repository operations and branch handling + ✅ Performance Features - Optimization validation and timing + +FEATURES: + • Hermetic test environments with no external dependencies + • HTTP response validation with actual server testing + • Git repository and branch validation + • Symlink preservation testing + • Port conflict resolution testing + • Performance timing and optimization validation + • Configuration persistence and workspace management + • Error handling and edge case detection + +NOTES: + - All tests run in isolated temporary directories + - No interference with real workspaces or configurations + - Comprehensive regression detection and validation + - Automatically cleans up after completion + - Developer-only tool, not for end users + +EOF + exit 0 + ;; + setup) + setup_test_environment + exit 0 + ;; + cleanup) + cleanup_test_environment + exit 0 + ;; + run|"") + run_all_tests + ;; + *) + print_fail "Unknown command: $1" + echo "Use --help for usage information" + exit 1 + ;; +esac \ No newline at end of file