Skip to content

Commit 2f0955a

Browse files
committed
feat: api spec hosting mechanism
1 parent fdcab1b commit 2f0955a

File tree

131 files changed

+20922
-19153
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

131 files changed

+20922
-19153
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
name: API Specs Merger
2+
3+
on:
4+
push:
5+
paths:
6+
- 'specs/**'
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
lint-bundle-host:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Checkout repository
16+
uses: actions/checkout@v4
17+
with:
18+
ref: ${{ github.ref }}
19+
20+
- name: Set up Redocly CLI
21+
run: npm install -g @redocly/cli
22+
23+
- name: Run specs bundling script
24+
working-directory: ${{ github.workspace }}
25+
run: |
26+
chmod +x scripts/generate-api-docs.sh
27+
./scripts/generate-api-docs.sh
28+
29+
- name: Commit and push documentation
30+
env:
31+
GITHUB_TOKEN: ${{ secrets.GH_SYSTEMSDT_TOKEN }}
32+
run: |
33+
git config --local user.email "[email protected]"
34+
git config --local user.name "GitHub Action"
35+
36+
# Fetch the gh-pages branch
37+
git fetch origin gh-pages
38+
39+
# Create a separate worktree for the branch
40+
git worktree add /tmp/gh-pages gh-pages
41+
42+
# Copy generated docs into the worktree
43+
mkdir -p /tmp/gh-pages/docs/api-docs
44+
cp -r docs/api-docs/* /tmp/gh-pages/docs/api-docs/ || true
45+
46+
cd /tmp/gh-pages
47+
48+
git add .
49+
if git diff --staged --quiet; then
50+
echo "✅ No changes to commit"
51+
else
52+
git commit -m "📄 Update API documentation"
53+
git push origin gh-pages
54+
echo "🚀 Documentation pushed to gh-pages branch!"
55+
fi
56+

scripts/generate-api-docs.sh

Lines changed: 325 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,325 @@
1+
#!/bin/bash
2+
3+
# Script to generate HTML documentation from all API specs using Redocly
4+
# Preserves folder structure and generates a card-based index page
5+
6+
set -ex
7+
set -o pipefail
8+
9+
# Colors
10+
RED='\033[0;31m'
11+
GREEN='\033[0;32m'
12+
YELLOW='\033[1;33m'
13+
BLUE='\033[0;34m'
14+
NC='\033[0m'
15+
16+
# Directories
17+
SPECS_DIR="specs"
18+
OUTPUT_DIR="docs/api-docs"
19+
INDEX_FILE="$OUTPUT_DIR/index.html"
20+
ERROR_LOG="$OUTPUT_DIR/errors.log"
21+
22+
echo -e "${BLUE}🚀 Starting API documentation generation...${NC}"
23+
24+
# Clean output folder
25+
rm -rf "$OUTPUT_DIR"
26+
mkdir -p "$OUTPUT_DIR"
27+
28+
# Check Redocly
29+
if ! command -v redocly &>/dev/null; then
30+
echo -e "${RED}❌ Redocly CLI not found. Install it:${NC}"
31+
echo "npm install -g @redocly/cli"
32+
exit 1
33+
fi
34+
echo -e "${GREEN}✅ Redocly found: $(redocly --version)${NC}"
35+
36+
success_count=0
37+
error_count=0
38+
> "$ERROR_LOG"
39+
40+
41+
convert_spec_to_html() {
42+
local spec_file="$1"
43+
local relative_path="${spec_file#$SPECS_DIR/}"
44+
local output_file="$OUTPUT_DIR/${relative_path%.*}.html"
45+
mkdir -p "$(dirname "$output_file")"
46+
47+
echo -e "${BLUE}📄 Converting: $spec_file${NC}"
48+
echo -e "${BLUE} → Output: ${relative_path%.*}.html${NC}"
49+
50+
if redocly build-docs "$spec_file" -o "$output_file" >/dev/null 2>&1; then
51+
echo -e "${GREEN}✅ Success: $output_file${NC}"
52+
((success_count++))
53+
else
54+
echo -e "${RED}❌ Failed: $spec_file${NC}"
55+
echo "$spec_file" >> "$ERROR_LOG"
56+
((error_count++))
57+
fi
58+
}
59+
60+
# Find spec files
61+
mapfile -t spec_files < <(find "$SPECS_DIR" -type f \( -name "*.yaml" -o -name "*.yml" \) | sort)
62+
echo -e "${BLUE}📊 Found ${#spec_files[@]} specs${NC}"
63+
64+
# Convert specs
65+
for spec_file in "${spec_files[@]}"; do
66+
convert_spec_to_html "$spec_file" || true
67+
done
68+
69+
70+
71+
# Generate index.html
72+
cat > "$INDEX_FILE" << 'EOF'
73+
<!DOCTYPE html>
74+
<html lang="en">
75+
<head>
76+
<meta charset="UTF-8">
77+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
78+
<title>Devtron API Documentation</title>
79+
<style>
80+
body { font-family: Arial, sans-serif; margin: 20px; background: #f8f9fa; color: #333; }
81+
h1 { text-align: center; color: #2c3e50; margin-bottom: 40px; }
82+
.container { max-width: 1200px; margin: auto; }
83+
.categories-grid {
84+
display: flex;
85+
flex-wrap: wrap;
86+
justify-content: center;
87+
gap: 30px;
88+
margin-top: 20px;
89+
align-items: stretch; /* Ensures all cards stretch to same height */
90+
}
91+
92+
/* Single card centering */
93+
.categories-grid.single-card {
94+
justify-content: center;
95+
max-width: 400px;
96+
margin: 20px auto;
97+
}
98+
99+
/* Category Cards */
100+
.category-card {
101+
background: #fff;
102+
border-radius: 12px;
103+
padding: 25px;
104+
width: calc(33.33% - 30px);
105+
min-width: 300px;
106+
max-width: 400px;
107+
height: auto;
108+
min-height: 300px; /* Minimum height for consistency */
109+
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
110+
border: 1px solid #e1e5e9;
111+
transition: transform 0.2s ease, box-shadow 0.2s ease;
112+
display: flex;
113+
flex-direction: column;
114+
}
115+
.category-card:hover {
116+
transform: translateY(-2px);
117+
box-shadow: 0 6px 20px rgba(0,0,0,0.15);
118+
}
119+
120+
/* Single card styling */
121+
.category-card.single {
122+
width: 100%;
123+
max-width: 400px;
124+
}
125+
126+
/* Category Headers */
127+
.category-header {
128+
color: #2c3e50;
129+
font-size: 1.4em;
130+
font-weight: bold;
131+
margin-bottom: 15px;
132+
padding-bottom: 10px;
133+
border-bottom: 2px solid #3498db;
134+
text-align: center;
135+
}
136+
137+
/* API Links within Categories */
138+
.api-links {
139+
display: flex;
140+
flex-direction: column;
141+
gap: 8px;
142+
flex-grow: 1; /* Takes up remaining space in the card */
143+
overflow-y: auto; /* Allows scrolling if too many links */
144+
max-height: 400px; /* Maximum height before scrolling */
145+
}
146+
.api-link {
147+
display: block;
148+
padding: 8px 12px;
149+
background: #f8f9fa;
150+
border-radius: 6px;
151+
text-decoration: none;
152+
color: #1a73e8;
153+
font-weight: 500;
154+
transition: all 0.2s ease;
155+
border-left: 3px solid transparent;
156+
}
157+
.api-link:hover {
158+
background: #e3f2fd;
159+
border-left-color: #1a73e8;
160+
text-decoration: none;
161+
transform: translateX(5px);
162+
}
163+
164+
/* Footer */
165+
.footer {
166+
margin-top: 50px;
167+
font-size: 0.9rem;
168+
color: #666;
169+
text-align: center;
170+
padding-top: 20px;
171+
border-top: 1px solid #e1e5e9;
172+
}
173+
.footer a { color: #1a73e8; text-decoration: none; }
174+
.footer a:hover { text-decoration: underline; }
175+
.timestamp { font-style: italic; }
176+
177+
/* Responsive Design */
178+
@media(max-width: 1024px){
179+
.category-card { width: calc(50% - 30px); }
180+
.categories-grid.single-card { max-width: 500px; }
181+
}
182+
@media(max-width: 768px){
183+
.category-card { width: 100%; min-width: unset; max-width: none; }
184+
.categories-grid.single-card { max-width: 100%; margin: 20px 0; }
185+
}
186+
@media(max-width: 480px){
187+
.category-card { margin: 0 10px; }
188+
.categories-grid { gap: 20px; }
189+
}
190+
</style>
191+
</head>
192+
<body>
193+
<div class="container">
194+
<h1> Devtron API Documentation</h1>
195+
<div id="categories" class="categories-grid"></div>
196+
<div class="footer">
197+
<p><a href="https://devtron.ai/" target="_blank">Devtron</a></p>
198+
<p class="timestamp">Last updated: <span id="timestamp"></span></p>
199+
</div>
200+
</div>
201+
<script>
202+
const apiData = {
203+
EOF
204+
205+
206+
207+
# Populate apiData preserving folder structure
208+
for spec_file in "${spec_files[@]}"; do
209+
relative_path="${spec_file#$SPECS_DIR/}"
210+
html_file="${relative_path%.*}.html"
211+
category=$(dirname "$relative_path")
212+
[[ "$category" == "." ]] && category="Root"
213+
214+
display_category=$(echo "$category" | sed 's/[-_]/ /g' | sed 's/\([a-z]\)\([A-Z]\)/\1 \2/g' | sed 's/\b\w/\U&/g')
215+
title=$(grep -m 1 '^[[:space:]]*title:' "$spec_file" | sed 's/^[[:space:]]*title:[[:space:]]*//' | tr -d '"' || echo "${relative_path%.*}")
216+
217+
# Only include if HTML file was successfully generated
218+
if [[ -f "$OUTPUT_DIR/$html_file" ]]; then
219+
# Ensure proper relative path from index.html to the generated HTML file
220+
# Since index.html is in docs/api-docs/ and HTML files maintain folder structure
221+
echo " \"${category}_$(basename "${relative_path%.*}")\": {\"category\": \"${display_category}\", \"title\": \"${title}\", \"filename\": \"${html_file}\"}," >> "$INDEX_FILE"
222+
fi
223+
done
224+
225+
sed -i '$ s/,$//' "$INDEX_FILE"
226+
227+
228+
229+
cat >> "$INDEX_FILE" << 'EOF'
230+
};
231+
232+
function populatePage() {
233+
const container = document.getElementById('categories');
234+
const categories = {};
235+
236+
// Group APIs by category
237+
Object.values(apiData).forEach(api => {
238+
if (!categories[api.category]) categories[api.category] = [];
239+
categories[api.category].push(api);
240+
});
241+
242+
const categoryNames = Object.keys(categories).sort();
243+
244+
// Add class for single card centering
245+
if (categoryNames.length === 1) {
246+
container.classList.add('single-card');
247+
}
248+
249+
// Create category cards
250+
categoryNames.forEach(categoryName => {
251+
// Create category card
252+
const categoryCard = document.createElement('div');
253+
categoryCard.className = 'category-card';
254+
255+
// Add single class if only one card
256+
if (categoryNames.length === 1) {
257+
categoryCard.classList.add('single');
258+
}
259+
260+
// Create category header
261+
const categoryHeader = document.createElement('div');
262+
categoryHeader.className = 'category-header';
263+
categoryHeader.textContent = categoryName;
264+
categoryCard.appendChild(categoryHeader);
265+
266+
// Create links container
267+
const linksContainer = document.createElement('div');
268+
linksContainer.className = 'api-links';
269+
270+
// Add API links to this category
271+
categories[categoryName]
272+
.sort((a, b) => a.title.localeCompare(b.title))
273+
.forEach(api => {
274+
const apiLink = document.createElement('a');
275+
// Ensure proper relative path
276+
apiLink.href = api.filename;
277+
apiLink.textContent = api.title;
278+
apiLink.className = 'api-link';
279+
apiLink.title = `View ${api.title} API documentation`;
280+
281+
// Add click handler to check if file exists
282+
apiLink.addEventListener('click', function(e) {
283+
// Let the browser handle the navigation normally
284+
// This is just for debugging - remove in production if needed
285+
console.log(`Navigating to: ${api.filename}`);
286+
});
287+
288+
linksContainer.appendChild(apiLink);
289+
});
290+
291+
categoryCard.appendChild(linksContainer);
292+
container.appendChild(categoryCard);
293+
});
294+
295+
document.getElementById('timestamp').textContent = new Date().toLocaleString();
296+
}
297+
298+
document.addEventListener('DOMContentLoaded', populatePage);
299+
</script>
300+
</body>
301+
</html>
302+
EOF
303+
304+
305+
306+
echo -e "${GREEN}✅ Card-based index page generated: $INDEX_FILE${NC}"
307+
308+
# === SUMMARY ===
309+
echo -e "${BLUE}📊 Final Summary:${NC}"
310+
echo -e "${GREEN}✅ Successfully converted: $success_count specs${NC}"
311+
if (( error_count > 0 )); then
312+
echo -e "${RED}❌ Failed: $error_count (see $ERROR_LOG)${NC}"
313+
fi
314+
echo -e "${BLUE}📁 Output directory: $OUTPUT_DIR${NC}"
315+
echo -e "${BLUE}🌐 Main index: $INDEX_FILE${NC}"
316+
317+
# === CREATE README ===
318+
cat > "$OUTPUT_DIR/README.md" << 'EOF'
319+
# Devtron API Documentation
320+
321+
This folder contains the HTML documentation generated from the OpenAPI specs in the `specs` directory.
322+
EOF
323+
324+
echo -e "${GREEN}✅ README created: $OUTPUT_DIR/README.md${NC}"
325+
echo -e "${GREEN}🎉 API documentation generation complete!${NC}"

0 commit comments

Comments
 (0)