Skip to content

Commit c4d5d2e

Browse files
committed
Initial commit
0 parents  commit c4d5d2e

Some content is hidden

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

95 files changed

+6449
-0
lines changed

.github/generate_readme.py

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Script for automatic README generation based on the /docs structure
4+
Analyzes markdown files and creates a README following the existing concept
5+
"""
6+
7+
import os
8+
import re
9+
from pathlib import Path
10+
from typing import Dict, List, Tuple
11+
12+
def is_file_complete(file_path: str) -> bool:
13+
"""
14+
Checks if a markdown file is completely filled out or just a template
15+
"""
16+
try:
17+
with open(file_path, 'r', encoding='utf-8') as f:
18+
content = f.read()
19+
20+
# Check for empty or template indicators
21+
if not content.strip():
22+
return False
23+
24+
# Check for template indicators
25+
template_indicators = [
26+
'## \n**Description:**\n\n**Latest status:**\n\n```\n\n```',
27+
'**Description:**\n\n**Latest status:**\n\n```\n\n```',
28+
'Example |',
29+
'| Name | Type | Description | Example | Required |',
30+
]
31+
32+
# If it only contains template structure, it's not complete
33+
for indicator in template_indicators:
34+
if indicator in content and len(content.strip()) < 500:
35+
return False
36+
37+
# Check for actual API endpoints
38+
if 'GET /' in content or 'POST /' in content or 'DELETE /' in content or 'PUT /' in content:
39+
return True
40+
41+
# Check for filled query parameters or response examples
42+
if ('```json\n{' in content and content.count('```json') > 1) or \
43+
('startAt | number' in content) or \
44+
('"x":' in content and '"y":' in content):
45+
return True
46+
47+
# Check for index/overview pages with meaningful content and links
48+
# These are complete if they have a title, description, and multiple internal links
49+
has_title = bool(re.search(r'^## \w+', content, re.MULTILINE))
50+
has_description = 'Description:' in content and not content.count('Description:') == content.count('**Description:**\n\n**Latest status:**')
51+
has_internal_links = content.count('](/docs/') >= 3 # At least 3 internal documentation links
52+
has_structure = content.count('###') >= 2 # At least 2 subsections
53+
54+
if has_title and (has_description or has_internal_links) and has_structure:
55+
return True
56+
57+
return False
58+
59+
except Exception:
60+
return False
61+
62+
def extract_title_from_file(file_path: str) -> str:
63+
"""
64+
Extracts the title from a markdown file
65+
"""
66+
try:
67+
with open(file_path, 'r', encoding='utf-8') as f:
68+
content = f.read()
69+
70+
# Search for ## Title
71+
match = re.search(r'^## (.+)', content, re.MULTILINE)
72+
if match:
73+
title = match.group(1).strip()
74+
if title and title != '':
75+
return title
76+
except Exception:
77+
pass
78+
79+
# Fallback: Use filename
80+
filename = Path(file_path).stem
81+
return filename.replace('-', ' ').title()
82+
83+
def scan_directory(base_path: str) -> Dict:
84+
"""
85+
Scans a directory recursively and creates a structure of markdown files
86+
"""
87+
structure = {}
88+
docs_path = Path(base_path) / 'docs'
89+
90+
if not docs_path.exists():
91+
print(f"Error: {docs_path} does not exist!")
92+
return structure
93+
94+
for root, dirs, files in os.walk(docs_path):
95+
# Skip template.md
96+
if 'template.md' in files:
97+
files.remove('template.md')
98+
99+
for file in files:
100+
if file.endswith('.md'):
101+
file_path = os.path.join(root, file)
102+
relative_path = os.path.relpath(file_path, docs_path)
103+
104+
# Create structure based on path
105+
parts = relative_path.split(os.sep)
106+
current = structure
107+
108+
for part in parts[:-1]: # All except the file
109+
if part not in current:
110+
current[part] = {}
111+
current = current[part]
112+
113+
# Add the file
114+
filename = parts[-1]
115+
title = extract_title_from_file(file_path)
116+
is_complete = is_file_complete(file_path)
117+
118+
if '_files' not in current:
119+
current['_files'] = []
120+
121+
current['_files'].append({
122+
'filename': filename,
123+
'title': title,
124+
'path': relative_path,
125+
'complete': is_complete
126+
})
127+
128+
return structure
129+
130+
def generate_table_entries(files: List[Dict]) -> List[str]:
131+
"""
132+
Generates table entries for a list of files with links to the documents
133+
"""
134+
entries = []
135+
for file_info in sorted(files, key=lambda x: x['filename']):
136+
state = "✅" if file_info['complete'] else "❌"
137+
title = file_info['title']
138+
# Create relative link to the markdown file
139+
link = f"/docs/{file_info['path']}"
140+
linked_title = f"[{title}]({link})"
141+
entries.append(f"| {state} | {linked_title} |")
142+
143+
return entries
144+
145+
def generate_table_of_contents(structure: Dict) -> str:
146+
"""
147+
Dynamically generates table of contents based on actual structure
148+
"""
149+
toc = ["## Table of Contents", ""]
150+
151+
# Websites section
152+
if 'websites' in structure:
153+
toc.append("- [Websites](#websites)")
154+
websites = structure['websites']
155+
156+
for section_key in sorted(websites.keys()):
157+
if section_key != '_files':
158+
section_name = section_key.title()
159+
toc.append(f" - [{section_name}](#{ section_key.lower()})")
160+
161+
# Add subsections if they exist
162+
section = websites[section_key]
163+
if isinstance(section, dict):
164+
for subsection_key in sorted(section.keys()):
165+
if subsection_key != '_files':
166+
subsection_name = subsection_key.title()
167+
toc.append(f" - [{subsection_name}](#{subsection_key.lower()})")
168+
169+
# Root level sections
170+
for section_key in sorted(structure.keys()):
171+
if section_key not in ['websites'] and section_key != '_files':
172+
section_name = section_key.title()
173+
toc.append(f"- [{section_name}](#{section_key.lower()})")
174+
175+
# Add subsections if they exist
176+
section = structure[section_key]
177+
if isinstance(section, dict):
178+
for subsection_key in sorted(section.keys()):
179+
if subsection_key != '_files':
180+
subsection_name = subsection_key.title()
181+
toc.append(f" - [{subsection_name}](#{subsection_key.lower()})")
182+
183+
return '\n'.join(toc) + '\n\n'
184+
185+
def generate_section_content(section_name: str, section_data: Dict, level: int = 3) -> str:
186+
"""
187+
Recursively generates content for any section
188+
"""
189+
content = ""
190+
heading = "#" * level
191+
192+
content += f"{heading} {section_name.title()}\n"
193+
194+
# Add files in this section
195+
if '_files' in section_data and section_data['_files']:
196+
content += "| State | Name |\n"
197+
content += "| :---: | :--- |\n"
198+
entries = generate_table_entries(section_data['_files'])
199+
content += '\n'.join(entries) + '\n'
200+
else:
201+
# Check if there are subsections
202+
has_subsections = any(key != '_files' and isinstance(section_data[key], dict)
203+
for key in section_data.keys())
204+
205+
if not has_subsections:
206+
# No files and no subsections, add empty placeholder
207+
content += "| State | Name |\n"
208+
content += "| :---: | :--- |\n"
209+
content += "| | |\n"
210+
211+
content += "\n"
212+
213+
# Process subsections
214+
for subsection_key in sorted(section_data.keys()):
215+
if subsection_key != '_files' and isinstance(section_data[subsection_key], dict):
216+
content += generate_section_content(subsection_key, section_data[subsection_key], level + 1)
217+
218+
return content
219+
220+
def generate_readme_content(structure: Dict) -> str:
221+
"""
222+
Generates README content based on the structure - now completely dynamic
223+
"""
224+
content = "# umami API docs\n\n"
225+
226+
# Add status legend
227+
content += "### 📊 Documentation Status Legend\n"
228+
content += "| Symbol | Meaning |\n"
229+
content += "| :---: | :--- |\n"
230+
content += "| ✅ | Documentation complete – file contains full API specification |\n"
231+
content += "| ❌ | Documentation incomplete – file is template or missing content |\n\n"
232+
233+
# Generate dynamic table of contents
234+
content += generate_table_of_contents(structure)
235+
236+
content += "---\n---\n\n"
237+
# Generate Websites section
238+
if 'websites' in structure:
239+
content += "## Websites\n\n"
240+
websites_content = generate_section_content("websites", structure['websites'], level=2)
241+
# Remove the first line (## Websites) since we already added it
242+
content += '\n'.join(websites_content.split('\n')[1:])
243+
244+
content += "--- \n\n"
245+
246+
# Generate all other root-level sections dynamically
247+
for section_key in sorted(structure.keys()):
248+
if section_key not in ['websites'] and section_key != '_files':
249+
content += generate_section_content(section_key, structure[section_key], level=2)
250+
content += "---\n\n"
251+
252+
return content.rstrip() + "\n"
253+
254+
def main():
255+
"""
256+
Main function of the script
257+
"""
258+
# Get the parent directory of .github (the repository root)
259+
base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
260+
261+
print("🔍 Scanning /docs directory...")
262+
structure = scan_directory(base_path)
263+
264+
print("📝 Generating README content...")
265+
readme_content = generate_readme_content(structure)
266+
267+
# Write new README
268+
readme_path = os.path.join(base_path, 'README.md')
269+
270+
print(f"💾 Writing README to {readme_path}...")
271+
with open(readme_path, 'w', encoding='utf-8') as f:
272+
f.write(readme_content)
273+
274+
print("✅ README successfully generated!")
275+
276+
# Output statistics
277+
total_files = 0
278+
complete_files = 0
279+
280+
def count_files(struct):
281+
nonlocal total_files, complete_files
282+
if isinstance(struct, dict):
283+
if '_files' in struct:
284+
for file_info in struct['_files']:
285+
total_files += 1
286+
if file_info['complete']:
287+
complete_files += 1
288+
for key, value in struct.items():
289+
if key != '_files':
290+
count_files(value)
291+
292+
count_files(structure)
293+
294+
print(f"\n📊 Statistics:")
295+
print(f" Total files found: {total_files}")
296+
print(f" Completely filled: {complete_files}")
297+
print(f" Still to be processed: {total_files - complete_files}")
298+
print(f" Progress: {(complete_files/total_files*100):.1f}%" if total_files > 0 else " Progress: 0%")
299+
300+
if __name__ == "__main__":
301+
main()
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# README Generation
2+
3+
This repository uses automated README generation based on the documentation structure in the `/docs` folder.
4+
5+
## How it works
6+
7+
1. **GitHub Workflows** automatically generate and commit README.md on every push
8+
2. **README.md** is auto-generated - no manual editing needed
9+
3. **Generated README** is directly committed to the repository for visibility
10+
11+
## Manual Generation
12+
13+
To generate the README locally:
14+
15+
```bash
16+
python3 generate_readme.py
17+
```
18+
19+
## Workflows
20+
21+
### 1. `generate-readme.yml`
22+
- **Triggers**: Push to main branches, PR, manual dispatch
23+
- **Purpose**: Simple README generation
24+
- **Output**: README.md as artifact
25+
26+
### 2. `documentation-update.yml`
27+
- **Triggers**: Changes to docs/ or generate_readme.py
28+
- **Purpose**: Enhanced documentation workflow with validation
29+
- **Features**:
30+
- Content validation
31+
- Progress statistics
32+
- Detailed summaries
33+
- Automatic commit with detailed messages
34+
35+
## Viewing Generated README
36+
37+
The README.md is automatically committed to the repository and visible directly on GitHub!
38+
39+
## Benefits
40+
41+
- ✅ Always up-to-date documentation overview
42+
- ✅ Automatic linking to documentation files
43+
- ✅ Progress tracking (✅/❌ status indicators)
44+
- ✅ No manual README maintenance needed
45+
- ✅ Consistent formatting and structure

0 commit comments

Comments
 (0)