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