312 lines
14 KiB
YAML
312 lines
14 KiB
YAML
name: README Quality Check
|
|
|
|
on:
|
|
pull_request:
|
|
paths:
|
|
- 'README*.md'
|
|
- 'Docs/**/*.md'
|
|
push:
|
|
branches: [main, master, develop]
|
|
workflow_dispatch:
|
|
|
|
permissions:
|
|
contents: read
|
|
pull-requests: write
|
|
issues: write
|
|
|
|
jobs:
|
|
readme-quality-check:
|
|
name: Multi-language README Quality Assessment
|
|
runs-on: ubuntu-latest
|
|
|
|
steps:
|
|
- name: Checkout repository
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Set up Python
|
|
uses: actions/setup-python@v5
|
|
with:
|
|
python-version: '3.11'
|
|
|
|
- name: Install dependencies
|
|
run: |
|
|
python -m pip install --upgrade pip
|
|
pip install requests beautifulsoup4 pyyaml
|
|
|
|
- name: Create quality checker script
|
|
run: |
|
|
cat > readme_checker.py << 'EOF'
|
|
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
SuperClaude Multi-language README Quality Checker
|
|
Checks version sync, link validity, and structural consistency
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import requests
|
|
import json
|
|
from pathlib import Path
|
|
from urllib.parse import urljoin
|
|
|
|
class READMEQualityChecker:
|
|
def __init__(self):
|
|
self.readme_files = ['README.md', 'README-zh.md', 'README-ja.md', 'README-kr.md']
|
|
self.results = {
|
|
'structure_consistency': [],
|
|
'link_validation': [],
|
|
'translation_sync': [],
|
|
'overall_score': 0
|
|
}
|
|
|
|
def check_structure_consistency(self):
|
|
"""Check structural consistency"""
|
|
print("🔍 Checking structural consistency...")
|
|
|
|
structures = {}
|
|
for file in self.readme_files:
|
|
if os.path.exists(file):
|
|
with open(file, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
# Extract heading structure
|
|
headers = re.findall(r'^#{1,6}\s+(.+)$', content, re.MULTILINE)
|
|
structures[file] = len(headers)
|
|
|
|
# Compare structural differences
|
|
line_counts = [structures.get(f, 0) for f in self.readme_files if f in structures]
|
|
if line_counts:
|
|
max_diff = max(line_counts) - min(line_counts)
|
|
consistency_score = max(0, 100 - (max_diff * 5))
|
|
|
|
self.results['structure_consistency'] = {
|
|
'score': consistency_score,
|
|
'details': structures,
|
|
'status': 'PASS' if consistency_score >= 90 else 'WARN'
|
|
}
|
|
|
|
print(f"✅ Structural consistency: {consistency_score}/100")
|
|
for file, count in structures.items():
|
|
print(f" {file}: {count} headers")
|
|
|
|
def check_link_validation(self):
|
|
"""Check link validity"""
|
|
print("🔗 Checking link validity...")
|
|
|
|
all_links = {}
|
|
broken_links = []
|
|
|
|
for file in self.readme_files:
|
|
if os.path.exists(file):
|
|
with open(file, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
# Extract all links
|
|
links = re.findall(r'\[([^\]]+)\]\(([^)]+)\)', content)
|
|
all_links[file] = []
|
|
|
|
for text, url in links:
|
|
link_info = {'text': text, 'url': url, 'status': 'unknown'}
|
|
|
|
# Check local file links
|
|
if not url.startswith(('http://', 'https://', '#')):
|
|
if os.path.exists(url):
|
|
link_info['status'] = 'valid'
|
|
else:
|
|
link_info['status'] = 'broken'
|
|
broken_links.append(f"{file}: {url}")
|
|
|
|
# HTTP link check (simplified)
|
|
elif url.startswith(('http://', 'https://')):
|
|
try:
|
|
# Only check key links to avoid excessive requests
|
|
if any(domain in url for domain in ['github.com', 'pypi.org', 'npmjs.com']):
|
|
response = requests.head(url, timeout=10, allow_redirects=True)
|
|
link_info['status'] = 'valid' if response.status_code < 400 else 'broken'
|
|
else:
|
|
link_info['status'] = 'skipped'
|
|
except:
|
|
link_info['status'] = 'error'
|
|
else:
|
|
link_info['status'] = 'anchor'
|
|
|
|
all_links[file].append(link_info)
|
|
|
|
# Calculate link health score
|
|
total_links = sum(len(links) for links in all_links.values())
|
|
broken_count = len(broken_links)
|
|
link_score = max(0, 100 - (broken_count * 10)) if total_links > 0 else 100
|
|
|
|
self.results['link_validation'] = {
|
|
'score': link_score,
|
|
'total_links': total_links,
|
|
'broken_links': broken_count,
|
|
'broken_list': broken_links[:10], # Show max 10
|
|
'status': 'PASS' if link_score >= 80 else 'FAIL'
|
|
}
|
|
|
|
print(f"✅ Link validity: {link_score}/100")
|
|
print(f" Total links: {total_links}")
|
|
print(f" Broken links: {broken_count}")
|
|
|
|
def check_translation_sync(self):
|
|
"""Check translation sync"""
|
|
print("🌍 Checking translation sync...")
|
|
|
|
if not all(os.path.exists(f) for f in self.readme_files):
|
|
print("⚠️ Some README files are missing")
|
|
self.results['translation_sync'] = {
|
|
'score': 60,
|
|
'status': 'WARN',
|
|
'message': 'Some README files are missing'
|
|
}
|
|
return
|
|
|
|
# Check file modification times
|
|
mod_times = {}
|
|
for file in self.readme_files:
|
|
mod_times[file] = os.path.getmtime(file)
|
|
|
|
# Calculate time difference (seconds)
|
|
times = list(mod_times.values())
|
|
time_diff = max(times) - min(times)
|
|
|
|
# Score based on time diff (within 7 days = synced)
|
|
sync_score = max(0, 100 - (time_diff / (7 * 24 * 3600) * 20))
|
|
|
|
self.results['translation_sync'] = {
|
|
'score': int(sync_score),
|
|
'time_diff_days': round(time_diff / (24 * 3600), 2),
|
|
'status': 'PASS' if sync_score >= 80 else 'WARN',
|
|
'mod_times': {f: f"{os.path.getmtime(f):.0f}" for f in self.readme_files}
|
|
}
|
|
|
|
print(f"✅ Translation sync: {int(sync_score)}/100")
|
|
print(f" Max time difference: {round(time_diff / (24 * 3600), 1)} days")
|
|
|
|
def generate_report(self):
|
|
"""Generate quality report"""
|
|
print("\n📊 Generating quality report...")
|
|
|
|
# Calculate overall score
|
|
scores = [
|
|
self.results['structure_consistency'].get('score', 0),
|
|
self.results['link_validation'].get('score', 0),
|
|
self.results['translation_sync'].get('score', 0)
|
|
]
|
|
overall_score = sum(scores) // len(scores)
|
|
self.results['overall_score'] = overall_score
|
|
|
|
# Generate GitHub Actions summary
|
|
pipe = "|"
|
|
table_header = f"{pipe} Check {pipe} Score {pipe} Status {pipe} Details {pipe}"
|
|
table_separator = f"{pipe}----------|------|------|------|"
|
|
table_row1 = f"{pipe} 📐 Structure {pipe} {self.results['structure_consistency'].get('score', 0)}/100 {pipe} {self.results['structure_consistency'].get('status', 'N/A')} {pipe} {len(self.results['structure_consistency'].get('details', {}))} files {pipe}"
|
|
table_row2 = f"{pipe} 🔗 Links {pipe} {self.results['link_validation'].get('score', 0)}/100 {pipe} {self.results['link_validation'].get('status', 'N/A')} {pipe} {self.results['link_validation'].get('broken_links', 0)} broken {pipe}"
|
|
table_row3 = f"{pipe} 🌍 Translation {pipe} {self.results['translation_sync'].get('score', 0)}/100 {pipe} {self.results['translation_sync'].get('status', 'N/A')} {pipe} {self.results['translation_sync'].get('time_diff_days', 0)} days diff {pipe}"
|
|
|
|
summary_parts = [
|
|
"## 📊 README Quality Check Report",
|
|
"",
|
|
f"### 🏆 Overall Score: {overall_score}/100",
|
|
"",
|
|
table_header,
|
|
table_separator,
|
|
table_row1,
|
|
table_row2,
|
|
table_row3,
|
|
"",
|
|
"### 📋 Details",
|
|
"",
|
|
"**Structural consistency details:**"
|
|
]
|
|
summary = "\n".join(summary_parts)
|
|
|
|
for file, count in self.results['structure_consistency'].get('details', {}).items():
|
|
summary += f"\n- `{file}`: {count} headings"
|
|
|
|
if self.results['link_validation'].get('broken_links'):
|
|
summary += f"\n\n**Broken links:**\n"
|
|
for link in self.results['link_validation']['broken_list']:
|
|
summary += f"\n- ❌ {link}"
|
|
|
|
summary += f"\n\n### 🎯 Recommendations\n"
|
|
|
|
if overall_score >= 90:
|
|
summary += "✅ Excellent quality! Keep it up."
|
|
elif overall_score >= 70:
|
|
summary += "⚠️ Good quality with room for improvement."
|
|
else:
|
|
summary += "🚨 Needs improvement! Please review the issues above."
|
|
|
|
# Write GitHub Actions summary
|
|
github_step_summary = os.environ.get('GITHUB_STEP_SUMMARY')
|
|
if github_step_summary:
|
|
with open(github_step_summary, 'w', encoding='utf-8') as f:
|
|
f.write(summary)
|
|
|
|
# Save detailed results
|
|
with open('readme-quality-report.json', 'w', encoding='utf-8') as f:
|
|
json.dump(self.results, f, indent=2, ensure_ascii=False)
|
|
|
|
print("✅ Report generated")
|
|
|
|
# Determine exit code based on score
|
|
return 0 if overall_score >= 70 else 1
|
|
|
|
def run_all_checks(self):
|
|
"""Run all checks"""
|
|
print("🚀 Starting README quality check...\n")
|
|
|
|
self.check_structure_consistency()
|
|
self.check_link_validation()
|
|
self.check_translation_sync()
|
|
|
|
exit_code = self.generate_report()
|
|
|
|
print(f"\n🎯 Check complete! Score: {self.results['overall_score']}/100")
|
|
return exit_code
|
|
|
|
if __name__ == "__main__":
|
|
checker = READMEQualityChecker()
|
|
exit_code = checker.run_all_checks()
|
|
exit(exit_code)
|
|
EOF
|
|
|
|
- name: Run README quality check
|
|
run: python readme_checker.py
|
|
|
|
- name: Upload quality report
|
|
if: always()
|
|
uses: actions/upload-artifact@v4
|
|
with:
|
|
name: readme-quality-report
|
|
path: readme-quality-report.json
|
|
retention-days: 30
|
|
|
|
- name: Comment PR (if applicable)
|
|
if: github.event_name == 'pull_request' && always() && github.event.pull_request.head.repo.full_name == github.repository
|
|
uses: actions/github-script@v7
|
|
with:
|
|
script: |
|
|
const fs = require('fs');
|
|
|
|
if (fs.existsSync('readme-quality-report.json')) {
|
|
const report = JSON.parse(fs.readFileSync('readme-quality-report.json', 'utf8'));
|
|
|
|
const score = report.overall_score;
|
|
const emoji = score >= 90 ? '🏆' : score >= 70 ? '✅' : '⚠️';
|
|
|
|
const comment = `${emoji} **README Quality Check: ${score}/100**\n\n` +
|
|
`📐 Structural consistency: ${report.structure_consistency?.score || 0}/100\n` +
|
|
`🔗 Link validity: ${report.link_validation?.score || 0}/100\n` +
|
|
`🌍 Translation sync: ${report.translation_sync?.score || 0}/100\n\n` +
|
|
`See the Actions tab for the detailed report.`;
|
|
|
|
github.rest.issues.createComment({
|
|
issue_number: context.issue.number,
|
|
owner: context.repo.owner,
|
|
repo: context.repo.repo,
|
|
body: comment
|
|
});
|
|
} |