ci: Add GitHub Actions workflow for documentation checks
This commit is contained in:
114
.cspell.json
Normal file
114
.cspell.json
Normal file
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"version": "0.2",
|
||||
"language": "en",
|
||||
"words": [
|
||||
"claude",
|
||||
"Claude",
|
||||
"Anthropic",
|
||||
"subagent",
|
||||
"subagents",
|
||||
"CLAUDE",
|
||||
"MCP",
|
||||
"Mermaid",
|
||||
"frontmatter",
|
||||
"pre-commit",
|
||||
"Ruff",
|
||||
"Bandit",
|
||||
"EPUB",
|
||||
"Sonnet",
|
||||
"Opus",
|
||||
"Haiku",
|
||||
"luongnv89",
|
||||
"claude-howto",
|
||||
"ModelContextProtocol",
|
||||
"YAML",
|
||||
"JSON",
|
||||
"API",
|
||||
"DevOps",
|
||||
"CRUD",
|
||||
"CLI",
|
||||
"SSH",
|
||||
"HTTP",
|
||||
"HTTPS",
|
||||
"AWS",
|
||||
"GitHub",
|
||||
"GitLab",
|
||||
"MongoDB",
|
||||
"PostgreSQL",
|
||||
"SQLite",
|
||||
"Redis",
|
||||
"JWT",
|
||||
"OAuth",
|
||||
"CORS",
|
||||
"REST",
|
||||
"GraphQL",
|
||||
"TypeScript",
|
||||
"JavaScript",
|
||||
"Python",
|
||||
"Bash",
|
||||
"Unix",
|
||||
"Linux",
|
||||
"macOS",
|
||||
"Windows",
|
||||
"PowerShell",
|
||||
"VSCode",
|
||||
"IDE",
|
||||
"MCP",
|
||||
"Checks",
|
||||
"checkpoints",
|
||||
"THINKING",
|
||||
"Headless",
|
||||
"Headless mode",
|
||||
"Kyrie",
|
||||
"subprocesses",
|
||||
"subprocess",
|
||||
"checklist",
|
||||
"checklists",
|
||||
"customizable",
|
||||
"customization",
|
||||
"delegated",
|
||||
"delegating",
|
||||
"permissioning",
|
||||
"GOPATH",
|
||||
"goToDefinition",
|
||||
"outgoingCalls",
|
||||
"incomingCalls",
|
||||
"docstring",
|
||||
"docstrings",
|
||||
"EOSMTP",
|
||||
"OPENSPEC",
|
||||
"Markdown",
|
||||
"realtime",
|
||||
"real-time",
|
||||
"refactoring",
|
||||
"runtime",
|
||||
"namespace",
|
||||
"namespacing",
|
||||
"linting",
|
||||
"pytest",
|
||||
"Pytest",
|
||||
"telemetry",
|
||||
"caching",
|
||||
"HTTPS",
|
||||
"repo",
|
||||
"repos",
|
||||
"readme",
|
||||
"README"
|
||||
],
|
||||
"ignoreWords": [
|
||||
"luongnv89"
|
||||
],
|
||||
"ignoreRegExpList": [
|
||||
"code\\{[^}]*\\}",
|
||||
"```[\\s\\S]*?```"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"filename": "**/*.md",
|
||||
"words": [
|
||||
"commandline",
|
||||
"cmd"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
27
.github/markdown-link-check-config.json
vendored
Normal file
27
.github/markdown-link-check-config.json
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"ignorePatterns": [
|
||||
{
|
||||
"pattern": "^https://twitter.com",
|
||||
"comment": "Twitter/X links require authentication"
|
||||
},
|
||||
{
|
||||
"pattern": "^https://x.com",
|
||||
"comment": "X (Twitter) links require authentication"
|
||||
},
|
||||
{
|
||||
"pattern": "^https://github.com/[^/]+/claude-howto",
|
||||
"comment": "Self-referencing links"
|
||||
}
|
||||
],
|
||||
"replacementPatterns": [
|
||||
{
|
||||
"pattern": "^/",
|
||||
"replacement": "https://github.com/luongnv89/claude-howto/blob/main/"
|
||||
}
|
||||
],
|
||||
"timeout": 10000,
|
||||
"retryOn": [429, 503],
|
||||
"retryCount": 3,
|
||||
"fallbackRetryDelay": 5000,
|
||||
"aliveStatusCodes": [200, 206]
|
||||
}
|
||||
340
.github/workflows/docs-check.yml
vendored
Normal file
340
.github/workflows/docs-check.yml
vendored
Normal file
@@ -0,0 +1,340 @@
|
||||
name: Documentation Checks
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- '**.md'
|
||||
- '.github/workflows/docs-check.yml'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- '**.md'
|
||||
- 'CONTRIBUTING.md'
|
||||
- 'LICENSE'
|
||||
|
||||
# Cancel in-progress runs for the same branch
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
markdown-lint:
|
||||
name: Markdown Linting
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Install markdownlint-cli
|
||||
run: npm install -g markdownlint-cli
|
||||
|
||||
- name: Run Markdown Linter
|
||||
run: markdownlint '**/*.md' --ignore node_modules --ignore .venv
|
||||
continue-on-error: true
|
||||
|
||||
link-check:
|
||||
name: Check Links
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check markdown links
|
||||
uses: gaurav-nelson/github-action-markdown-link-check@v1
|
||||
with:
|
||||
use-quiet-mode: 'yes'
|
||||
use-verbose-mode: 'no'
|
||||
config-file: '.github/markdown-link-check-config.json'
|
||||
|
||||
spelling:
|
||||
name: Spell Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run cSpell
|
||||
uses: streetsidesoftware/cspell-action@v6
|
||||
with:
|
||||
files: |
|
||||
**/*.md
|
||||
**/*.json
|
||||
**/*.yml
|
||||
**/*.yaml
|
||||
incremental: 'no'
|
||||
config: '.cspell.json'
|
||||
|
||||
frontmatter:
|
||||
name: YAML Frontmatter Validation
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install PyYAML
|
||||
run: pip install pyyaml
|
||||
|
||||
- name: Validate YAML Frontmatter
|
||||
run: |
|
||||
python3 << 'EOF'
|
||||
import os
|
||||
import yaml
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
errors = []
|
||||
|
||||
# Check all markdown files in Claude sections
|
||||
md_files = list(Path('.').glob('**/README.md')) + list(Path('.').glob('01-*/**/*.md'))
|
||||
|
||||
for file_path in md_files:
|
||||
if '.venv' in str(file_path) or 'node_modules' in str(file_path):
|
||||
continue
|
||||
|
||||
with open(file_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Check for YAML frontmatter in skill/agent templates
|
||||
if 'SKILL.md' in str(file_path) or any(x in str(file_path) for x in ['agent', 'template']):
|
||||
if content.startswith('---'):
|
||||
try:
|
||||
# Extract frontmatter
|
||||
parts = content.split('---', 2)
|
||||
if len(parts) >= 3:
|
||||
yaml.safe_load(parts[1])
|
||||
except yaml.YAMLError as e:
|
||||
errors.append(f"{file_path}: Invalid YAML frontmatter - {e}")
|
||||
else:
|
||||
if 'skill' in str(file_path).lower():
|
||||
print(f"⚠ {file_path}: Consider adding YAML frontmatter")
|
||||
|
||||
if errors:
|
||||
for error in errors:
|
||||
print(f"❌ {error}")
|
||||
exit(1)
|
||||
else:
|
||||
print("✅ All YAML frontmatter is valid")
|
||||
EOF
|
||||
|
||||
structure:
|
||||
name: Documentation Structure
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Validate Documentation Structure
|
||||
run: |
|
||||
python3 << 'EOF'
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
errors = []
|
||||
warnings = []
|
||||
|
||||
# Check that all numbered directories have README.md
|
||||
for i in range(1, 11):
|
||||
dir_path = Path(f"{i:02d}-*")
|
||||
matches = list(dir_path.parent.glob(str(dir_path)))
|
||||
if matches:
|
||||
for match in matches:
|
||||
if (match / "README.md").exists():
|
||||
print(f"✅ {match}/README.md found")
|
||||
else:
|
||||
errors.append(f"{match}: Missing README.md")
|
||||
|
||||
# Check README.md has required sections
|
||||
readme_path = Path("README.md")
|
||||
if readme_path.exists():
|
||||
with open(readme_path) as f:
|
||||
content = f.read()
|
||||
|
||||
required_sections = [
|
||||
"## Table of Contents",
|
||||
"## Contributing",
|
||||
"## License",
|
||||
]
|
||||
|
||||
for section in required_sections:
|
||||
if section not in content:
|
||||
errors.append(f"README.md: Missing '{section}' section")
|
||||
else:
|
||||
print(f"✅ README.md has '{section}' section")
|
||||
|
||||
# Check for broken relative links
|
||||
md_files = list(Path(".").rglob("*.md"))
|
||||
for file_path in md_files:
|
||||
if ".venv" in str(file_path) or "node_modules" in str(file_path):
|
||||
continue
|
||||
|
||||
with open(file_path) as f:
|
||||
content = f.read()
|
||||
|
||||
# Find markdown links to local files
|
||||
local_links = re.findall(r'\[([^\]]+)\]\(([^)]+\.md)\)', content)
|
||||
for link_text, link_path in local_links:
|
||||
# Skip anchor-only links
|
||||
if link_path.startswith("#"):
|
||||
continue
|
||||
|
||||
resolved_path = (file_path.parent / link_path).resolve()
|
||||
if not resolved_path.exists():
|
||||
warnings.append(f"{file_path}: Broken link to '{link_path}'")
|
||||
|
||||
if errors:
|
||||
print("\n❌ Validation Errors:")
|
||||
for error in errors:
|
||||
print(f" - {error}")
|
||||
exit(1)
|
||||
|
||||
if warnings:
|
||||
print("\n⚠ Warnings:")
|
||||
for warning in warnings:
|
||||
print(f" - {warning}")
|
||||
|
||||
print("\n✅ Documentation structure is valid")
|
||||
EOF
|
||||
|
||||
metadata:
|
||||
name: File Metadata Checks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check for common issues
|
||||
run: |
|
||||
set +e # Don't exit on first error
|
||||
|
||||
echo "📋 Checking for documentation issues..."
|
||||
|
||||
# Check for files over 100KB
|
||||
echo "Checking file sizes..."
|
||||
large_files=$(find . -name "*.md" -size +100k | grep -v ".venv" | grep -v "node_modules")
|
||||
if [ ! -z "$large_files" ]; then
|
||||
echo "⚠ Large markdown files found (>100KB):"
|
||||
echo "$large_files"
|
||||
fi
|
||||
|
||||
# Check for TODO markers (might indicate incomplete content)
|
||||
echo "Checking for TODO markers in main docs..."
|
||||
todos=$(grep -r "TODO\|FIXME" --include="*.md" . --exclude-dir=.venv --exclude-dir=node_modules --exclude-dir=.git | grep -v ".github" | head -10)
|
||||
if [ ! -z "$todos" ]; then
|
||||
echo "⚠ Found TODO/FIXME markers:"
|
||||
echo "$todos"
|
||||
fi
|
||||
|
||||
# Check for consistent heading structure
|
||||
echo "Checking heading consistency in key files..."
|
||||
for file in README.md CONTRIBUTING.md; do
|
||||
if [ -f "$file" ]; then
|
||||
heading_count=$(grep -c "^##" "$file")
|
||||
echo "✅ $file has $heading_count main sections"
|
||||
fi
|
||||
done
|
||||
|
||||
# Check that LICENSE file exists
|
||||
if [ -f "LICENSE" ]; then
|
||||
echo "✅ LICENSE file exists"
|
||||
else
|
||||
echo "❌ LICENSE file not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
consistency:
|
||||
name: Content Consistency
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Check Content Consistency
|
||||
run: |
|
||||
python3 << 'EOF'
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
errors = []
|
||||
|
||||
# Check that all numbered sections are referenced in README
|
||||
readme_path = Path("README.md")
|
||||
with open(readme_path) as f:
|
||||
readme_content = f.read()
|
||||
|
||||
# Look for numbered directories
|
||||
for i in range(1, 11):
|
||||
dir_pattern = f"{i:02d}-"
|
||||
dirs = list(Path(".").glob(f"{dir_pattern}*"))
|
||||
|
||||
if dirs:
|
||||
for dir_path in dirs:
|
||||
if dir_path.is_dir():
|
||||
# Check if referenced in README
|
||||
dir_name = dir_path.name
|
||||
if dir_name not in readme_content:
|
||||
errors.append(f"README.md: Directory '{dir_name}' not mentioned")
|
||||
else:
|
||||
print(f"✅ '{dir_name}' is referenced in README")
|
||||
|
||||
# Check for consistent code fence formatting
|
||||
md_files = [f for f in Path(".").rglob("*.md") if ".venv" not in str(f)]
|
||||
for file_path in md_files:
|
||||
with open(file_path) as f:
|
||||
content = f.read()
|
||||
|
||||
# Count code fence types
|
||||
backtick_fences = len(re.findall(r'```', content))
|
||||
if backtick_fences % 2 != 0:
|
||||
errors.append(f"{file_path}: Unmatched code fences (backticks)")
|
||||
|
||||
if errors:
|
||||
print("\n❌ Consistency Errors:")
|
||||
for error in errors:
|
||||
print(f" - {error}")
|
||||
exit(1)
|
||||
|
||||
print("\n✅ Content is consistent")
|
||||
EOF
|
||||
|
||||
summary:
|
||||
name: Summary
|
||||
needs: [markdown-lint, link-check, spelling, frontmatter, structure, metadata, consistency]
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
- name: Check Job Results
|
||||
run: |
|
||||
if [ "${{ needs.markdown-lint.result }}" = "failure" ] || \
|
||||
[ "${{ needs.link-check.result }}" = "failure" ] || \
|
||||
[ "${{ needs.spelling.result }}" = "failure" ] || \
|
||||
[ "${{ needs.frontmatter.result }}" = "failure" ] || \
|
||||
[ "${{ needs.structure.result }}" = "failure" ] || \
|
||||
[ "${{ needs.metadata.result }}" = "failure" ] || \
|
||||
[ "${{ needs.consistency.result }}" = "failure" ]; then
|
||||
echo "❌ Some documentation checks failed"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ All documentation checks passed!"
|
||||
fi
|
||||
Reference in New Issue
Block a user