diff --git a/customers/demo-felhom.yaml b/customers/demo-felhom.yaml deleted file mode 100644 index daa0a99..0000000 --- a/customers/demo-felhom.yaml +++ /dev/null @@ -1,84 +0,0 @@ -# Customer: Demo / Test Server (N100 Mini PC) -# Hardware: Intel N100, 16GB RAM, 128GB NVMe + 1TB HDD -# Network: Local + Cloudflare Tunnel for demo access - -customer_id: demo-felhom -domain: demo-felhom.eu -hdd_path: /mnt/hdd_1 -gitea_repo: customers/demo-felhom-stacks -hardware: n100 -notes: "Internal demo/test server for validating deployments" - -# Apps to deploy on this node -apps: - - actualbudget - - docmost - - filebrowser - - homebox - - immich - - mealie - - paperless-ngx - - romm - - stirling-pdf - - vaultwarden - -# Per-customer overrides (optional) -# Uncomment to pin versions or disable auto-updates -overrides: {} - # immich_version: "v2.5.5" # Pin Immich to specific version - # auto_update: false # Skip ALL version updates from catalog - -# Portainer env vars to set (reference only — actual secrets go in Portainer!) -# These are documented here so you remember what each stack needs. -env_vars_reference: - docmost: - APP_SECRET: "generate with: openssl rand -hex 32" - DB_PASSWORD: "generate secure password" - immich: - DB_PASSWORD: "generate secure password" - paperless-ngx: - PAPERLESS_SECRET_KEY: "generate with: openssl rand -hex 32" - DB_PASSWORD: "generate secure password" - PAPERLESS_ADMIN_USER: "admin" - PAPERLESS_ADMIN_PASSWORD: "set initial password" - romm: - DB_PASSWORD: "generate secure password" - MYSQL_ROOT_PASSWORD: "generate secure password" - ROMM_AUTH_SECRET_KEY: "generate with: openssl rand -hex 32" - vaultwarden: - ADMIN_TOKEN: "generate with: openssl rand -hex 32" - SIGNUPS_ALLOWED: "true (set to false after account creation)" - -# Storage layout reference -# This shows where user data lives after render (HDD host paths): -# -# /mnt/hdd_1/ ← HDD root (filebrowser serves this) -# /mnt/hdd_1/storage/immich/ ← photos & videos -# /mnt/hdd_1/storage/paperless/consume/ ← drop documents here for OCR -# /mnt/hdd_1/storage/paperless/media/ ← processed documents -# /mnt/hdd_1/storage/paperless/export/ ← document exports / backup -# /mnt/hdd_1/storage/romm/library/ ← ROM files -# /mnt/hdd_1/storage/romm/resources/ ← cover art, metadata -# -# Named volumes (on NVMe, managed by Docker): -# actualbudget_data, docmost_*, homebox_data, mealie_data, -# immich_postgres_data, paperless_data, vaultwarden_data, etc. - -# Backup considerations -backup_notes: - databases: - - "docmost: PostgreSQL (docmost-postgres)" - - "immich: PostgreSQL (immich-postgres)" - - "paperless-ngx: PostgreSQL (paperless-postgres)" - - "romm: MariaDB (romm-db)" - hdd_paths: - - "/mnt/hdd_1/storage/immich (photos — large, Backrest read-only mount)" - - "/mnt/hdd_1/storage/paperless/media (documents — Backrest read-only mount)" - - "/mnt/hdd_1/storage/romm/library (ROMs — Backrest read-only mount)" - named_volumes: - - "actualbudget_data" - - "docmost_storage" - - "homebox_data" - - "mealie_data" - - "stirling_data" - - "vaultwarden_data" diff --git a/customers/pi-customer-1.yaml b/customers/pi-customer-1.yaml deleted file mode 100644 index f32812b..0000000 --- a/customers/pi-customer-1.yaml +++ /dev/null @@ -1,44 +0,0 @@ -# Customer: Pi Test Customer #1 (Raspberry Pi) -# Hardware: Raspberry Pi 4/5, 4-8GB RAM, SD/USB + External HDD -# Network: Local only (.local domain with self-signed cert) - -customer_id: pi-customer-1 -domain: pi-customer-1.local -hdd_path: /mnt/hdd_1 -gitea_repo: customers/pi-customer-1-stacks -hardware: rpi -notes: "Test customer on Raspberry Pi — lightweight apps only" - -# Apps to deploy on this node (Pi-compatible only) -apps: - - actualbudget - - filebrowser - - mealie - - stirling-pdf - - vaultwarden - -# Per-customer overrides -overrides: {} - -# Portainer env vars to set (reference only) -env_vars_reference: - vaultwarden: - ADMIN_TOKEN: "generate with: openssl rand -hex 32" - SIGNUPS_ALLOWED: "true (set to false after account creation)" - -# Storage layout reference: -# /mnt/hdd_1/ ← HDD root (filebrowser serves this) -# -# Named volumes (on SD/USB boot, managed by Docker): -# actualbudget_data, mealie_data, stirling_data, vaultwarden_data - -# Backup considerations -backup_notes: - databases: [] # No database containers — all apps use SQLite/file storage - hdd_paths: - - "/mnt/hdd_1 (filebrowser root — user files)" - named_volumes: - - "actualbudget_data" - - "mealie_data" - - "stirling_data" - - "vaultwarden_data" diff --git a/scripts/render.sh b/scripts/render.sh deleted file mode 100755 index 186be8d..0000000 --- a/scripts/render.sh +++ /dev/null @@ -1,585 +0,0 @@ -#!/bin/bash -#=============================================================================== -# Felhom App Catalog - Customer Repo Renderer v2.0 -# -# Reads customer YAML configs and generates Docker Compose stacks -# by substituting {{DOMAIN}} and {{HDD_PATH}} placeholders in templates. -# -# Output structure (monorepo — all customers in one Gitea repo): -# output/ -# ├── README.md -# ├── demo-felhom/ -# │ ├── actualbudget/docker-compose.yml -# │ ├── immich/docker-compose.yml -# │ └── ... -# └── pi-customer-1/ -# ├── actualbudget/docker-compose.yml -# └── ... -# -# Portainer GitOps compose path example: -# demo-felhom/actualbudget/docker-compose.yml -# -# Usage: -# ./render.sh # Render all customers -# ./render.sh --customer demo-felhom # Render specific customer -# ./render.sh --dry-run # Show what would be done -# ./render.sh --push # Render + commit + push to Gitea -# ./render.sh --output-dir /tmp/out # Custom output directory -# ./render.sh --debug # Verbose output -# -# Prerequisites: -# - git (for push mode) -# - Access to Gitea repo (for push mode) -# -#=============================================================================== - -set -euo pipefail - -#------------------------------------------------------------------------------- -# Configuration -#------------------------------------------------------------------------------- -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -CATALOG_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" -TEMPLATES_DIR="${CATALOG_DIR}/templates" -CUSTOMERS_DIR="${CATALOG_DIR}/customers" -DEFAULT_OUTPUT_DIR="${CATALOG_DIR}/output" - -# Gitea monorepo settings (for --push mode) -GITEA_REPO_URL="${GITEA_REPO_URL:-https://gitea.dooplex.hu/admin/customers-felhom.eu.git}" - -#------------------------------------------------------------------------------- -# Colors & Logging -#------------------------------------------------------------------------------- -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -BOLD='\033[1m' -NC='\033[0m' - -log_info() { echo -e "${CYAN}[INFO]${NC} $*"; } -log_success() { echo -e "${GREEN}[OK]${NC} $*"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } -log_error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } -log_debug() { [[ "${DEBUG:-false}" == "true" ]] && echo -e "[DEBUG] $*" || true; } -log_step() { echo -e "\n${BOLD}[STEP]${NC} $*"; } - -#------------------------------------------------------------------------------- -# Simple YAML parser (no external dependencies) -#------------------------------------------------------------------------------- -yaml_get_value() { - local file="$1" key="$2" - grep -E "^${key}:" "$file" 2>/dev/null | sed "s/^${key}:[[:space:]]*//" | sed 's/^"//' | sed 's/"$//' | sed "s/^'//" | sed "s/'$//" || echo "" -} - -yaml_get_list() { - local file="$1" key="$2" - local in_section=false - while IFS= read -r line; do - if [[ "$line" =~ ^${key}: ]]; then - in_section=true - continue - fi - if $in_section; then - if [[ "$line" =~ ^[[:space:]]*-[[:space:]]+(.*) ]]; then - echo "${BASH_REMATCH[1]}" | sed 's/^"//' | sed 's/"$//' - elif [[ "$line" =~ ^[a-zA-Z_] ]]; then - break - fi - fi - done < "$file" -} - -yaml_get_override() { - local file="$1" key="$2" - local in_overrides=false - while IFS= read -r line; do - if [[ "$line" =~ ^overrides: ]]; then - in_overrides=true - continue - fi - if $in_overrides; then - if [[ "$line" =~ ^[[:space:]]+${key}:[[:space:]]*(.*) ]]; then - local val="${BASH_REMATCH[1]}" - echo "$val" | sed 's/^"//' | sed 's/"$//' | sed "s/^'//" | sed "s/'$//" - return - fi - if [[ "$line" =~ ^[a-zA-Z_] ]]; then - break - fi - fi - done < "$file" - echo "" -} - -#------------------------------------------------------------------------------- -# Template rendering (one customer) -#------------------------------------------------------------------------------- -render_customer() { - local customer_file="$1" - local output_base="$2" - - local customer_id domain hdd_path - customer_id=$(yaml_get_value "$customer_file" "customer_id") - domain=$(yaml_get_value "$customer_file" "domain") - hdd_path=$(yaml_get_value "$customer_file" "hdd_path") - - if [[ -z "$customer_id" || -z "$domain" ]]; then - log_error "Missing customer_id or domain in: $customer_file" - return 1 - fi - - log_step "Rendering customer: ${BOLD}${customer_id}${NC} (${domain})" - - # Check auto_update override - local auto_update - auto_update=$(yaml_get_override "$customer_file" "auto_update") - if [[ "$auto_update" == "false" ]]; then - log_warn "auto_update=false — skipping this customer" - return 0 - fi - - # Get app list - local apps=() - while IFS= read -r app; do - [[ -n "$app" ]] && apps+=("$app") - done < <(yaml_get_list "$customer_file" "apps") - - if [[ ${#apps[@]} -eq 0 ]]; then - log_warn "No apps configured for $customer_id" - return 0 - fi - - log_info "Apps: ${apps[*]}" - log_info "Domain: $domain" - if [[ -n "$hdd_path" ]]; then - log_info "HDD path: $hdd_path" - fi - - # Output directory: output// (no -stacks suffix) - local customer_output="${output_base}/${customer_id}" - - # Strip .git from repo URL for display - local repo_display="${GITEA_REPO_URL%.git}" - - if [[ "$DRY_RUN" == "true" ]]; then - echo -e " ${CYAN}[DRY-RUN]${NC} Would create: ${customer_output}/" - for app in "${apps[@]}"; do - echo -e " ${CYAN}[DRY-RUN]${NC} ${app}/docker-compose.yml ({{DOMAIN}} → ${domain})" - # Check if template uses HDD_PATH - local template="${TEMPLATES_DIR}/${app}/docker-compose.yml" - if [[ -f "$template" ]] && grep -q '{{HDD_PATH}}' "$template"; then - if [[ -n "$hdd_path" ]]; then - echo -e " ${CYAN}[DRY-RUN]${NC} ↳ HDD path: {{HDD_PATH}} → ${hdd_path}" - else - echo -e " ${YELLOW}[DRY-RUN]${NC} ↳ ⚠ Template uses {{HDD_PATH}} but hdd_path not set!" - fi - fi - local version_override - version_override=$(yaml_get_override "$customer_file" "${app}_version") - if [[ -n "$version_override" ]]; then - echo -e " ${CYAN}[DRY-RUN]${NC} ↳ version pinned to: ${version_override}" - fi - done - echo -e " ${CYAN}[DRY-RUN]${NC} Portainer compose paths:" - for app in "${apps[@]}"; do - echo -e " ${CYAN}[DRY-RUN]${NC} ${customer_id}/${app}/docker-compose.yml" - done - return 0 - fi - - # Create output directory - mkdir -p "$customer_output" - - # Generate a README for this customer's section - cat > "${customer_output}/README.md" << EOF -# ${customer_id} - -**Domain:** \`${domain}\` -**HDD Path:** \`${hdd_path:-N/A}\` -**Generated:** $(date -u '+%Y-%m-%d %H:%M:%S UTC') - -## Deployed Apps - -| App | Portainer Compose Path | -|-----|------------------------| -$(for app in "${apps[@]}"; do echo "| ${app} | \`${customer_id}/${app}/docker-compose.yml\` |"; done) - -## Portainer Stack Setup - -For each app, create a stack in Portainer: -1. **Stacks → Add Stack → Repository** -2. Repository URL: \`${repo_display}\` -3. Compose path: \`${customer_id}//docker-compose.yml\` -4. Add required environment variables (see comments in compose files) -5. Enable GitOps auto-update (optional, polling interval: 5 min) -6. Deploy - ---- -*Auto-generated by felhom-app-catalog render.sh. Do not edit manually.* -EOF - - # Process each app - local rendered_count=0 - for app in "${apps[@]}"; do - local template="${TEMPLATES_DIR}/${app}/docker-compose.yml" - - if [[ ! -f "$template" ]]; then - log_error "Template not found: ${template}" - continue - fi - - mkdir -p "${customer_output}/${app}" - - # Substitute {{DOMAIN}} with customer domain - sed "s/{{DOMAIN}}/${domain}/g" "$template" > "${customer_output}/${app}/docker-compose.yml" - - # Substitute {{HDD_PATH}} if the template uses it - if grep -q '{{HDD_PATH}}' "${customer_output}/${app}/docker-compose.yml"; then - if [[ -z "$hdd_path" ]]; then - log_error " ${app}: template uses {{HDD_PATH}} but hdd_path not set in customer config!" - log_error " Add 'hdd_path: /mnt/hdd_1' (or similar) to ${customer_file}" - rm "${customer_output}/${app}/docker-compose.yml" - rmdir "${customer_output}/${app}" 2>/dev/null || true - continue - fi - local clean_hdd_path="${hdd_path%/}" - sed -i "s|{{HDD_PATH}}|${clean_hdd_path}|g" "${customer_output}/${app}/docker-compose.yml" - fi - - # Apply version override if configured - local version_override - version_override=$(yaml_get_override "$customer_file" "${app}_version") - if [[ -n "$version_override" ]]; then - log_info " ${app}: applying version override → ${version_override}" - case "$app" in - immich) - sed -i "s|ghcr.io/immich-app/immich-server:[^ ]*|ghcr.io/immich-app/immich-server:${version_override}|g" \ - "${customer_output}/${app}/docker-compose.yml" - sed -i "s|ghcr.io/immich-app/immich-machine-learning:[^ ]*|ghcr.io/immich-app/immich-machine-learning:${version_override}|g" \ - "${customer_output}/${app}/docker-compose.yml" - ;; - *) - local first_image - first_image=$(grep -m1 "image:" "${customer_output}/${app}/docker-compose.yml" | sed 's/.*image:[[:space:]]*//') - if [[ -n "$first_image" ]]; then - local image_name - image_name=$(echo "$first_image" | cut -d: -f1) - sed -i "s|${first_image}|${image_name}:${version_override}|" \ - "${customer_output}/${app}/docker-compose.yml" - fi - ;; - esac - fi - - rendered_count=$((rendered_count + 1)) - log_debug " Rendered: ${app}/docker-compose.yml" - done - - log_success "Rendered ${rendered_count}/${#apps[@]} apps → ${customer_output}/" -} - -#------------------------------------------------------------------------------- -# Git push — single monorepo push (all customers together) -#------------------------------------------------------------------------------- -push_to_gitea() { - local output_dir="$1" - - log_step "Pushing to Gitea: ${GITEA_REPO_URL}" - - cd "$output_dir" - - # Initialize git if needed, or update remote - if [[ ! -d ".git" ]]; then - git init -b main - git remote add origin "$GITEA_REPO_URL" - log_info "Initialized new git repo" - else - # Ensure remote URL is current - git remote set-url origin "$GITEA_REPO_URL" 2>/dev/null || \ - git remote add origin "$GITEA_REPO_URL" 2>/dev/null || true - fi - - # Stage all changes - git add -A - - # Check if there are changes to commit - if git diff --cached --quiet 2>/dev/null; then - log_info "No changes to push — output matches Gitea" - return 0 - fi - - # Show what changed - local changed_files - changed_files=$(git diff --cached --name-only | wc -l) - log_info "Changed files: ${changed_files}" - if [[ "$DEBUG" == "true" ]]; then - git diff --cached --name-only | head -20 - fi - - # Commit - local commit_msg="render: $(date -u '+%Y-%m-%d %H:%M:%S UTC')" - # Add summary of what customers were rendered - local customers_rendered - customers_rendered=$(find "$output_dir" -maxdepth 1 -mindepth 1 -type d ! -name '.git' -printf '%f ' 2>/dev/null || true) - if [[ -n "$customers_rendered" ]]; then - commit_msg="${commit_msg} [${customers_rendered}]" - fi - - git commit -m "$commit_msg" - - # Push - if git push origin main 2>/dev/null; then - log_success "Pushed to ${GITEA_REPO_URL}" - else - log_warn "Normal push failed — trying pull+rebase first..." - if git pull --rebase origin main 2>/dev/null; then - git push origin main - log_success "Rebased and pushed to ${GITEA_REPO_URL}" - else - log_warn "Rebase failed — force pushing..." - git push -u origin main --force - log_success "Force-pushed to ${GITEA_REPO_URL}" - fi - fi -} - -#------------------------------------------------------------------------------- -# Generate root README for the monorepo -#------------------------------------------------------------------------------- -generate_root_readme() { - local output_dir="$1" - shift - local customer_files=("$@") - - local repo_display="${GITEA_REPO_URL%.git}" - - cat > "${output_dir}/README.md" << 'HEADER' -# Felhom Customer Stacks - -Auto-generated Docker Compose stacks for all Felhom customers. -**Do not edit manually** — regenerate with `render.sh` from `felhom-app-catalog`. - -HEADER - - echo "## Customers" >> "${output_dir}/README.md" - echo "" >> "${output_dir}/README.md" - echo "| Customer | Domain | Hardware | Apps |" >> "${output_dir}/README.md" - echo "|----------|--------|----------|------|" >> "${output_dir}/README.md" - - for cf in "${customer_files[@]}"; do - local cid cdomain chw capps - cid=$(yaml_get_value "$cf" "customer_id") - cdomain=$(yaml_get_value "$cf" "domain") - chw=$(yaml_get_value "$cf" "hardware") - # Count apps - capps=$(yaml_get_list "$cf" "apps" | wc -l) - echo "| [${cid}](./${cid}/) | \`${cdomain}\` | ${chw} | ${capps} apps |" >> "${output_dir}/README.md" - done - - cat >> "${output_dir}/README.md" << EOF - -## Portainer GitOps Setup - -For each app stack on a customer node: -1. **Stacks → Add Stack → Repository** -2. Repository URL: \`${repo_display}\` -3. Compose path: \`//docker-compose.yml\` -4. Set environment variables (secrets — see comments in compose files) -5. Optional: Enable auto-update polling (5 min) - -## Regenerating - -From the \`felhom-app-catalog\` repo: -\`\`\`bash -./scripts/render.sh # Render locally -./scripts/render.sh --push # Render + push to Gitea -./scripts/render.sh --dry-run # Preview changes -\`\`\` - ---- -*Last generated: $(date -u '+%Y-%m-%d %H:%M:%S UTC')* -EOF - - log_debug "Generated root README.md" -} - -#------------------------------------------------------------------------------- -# Argument parsing -#------------------------------------------------------------------------------- -DRY_RUN=false -PUSH=false -DEBUG=false -TARGET_CUSTOMER="" -OUTPUT_DIR="" - -print_help() { - cat << 'EOF' -Felhom App Catalog - Customer Repo Renderer v2.0 - -Renders Docker Compose stacks from templates into a single Gitea monorepo. -All customers share one repo, each in their own subdirectory. - -USAGE: - ./render.sh [OPTIONS] - -OPTIONS: - --customer ID Render only this customer (others left untouched) - --output-dir DIR Output directory (default: ./output/) - --push Render + commit + push to Gitea - --dry-run Show what would be done - --debug Verbose output - -h, --help Show this help - -ENVIRONMENT VARIABLES: - GITEA_REPO_URL Full Gitea repo URL - (default: https://gitea.dooplex.hu/admin/customers-felhom.eu.git) - -EXAMPLES: - ./render.sh # Render all customers - ./render.sh --customer demo-felhom # Render one customer - ./render.sh --dry-run # Preview changes - ./render.sh --push # Render + push to Gitea - GITEA_REPO_URL=https://gitea.example.com/org/repo.git ./render.sh --push -EOF -} - -while [[ $# -gt 0 ]]; do - case $1 in - --customer) TARGET_CUSTOMER="$2"; shift 2 ;; - --output-dir) OUTPUT_DIR="$2"; shift 2 ;; - --push) PUSH=true; shift ;; - --dry-run) DRY_RUN=true; shift ;; - --debug) DEBUG=true; shift ;; - -h|--help) print_help; exit 0 ;; - *) log_error "Unknown option: $1"; print_help; exit 1 ;; - esac -done - -OUTPUT_DIR="${OUTPUT_DIR:-${DEFAULT_OUTPUT_DIR}}" - -#------------------------------------------------------------------------------- -# Main -#------------------------------------------------------------------------------- -main() { - echo "" - echo -e "${BOLD}Felhom App Catalog — Renderer v2.0${NC}" - echo -e "Templates: ${TEMPLATES_DIR}" - echo -e "Customers: ${CUSTOMERS_DIR}" - echo -e "Output: ${OUTPUT_DIR}" - echo -e "Gitea repo: ${GITEA_REPO_URL}" - echo "" - - # Validate directories - if [[ ! -d "$TEMPLATES_DIR" ]]; then - log_error "Templates directory not found: $TEMPLATES_DIR" - exit 1 - fi - if [[ ! -d "$CUSTOMERS_DIR" ]]; then - log_error "Customers directory not found: $CUSTOMERS_DIR" - exit 1 - fi - - # Find customer configs to process - local customer_files=() - if [[ -n "$TARGET_CUSTOMER" ]]; then - local target_file="${CUSTOMERS_DIR}/${TARGET_CUSTOMER}.yaml" - if [[ ! -f "$target_file" ]]; then - log_error "Customer config not found: $target_file" - exit 1 - fi - customer_files=("$target_file") - else - while IFS= read -r -d '' f; do - customer_files+=("$f") - done < <(find "$CUSTOMERS_DIR" -name "*.yaml" -print0 | sort -z) - fi - - if [[ ${#customer_files[@]} -eq 0 ]]; then - log_error "No customer configs found in $CUSTOMERS_DIR" - exit 1 - fi - - log_info "Found ${#customer_files[@]} customer(s) to render" - - # Create output directory - if [[ "$DRY_RUN" != "true" ]]; then - mkdir -p "$OUTPUT_DIR" - fi - - # If --push and output dir already has a .git from previous push, pull first - if [[ "$PUSH" == "true" && "$DRY_RUN" != "true" && -d "${OUTPUT_DIR}/.git" ]]; then - log_info "Pulling latest from Gitea before rendering..." - cd "$OUTPUT_DIR" - git pull --rebase origin main 2>/dev/null || true - cd - > /dev/null - fi - - # Clean output directory before rendering (preserve .git only) - # This prevents stale files from old runs accumulating - if [[ "$DRY_RUN" != "true" ]]; then - local stale_count=0 - for item in "${OUTPUT_DIR}"/*; do - [[ -e "$item" ]] || continue - rm -rf "$item" - stale_count=$((stale_count + 1)) - done - # Also remove hidden files except .git - for item in "${OUTPUT_DIR}"/.[!.]*; do - [[ -e "$item" ]] || continue - [[ "$(basename "$item")" == ".git" ]] && continue - rm -rf "$item" - done - if [[ $stale_count -gt 0 ]]; then - log_info "Cleaned ${stale_count} item(s) from output directory" - fi - fi - - # Render each customer - local success_count=0 - for customer_file in "${customer_files[@]}"; do - if render_customer "$customer_file" "$OUTPUT_DIR"; then - success_count=$((success_count + 1)) - fi - done - - # Generate root README (only when rendering all customers) - if [[ -z "$TARGET_CUSTOMER" && "$DRY_RUN" != "true" ]]; then - # Get ALL customer files for the root README (even if we only rendered some) - local all_customer_files=() - while IFS= read -r -d '' f; do - all_customer_files+=("$f") - done < <(find "$CUSTOMERS_DIR" -name "*.yaml" -print0 | sort -z) - generate_root_readme "$OUTPUT_DIR" "${all_customer_files[@]}" - fi - - echo "" - echo -e "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - echo -e "${BOLD}${GREEN} Rendering complete: ${success_count}/${#customer_files[@]} customers${NC}" - echo -e "${BOLD}${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" - - if [[ "$DRY_RUN" != "true" ]]; then - echo "" - echo -e "${BOLD}Output structure:${NC}" - # Show tree-like output - for d in "${OUTPUT_DIR}"/*/; do - [[ -d "$d" && "$(basename "$d")" != ".git" ]] || continue - local cid - cid=$(basename "$d") - local app_count - app_count=$(find "$d" -name "docker-compose.yml" | wc -l) - echo -e " 📁 ${BOLD}${cid}/${NC} (${app_count} apps)" - find "$d" -name "docker-compose.yml" -printf " %P\n" | sort - done - echo "" - fi - - # Push all at once - if [[ "$PUSH" == "true" && "$DRY_RUN" != "true" ]]; then - push_to_gitea "$OUTPUT_DIR" - elif [[ "$PUSH" != "true" && "$DRY_RUN" != "true" ]]; then - echo -e "${YELLOW}Tip:${NC} Use --push to commit and push to Gitea" - fi -} - -main