|
1 | 1 | #!/bin/bash |
2 | | -set -e |
3 | 2 |
|
| 3 | +# Script to generate HTML documentation from all API specs using Redocly |
| 4 | +# This will convert all .yaml and .yml files in the specs directory to HTML |
| 5 | + |
| 6 | +set -ex |
| 7 | +set -o pipefail |
| 8 | + |
| 9 | +# Colors for output |
| 10 | +RED='\033[0;31m' |
| 11 | +GREEN='\033[0;32m' |
| 12 | +YELLOW='\033[1;33m' |
| 13 | +BLUE='\033[0;34m' |
| 14 | +NC='\033[0m' # No Color |
| 15 | + |
| 16 | +# Directories |
| 17 | +SPECS_DIR="specs" |
4 | 18 | OUTPUT_DIR="docs/api-docs" |
5 | 19 | INDEX_FILE="$OUTPUT_DIR/index.html" |
| 20 | +ERROR_LOG="$OUTPUT_DIR/errors.log" |
| 21 | + |
| 22 | +echo -e "${BLUE}🚀 Starting API documentation generation...${NC}" |
6 | 23 |
|
7 | | -# 1. Clean output dir |
| 24 | +# === CLEAN OUTPUT DIRECTORY === |
| 25 | +echo -e "${YELLOW}🧹 Cleaning output folder...${NC}" |
8 | 26 | rm -rf "$OUTPUT_DIR" |
9 | 27 | mkdir -p "$OUTPUT_DIR" |
10 | 28 |
|
11 | | -# 2. Prepare index.html |
| 29 | +# Check if redocly is installed |
| 30 | +if ! command -v redocly &> /dev/null; then |
| 31 | + echo -e "${RED}❌ Redocly is not installed. Please install it first:${NC}" |
| 32 | + echo "npm install -g @redocly/cli" |
| 33 | + exit 1 |
| 34 | +fi |
| 35 | +echo -e "${GREEN}✅ Redocly found: $(redocly --version)${NC}" |
| 36 | + |
| 37 | +# Counters |
| 38 | +success_count=0 |
| 39 | +error_count=0 |
| 40 | + |
| 41 | +# Clear error log |
| 42 | +> "$ERROR_LOG" |
| 43 | + |
| 44 | +# Function to convert a spec file to HTML |
| 45 | +convert_spec_to_html() { |
| 46 | + local spec_file="$1" |
| 47 | + local relative_path="${spec_file#$SPECS_DIR/}" |
| 48 | + local filename=$(basename "$spec_file") |
| 49 | + local name_without_ext="${filename%.*}" |
| 50 | + local output_file="$OUTPUT_DIR/${relative_path%.*}.html" |
| 51 | + |
| 52 | + # Create output directory if it doesn't exist |
| 53 | + mkdir -p "$(dirname "$output_file")" |
| 54 | + |
| 55 | + echo -e "${BLUE}📄 Converting: $spec_file${NC}" |
| 56 | + |
| 57 | + if redocly build-docs "$spec_file" -o "$output_file" >/dev/null 2>&1; then |
| 58 | + echo -e "${GREEN}✅ Success: $output_file${NC}" |
| 59 | + ((success_count++)) |
| 60 | + return 0 |
| 61 | + else |
| 62 | + echo -e "${RED}❌ Failed: $spec_file${NC}" |
| 63 | + echo "$spec_file" >> "$ERROR_LOG" |
| 64 | + ((error_count++)) |
| 65 | + return 1 |
| 66 | + fi |
| 67 | +} |
| 68 | + |
| 69 | +# === FIND AND CONVERT SPEC FILES === |
| 70 | +echo -e "${YELLOW}🔍 Finding all spec files...${NC}" |
| 71 | +mapfile -t spec_files < <(find "$SPECS_DIR" -type f \( -name "*.yaml" -o -name "*.yml" \) | sort) |
| 72 | +echo -e "${BLUE}📊 Found ${#spec_files[@]} spec files${NC}" |
| 73 | + |
| 74 | +for spec_file in "${spec_files[@]}"; do |
| 75 | + convert_spec_to_html "$spec_file" || true |
| 76 | +done |
| 77 | + |
| 78 | +# === GENERATE INDEX.HTML === |
| 79 | +echo -e "${YELLOW}📝 Generating index page...${NC}" |
| 80 | + |
12 | 81 | cat > "$INDEX_FILE" << 'EOF' |
13 | 82 | <!DOCTYPE html> |
14 | 83 | <html lang="en"> |
15 | 84 | <head> |
16 | | - <meta charset="UTF-8"> |
17 | | - <title>API Documentation</title> |
18 | | - <style> |
19 | | - body { font-family: Arial, sans-serif; margin: 20px; } |
20 | | - h1 { border-bottom: 2px solid #ccc; padding-bottom: 10px; } |
21 | | - .category { margin-bottom: 20px; } |
22 | | - .category h3 { margin-top: 0; color: #2c3e50; } |
23 | | - .api-list { list-style: none; padding-left: 0; } |
24 | | - .api-list li { margin: 5px 0; } |
25 | | - .timestamp { margin-top: 40px; font-size: 0.9em; color: #888; } |
26 | | - </style> |
| 85 | +<meta charset="UTF-8"> |
| 86 | +<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| 87 | +<title>Devtron API Documentation</title> |
| 88 | +<style> |
| 89 | +body { font-family: Arial, sans-serif; margin: 20px; } |
| 90 | +h1 { color: #333; } |
| 91 | +h3 { margin-top: 20px; } |
| 92 | +ul { list-style: none; padding-left: 0; } |
| 93 | +li { margin: 5px 0; } |
| 94 | +a { text-decoration: none; color: #0366d6; } |
| 95 | +a:hover { text-decoration: underline; } |
| 96 | +</style> |
27 | 97 | </head> |
28 | 98 | <body> |
29 | | - <h1>API Documentation</h1> |
30 | | - <div id="categories"></div> |
31 | | - <div class="timestamp">Last updated: <span id="timestamp"></span></div> |
32 | | - <script> |
33 | | - const apiData = { |
| 99 | +<div class="container"> |
| 100 | +<h1>🚀 Devtron API Documentation</h1> |
| 101 | +<div class="categories" id="categories"></div> |
| 102 | +<div class="footer"> |
| 103 | +<p><a href="https://devtron.ai/" target="_blank">Devtron</a></p> |
| 104 | +<p class="timestamp">Last updated: <span id="timestamp"></span></p> |
| 105 | +</div> |
| 106 | +</div> |
| 107 | +<script> |
| 108 | +const apiData = { |
34 | 109 | EOF |
35 | 110 |
|
36 | | -# 3. Loop over all specs |
37 | | -find specs -type f -name "*.yaml" | while read -r spec; do |
38 | | - title=$(grep -m1 "^title:" "$spec" | sed 's/^[[:space:]]*title:[[:space:]]*//' | tr -d '"') |
39 | | - filename=$(basename "$spec" .yaml).html |
40 | | - category=$(basename "$(dirname "$spec")") |
41 | | - |
42 | | - # Run redocly to generate HTML |
43 | | - npx @redocly/cli build-docs "$spec" -o "$OUTPUT_DIR/$filename" |
44 | | - |
45 | | - # Add entry to index.html |
46 | | - cat >> "$INDEX_FILE" << EOF |
47 | | - "${filename}": { |
48 | | - "title": "${title}", |
49 | | - "category": "${category}", |
50 | | - "filename": "${filename}" |
51 | | - }, |
52 | | -EOF |
| 111 | +# Populate apiData |
| 112 | +for spec_file in "${spec_files[@]}"; do |
| 113 | + relative_path="${spec_file#$SPECS_DIR/}" |
| 114 | + html_file="${relative_path%.*}.html" |
| 115 | + category=$(dirname "$relative_path") |
| 116 | + [[ "$category" == "." ]] && category="root" |
| 117 | + |
| 118 | + # Capitalise each word and split camelCase |
| 119 | + display_category=$(echo "$category" | sed 's/[-_]/ /g' | sed 's/\([a-z]\)\([A-Z]\)/\1 \2/g' | sed 's/\b\w/\U&/g') |
| 120 | + |
| 121 | + # Get title or fallback |
| 122 | + title=$(grep -m 1 '^[[:space:]]*title:' "$spec_file" | sed 's/^[[:space:]]*title:[[:space:]]*//' | tr -d '"' || echo "${relative_path%.*}") |
| 123 | + |
| 124 | + if [[ -f "$OUTPUT_DIR/$html_file" ]]; then |
| 125 | + echo " \"${category}_$(basename "${relative_path%.*}")\": {\"category\": \"${display_category}\", \"title\": \"${title}\", \"filename\": \"${html_file}\"}," >> "$INDEX_FILE" |
| 126 | + fi |
53 | 127 | done |
54 | 128 |
|
55 | | -# 4. Remove last comma in apiData |
| 129 | +# Remove trailing comma |
56 | 130 | sed -i '$ s/,$//' "$INDEX_FILE" |
57 | 131 |
|
58 | | -# 5. Append JS for rendering categories & APIs |
59 | 132 | cat >> "$INDEX_FILE" << 'EOF' |
60 | | - }; |
61 | | -
|
62 | | - function populatePage() { |
63 | | - const categoriesContainer = document.getElementById('categories'); |
64 | | - const categories = {}; |
65 | | -
|
66 | | - Object.values(apiData).forEach(api => { |
67 | | - if (!categories[api.category]) { |
68 | | - categories[api.category] = []; |
69 | | - } |
70 | | - categories[api.category].push(api); |
71 | | - }); |
72 | | -
|
73 | | - Object.keys(categories).sort().forEach(category => { |
74 | | - const categoryDiv = document.createElement('div'); |
75 | | - categoryDiv.className = 'category'; |
76 | | -
|
77 | | - const categoryTitle = document.createElement('h3'); |
78 | | - categoryTitle.textContent = category |
79 | | - .replace(/([a-z])([A-Z])/g, '$1 $2') // camelCase -> space |
80 | | - .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2') // ABCThing -> ABC Thing |
81 | | - .split(/[\s-_]+/) // split on space, dash, underscore |
82 | | - .map(word => { |
83 | | - if (word === word.toUpperCase()) return word; // keep acronyms uppercase |
84 | | - return word.charAt(0).toUpperCase() + word.slice(1); |
85 | | - }) |
86 | | - .join(' '); |
87 | | -
|
88 | | - categoryDiv.appendChild(categoryTitle); |
89 | | -
|
90 | | - const apiList = document.createElement('ul'); |
91 | | - apiList.className = 'api-list'; |
92 | | -
|
93 | | - categories[category] |
94 | | - .sort((a, b) => a.title.localeCompare(b.title)) |
95 | | - .forEach(api => { |
96 | | - const listItem = document.createElement('li'); |
97 | | - const link = document.createElement('a'); |
98 | | - link.href = api.filename; // removed target="_blank" |
99 | | - link.textContent = api.title; |
100 | | - listItem.appendChild(link); |
101 | | - apiList.appendChild(listItem); |
102 | | - }); |
103 | | -
|
104 | | - categoryDiv.appendChild(apiList); |
105 | | - categoriesContainer.appendChild(categoryDiv); |
106 | | - }); |
107 | | -
|
108 | | - document.getElementById('timestamp').textContent = new Date().toLocaleString(); |
109 | | - } |
110 | | -
|
111 | | - document.addEventListener('DOMContentLoaded', populatePage); |
112 | | - </script> |
| 133 | +}; |
| 134 | +
|
| 135 | +function populatePage() { |
| 136 | + const container = document.getElementById('categories'); |
| 137 | + const categories = {}; |
| 138 | +
|
| 139 | + Object.values(apiData).forEach(api => { |
| 140 | + if (!categories[api.category]) categories[api.category] = []; |
| 141 | + categories[api.category].push(api); |
| 142 | + }); |
| 143 | +
|
| 144 | + Object.keys(categories).sort().forEach(cat => { |
| 145 | + const section = document.createElement('div'); |
| 146 | + const h3 = document.createElement('h3'); |
| 147 | + h3.textContent = cat; |
| 148 | + section.appendChild(h3); |
| 149 | +
|
| 150 | + const ul = document.createElement('ul'); |
| 151 | + categories[cat].sort((a,b)=>a.title.localeCompare(b.title)).forEach(api => { |
| 152 | + const li = document.createElement('li'); |
| 153 | + const a = document.createElement('a'); |
| 154 | + a.href = api.filename; |
| 155 | + a.textContent = api.title; |
| 156 | + li.appendChild(a); |
| 157 | + ul.appendChild(li); |
| 158 | + }); |
| 159 | +
|
| 160 | + section.appendChild(ul); |
| 161 | + container.appendChild(section); |
| 162 | + }); |
| 163 | +
|
| 164 | + document.getElementById('timestamp').textContent = new Date().toLocaleString(); |
| 165 | +} |
| 166 | +
|
| 167 | +document.addEventListener('DOMContentLoaded', populatePage); |
| 168 | +</script> |
113 | 169 | </body> |
114 | 170 | </html> |
115 | 171 | EOF |
116 | 172 |
|
117 | | -echo "✅ API documentation generated at $OUTPUT_DIR" |
| 173 | +echo -e "${GREEN}✅ Index page generated: $INDEX_FILE${NC}" |
| 174 | + |
| 175 | +# === FINAL SUMMARY === |
| 176 | +echo -e "${BLUE}📊 Final Summary:${NC}" |
| 177 | +echo -e "${GREEN}✅ Successfully converted: $success_count files${NC}" |
| 178 | +if [ $error_count -gt 0 ]; then |
| 179 | + echo -e "${RED}❌ Failed to convert: $error_count files${NC}" |
| 180 | + echo -e "${YELLOW}📝 Check $ERROR_LOG for details${NC}" |
| 181 | +fi |
| 182 | +echo -e "${BLUE}📁 Output directory: $OUTPUT_DIR${NC}" |
| 183 | +echo -e "${BLUE}🌐 Main index: $INDEX_FILE${NC}" |
| 184 | + |
| 185 | +# === CREATE README === |
| 186 | +cat > "$OUTPUT_DIR/README.md" << 'EOF' |
| 187 | +# Devtron API Documentation |
| 188 | +This folder contains the HTML documentation generated from the OpenAPI specs in the `specs` directory. |
| 189 | +EOF |
| 190 | + |
| 191 | +echo -e "${GREEN}✅ README created: $OUTPUT_DIR/README.md${NC}" |
| 192 | +echo -e "${GREEN}🎉 API documentation generation complete!${NC}" |
0 commit comments