mirror of
https://github.com/luongnv89/claude-howto.git
synced 2026-06-01 10:31:33 +02:00
refactor(ci): shift quality checks to pre-commit, CI as 2nd pass (#34)
* refactor(ci): shift quality checks to pre-commit, CI as 2nd pass
- Remove ci.yml (lint, security, pytest were only for EPUB scripts)
- Move EPUB build to pre-commit local hook (runs on .md changes)
- Add check_cross_references.py, check_mermaid.py, check_links.py scripts
- Add markdown-lint, cross-references, mermaid-syntax, link-check as
pre-commit hooks — mirrors all 4 CI doc-check jobs locally
- Remove spell check job from docs-check.yml (breaks on translations)
- Refactor docs-check.yml to reuse scripts/ instead of inline Python
- Add .markdownlint.json config shared by pre-commit and CI
- Update CONTRIBUTING.md with required dependencies and hook table
* fix(ci): resolve all CI check failures in docs-check workflow
- fix(check_cross_references): skip code blocks and inline code spans
to avoid false positives from documentation examples; fix emoji
heading anchor generation (rstrip not strip); add blog-posts,
openspec, prompts, .agents to IGNORE_DIRS; ignore README.backup.md
- fix(check_links): strip trailing Markdown punctuation from captured
URLs; add wikipedia, api.github.com to SKIP_DOMAINS; add placeholder
URL patterns to SKIP_URL_PATTERNS; add .agents/.claude to IGNORE_DIRS
- fix(check_mermaid): add --no-sandbox puppeteer config support via
MERMAID_PUPPETEER_NO_SANDBOX env var for GitHub Actions Linux runners
- fix(docs-check.yml): pass MERMAID_PUPPETEER_NO_SANDBOX=true to mermaid job
- fix(content): repair broken anchors in README.md, 09-advanced-features;
fix #plugins -> #claude-code-plugins in claude_concepts_guide.md;
remove non-existent ./docs/performance.md placeholder links; fix
dependabot alerts URL in SECURITY_REPORTING.md; update auto-mode URL
in resources.md; use placeholder pattern for 07-plugins example URL
- remove README.backup.md (stale file)
* fix(check-scripts): fix strip_code_blocks regex and URL fragment handling
- fix regex in strip_code_blocks to avoid conflicting MULTILINE+DOTALL
flags that could fail to strip indented code fences; use DOTALL only
- strip URL fragments (#section) before dispatching link checks to avoid
false-positive 404s on valid URLs with anchor fragments
* fix(check-scripts): fix anchor stripping, cross-ref enforcement, and mermaid temp file cleanup
- heading_to_anchor: use .strip("-") instead of .rstrip("-") to also strip leading hyphens
produced by emoji-prefixed headings, preventing false-positive anchor errors
- check_cross_references: always exit with main()'s return code — filesystem checks
should block pre-commit unconditionally, not silently pass on errors
- check_mermaid: wrap file-processing loop in try/finally so the puppeteer config
temp file is cleaned up even if an unexpected exception (e.g. UnicodeDecodeError) occurs
- docs-check.yml: remove now-unused CROSS_REF_STRICT env var
* fix(scripts): fix anchor stripping and mermaid output path
- Replace .strip('-') with .rstrip('-') in heading_to_anchor() so leading
hyphens from emoji-prefixed headings are preserved, matching GitHub's
anchor generation behaviour.
- Use Path.with_suffix('.svg') in check_mermaid.py instead of
str.replace('.mmd', '.svg') to avoid replacing all occurrences of .mmd
in the full temp path.
This commit is contained in:
@@ -1,130 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
# Cancel in-progress runs for the same branch
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint & Format
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install 3.11
|
||||
|
||||
- name: Create venv and install Ruff
|
||||
run: |
|
||||
uv venv
|
||||
uv pip install ruff
|
||||
|
||||
- name: Ruff Format Check
|
||||
run: uv run ruff format --check scripts/
|
||||
|
||||
- name: Ruff Lint Check
|
||||
run: uv run ruff check scripts/
|
||||
|
||||
security:
|
||||
name: Security Scan
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install 3.11
|
||||
|
||||
- name: Create venv and install Bandit
|
||||
run: |
|
||||
uv venv
|
||||
uv pip install "bandit[toml]"
|
||||
|
||||
- name: Run Bandit Security Scan
|
||||
run: uv run bandit -c scripts/pyproject.toml -r scripts/ --exclude scripts/tests/
|
||||
|
||||
test:
|
||||
name: Unit Tests (Python ${{ matrix.python-version }})
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ['3.10', '3.11', '3.12']
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
run: uv python install ${{ matrix.python-version }}
|
||||
|
||||
- name: Create venv and install dependencies
|
||||
run: |
|
||||
uv venv
|
||||
uv pip install -r scripts/requirements-dev.txt
|
||||
|
||||
- name: Run pytest with coverage
|
||||
run: uv run pytest scripts/tests/ -v --tb=short --cov=scripts --cov-report=xml --cov-report=term-missing
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
files: ./coverage.xml
|
||||
flags: unittests
|
||||
name: codecov-python-${{ matrix.python-version }}
|
||||
fail_ci_if_error: false
|
||||
|
||||
build:
|
||||
name: Build EPUB
|
||||
runs-on: ubuntu-latest
|
||||
needs: [lint, security, test]
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v4
|
||||
|
||||
- name: Set up Python
|
||||
run: uv python install 3.11
|
||||
|
||||
- name: Create venv and install dependencies
|
||||
run: |
|
||||
uv venv
|
||||
uv pip install -r scripts/requirements-dev.txt
|
||||
|
||||
- name: Build EPUB
|
||||
run: uv run scripts/build_epub.py
|
||||
|
||||
- name: Verify EPUB Created
|
||||
run: |
|
||||
if [ -f claude-howto-guide.epub ]; then
|
||||
echo "EPUB built successfully"
|
||||
ls -lh claude-howto-guide.epub
|
||||
else
|
||||
echo "EPUB file not found!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload EPUB Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: claude-howto-guide-epub
|
||||
path: claude-howto-guide.epub
|
||||
retention-days: 7
|
||||
@@ -6,16 +6,18 @@ on:
|
||||
paths:
|
||||
- '**.md'
|
||||
- '.github/workflows/docs-check.yml'
|
||||
- '.cspell.json'
|
||||
- '.github/markdown-link-check-config.json'
|
||||
- '.markdownlint.json'
|
||||
- 'scripts/check_*.py'
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- '**.md'
|
||||
- 'CONTRIBUTING.md'
|
||||
- 'LICENSE'
|
||||
- '.cspell.json'
|
||||
- '.github/markdown-link-check-config.json'
|
||||
- '.markdownlint.json'
|
||||
- 'scripts/check_*.py'
|
||||
|
||||
# Cancel in-progress runs for the same branch
|
||||
concurrency:
|
||||
@@ -39,8 +41,7 @@ jobs:
|
||||
run: npm install -g markdownlint-cli
|
||||
|
||||
- name: Run Markdown Linter
|
||||
run: markdownlint '**/*.md' --ignore node_modules --ignore .venv
|
||||
continue-on-error: true
|
||||
run: markdownlint '**/*.md' --ignore node_modules --ignore .venv --config .markdownlint.json
|
||||
|
||||
link-check:
|
||||
name: Check Links
|
||||
@@ -49,33 +50,43 @@ jobs:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check markdown links
|
||||
uses: gaurav-nelson/github-action-markdown-link-check@v1
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
use-quiet-mode: 'yes'
|
||||
use-verbose-mode: 'no'
|
||||
config-file: '.github/markdown-link-check-config.json'
|
||||
python-version: '3.11'
|
||||
|
||||
spelling:
|
||||
name: Spell Check
|
||||
- name: Run link check
|
||||
run: python scripts/check_links.py
|
||||
env:
|
||||
LINK_CHECK_STRICT: "1"
|
||||
|
||||
mermaid:
|
||||
name: Mermaid Syntax
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run cSpell
|
||||
uses: streetsidesoftware/cspell-action@v6
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
files: |
|
||||
**/*.md
|
||||
**/*.json
|
||||
**/*.yml
|
||||
**/*.yaml
|
||||
incremental: 'no'
|
||||
config: '.cspell.json'
|
||||
node-version: '18'
|
||||
|
||||
frontmatter:
|
||||
name: YAML Frontmatter Validation
|
||||
- name: Install mermaid CLI
|
||||
run: npm install -g @mermaid-js/mermaid-cli
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Validate Mermaid diagrams
|
||||
run: python scripts/check_mermaid.py
|
||||
env:
|
||||
MERMAID_PUPPETEER_NO_SANDBOX: "true"
|
||||
|
||||
cross-references:
|
||||
name: Cross-reference Check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
@@ -86,245 +97,12 @@ jobs:
|
||||
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
|
||||
- name: Validate cross-references
|
||||
run: python scripts/check_cross_references.py
|
||||
|
||||
summary:
|
||||
name: Summary
|
||||
needs: [markdown-lint, link-check, spelling, frontmatter, structure, metadata, consistency]
|
||||
needs: [markdown-lint, link-check, mermaid, cross-references]
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
@@ -332,11 +110,8 @@ jobs:
|
||||
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
|
||||
[ "${{ needs.mermaid.result }}" = "failure" ] || \
|
||||
[ "${{ needs.cross-references.result }}" = "failure" ]; then
|
||||
echo "❌ Some documentation checks failed"
|
||||
exit 1
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user