skill-article-publisher
Automate article validation, semantic commit generation, and git publishing for MDX documentation. Validates syntax, runs build checks, creates semantic commits, and pushes to remote repositories.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install foreveryh-claude-skills-tutorial-skill-article-publisher
Repository
Skill path: .claude/skills/skill-article-publisher
Automate article validation, semantic commit generation, and git publishing for MDX documentation. Validates syntax, runs build checks, creates semantic commits, and pushes to remote repositories.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack.
Target audience: everyone.
License: Complete terms in LICENSE.txt.
Original source
Catalog source: SkillHub Club.
Repository owner: foreveryh.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install skill-article-publisher into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/foreveryh/claude-skills-tutorial before adding skill-article-publisher to shared team environments
- Use skill-article-publisher for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: skill-article-publisher
license: Complete terms in LICENSE.txt
description: Automate article validation, semantic commit generation, and git publishing for MDX documentation. Validates syntax, runs build checks, creates semantic commits, and pushes to remote repositories.
---
# Article Publisher Automation Guide
This skill automates the complete article publishing workflow: MDX syntax validation, build verification, semantic commit generation, and git push. Use this to ensure documentation quality and maintain consistent git history.
---
# Process
## π 6-Step Publishing Workflow
This is a comprehensive publishing skill that validates and publishes MDX articles with automated semantic commits and git push.
### Phase 1: Identify Target Files
#### 1.1 Determine Article Location
First, identify what needs to be published:
**For single article**: Provide path to specific `.mdx` file
```bash
content/docs/en/development/my-article.mdx
```
**For multiple articles**: Provide directory containing changes
```bash
content/docs/en/development/
```
The skill will automatically detect:
- New MDX files (added to git)
- Modified MDX files
- Multi-language versions (en, zh, fr)
- File types (skill analysis, tutorials, etc.)
#### 1.2 Review Current Git Status
Before publishing, understand the current state:
**Check git status**:
```bash
git status
```
**Review changes**:
```bash
git diff --staged
```
**Ensure all MDX files are saved** and ready for validation.
---
### Phase 2: Run Validation
#### 2.1 MDX Syntax Validation
**Run validation script**:
```bash
cd .claude/skills/skill-article-publisher
python scripts/validate_mdx.py /path/to/article.mdx
```
**Or validate directory**:
```bash
python scripts/validate_mdx.py content/docs/en/development/
```
**What it checks**:
- β
YAML frontmatter structure
- β
Required fields (title, description, lang)
- β
Unescaped comparison operators (>, <) in text
- β
Common unescaped characters
- β
MDX component tag balance
- β
HTML tag balance
**Critical checks for Claude skills**:
- **Comparison operators**: Always use `>` and `<` instead of `>` and `<` in text
- β Wrong: `>80% accuracy`
- β
Correct: `>80% accuracy`
- **Frontmatter**: Must have `title`, `description`, `lang` fields
- **Lang codes**: Should be `en`, `zh`, or `fr` for standard Claude skills
#### 2.2 Build Validation (Optional but Recommended)
**Run build to verify MDX compilation**:
```bash
npm run build
```
**Why this matters**:
- **Primary validation**: Build catches all MDX syntax errors
- Verifies MDX components render correctly
- Ensures imports and dependencies work
- Validates TypeScript types if applicable
**Important note on validation**:
The MDX validation script focuses on common issues (comparison operators, frontmatter), while complex MDX component syntax validation is best handled by the build process. Always run build validation for complete assurance.
**Or use validation script with build**:
```bash
python scripts/validate_mdx.py content/docs/en/development/article.mdx --build
```
**Build timeout**: 5 minutes (adjust in script if needed)
**Interpret results**:
- β
Build succeeds β MDX syntax is valid
- β Build fails β Check error messages for issues
- β οΈ Build timeout β May indicate large project or problem
---
### Phase 3: Review Validation Results
#### 3.1 Analyze Validation Output
**Validation report structure**:
```
MDX VALIDATION REPORT
================================================================================
β ERRORS (2):
File: content/docs/en/development/article.mdx:730
Error: Unescaped comparison operator found. Use > instead of > in: Typical benchmarks: >80% accuracy
β οΈ WARNINGS (1):
File: content/docs/en/development/article.mdx:1
Warning: Lang code "ko" may not be supported.
π SUMMARY:
Files checked: 1
Files valid: 0
Errors: 2
Warnings: 1
β Validation failed due to errors
```
**Fix errors before proceeding**:
1. **Unescaped operators**: Replace `>` with `>` and `<` with `<`
2. **Missing fields**: Add required frontmatter fields
3. **Tag balance**: Close any unclosed MDX components
4. **Invalid lang**: Change to supported language code
**Warnings are acceptable** but should be reviewed.
#### 3.2 Run Validation Again
After fixing issues, re-run validation:
```bash
python scripts/validate_mdx.py content/docs/en/development/article.mdx
```
**Continue until**: "All files passed validation"
---
### Phase 4: Generate Commit Message
#### 4.1 Detect Change Types
The publisher automatically detects:
**Change type from file path**:
- `analyzing-mcp-builder` β `feat` (new skill analysis)
- `analyzing-skill-name` β `feat` (skill analysis)
- `tutorial-*` β `docs` (documentation/tutorial)
- Other patterns β `docs` (default)
**Change type from branch**:
- `feature/*` β `feat`
- `fix/*` β `fix`
- `docs/*` β `docs`
- `main` β `docs` (default)
#### 4.2 Multi-Language Detection
**Languages detected from path**:
- `/en/` β English
- `/zh/` β Chinese
- `/fr/` β French
**Single file**:
```
feat: publish analyzing-mcp-builder (en, zh, fr)
```
**Multiple files**:
```
feat: publish multiple articles (3 skill-analysis, 2 tutorial)
skill-analysis: analyzing-mcp-builder, analyzing-webapp-testing
tutorial: tutorial-usage-patterns, tutorial-best-practices
Languages: en, zh, fr
```
---
### Phase 5: Commit Changes
#### 5.1 Dry Run (Default)
**Test without actual push**:
```bash
python scripts/publish_article.py content/docs/en/development/article.mdx
```
**What happens**:
1. β
Run MDX validation
2. β
Run build validation
3. β
Generate commit message (shows in output)
4. βοΈ Commit (shows what would be committed)
5. βοΈ Push (requires `--push` flag)
**Output includes**:
```
π Changes detected (3 files):
- content/docs/en/development/analyzing-mcp-builder.mdx [en]
- content/docs/zh/development/analyzing-mcp-builder.mdx [zh]
- content/docs/fr/development/analyzing-mcp-builder.mdx [fr]
π Generated commit message:
feat: publish analyzing-mcp-builder (en, zh, fr)
skill-analysis: analyzing-mcp-builder
Languages: en, zh, fr
π¦ Actions:
β
Validate MDX
β
Create semantic commit (dry run)
βοΈ Push (use --push to enable)
```
#### 5.2 Create Commit
**With interactive confirmation**:
```bash
# Shows summary and asks for confirmation
python scripts/publish_article.py content/docs/en/development/article.mdx --push
```
**Automatically stage, commit, and push**:
```bash
python scripts/publish_article.py content/docs/en/development/article.mdx --push --type feat
```
**Available commit types**:
- `docs` β Documentation only (default)
- `feat` β New feature/skill analysis
- `fix` β Bug fix or correction
- `chore` β Maintenance, refactoring
---
### Phase 6: Push to Remote
#### 6.1 Automated Push
**With --push flag**:
```bash
python scripts/publish_article.py content/docs/en/development/article.mdx --push
```
**What happens**:
1. β
All validation passes
2. β
Commit created
3. β
Push to current branch
4. β
Shows success message
**Output**:
```
π Pushing to remote...
β
Changes pushed to origin/main
β
Publish complete!
```
#### 6.2 Manual Push (Alternative)
**If not using --push, manually push later**:
```bash
git push origin $(git branch --show-current)
```
**Or create PR from GitHub/GitLab interface**.
---
## Publishing Options
### Option 1: Single Article (Interactive)
**For one article with confirmation**:
```bash
cd .claude/skills/skill-article-publisher
python scripts/publish_article.py content/docs/en/development/analyzing-mcp-builder.mdx
```
**Review output, then rerun with --push** if satisfied.
### Option 2: Directory Publishing
**For all articles in a directory**:
```bash
python scripts/publish_article.py content/docs/en/development/
```
**Automatically detects**:
- All new/modified `.mdx` files
- Multi-language versions
- Generates comprehensive commit
### Option 3: CI/CD Integration
**For automated pipelines**:
```bash
python scripts/publish_article.py content/docs/en/development/ \
--push \
--type docs \
--skip-build # If build already ran in CI
```
**GitHub Actions example**:
```yaml
- name: Publish articles
run: |
cd .claude/skills/skill-article-publisher
python scripts/publish_article.py content/docs/ --push
```
### Option 4: Skip Validations (Fast Mode)
**When you know files are valid**:
```bash
python scripts/publish_article.py content/docs/en/development/article.mdx \
--push \
--skip-build \
--skip-mdx
```
**Use with caution** - only when certain files are valid.
---
## Best Practices
### Validation Best Practices
<Cards>
<Card title="Always Validate First" icon="CheckCircle">
Never commit without running validation first. Build failures are harder to debug after commit.
</Card>
<Card title="Escape Comparison Operators" icon="Code">
Always use > and < in text content. Never use raw > or < outside code blocks.
</Card>
<Card title="Validate Single File First" icon="FileText">
When publishing many files, validate one first to catch systematic errors.
</Card>
<Card title="Use Dry Run Mode" icon="Eye">
Review commit message and changes with dry run before actually pushing.
</Card>
</Cards>
### Commit Best Practices
<Steps>
<Step>
**Use Semantic Types**: Match commit type to change nature
- `feat`: New skill analysis, significant features
- `docs`: Documentation, tutorials
- `fix`: Corrections, bug fixes
- `chore`: Maintenance, refactoring
</Step>
<Step>
**Group Related Changes**: Publish related articles together for cohesive commits
</Step>
<Step>
**Write Descriptive Messages**: Generated messages should clearly describe what's published
</Step>
<Step>
**Include All Languages**: Multi-language articles ensure international accessibility
</Step>
</Steps>
### Common Pitfalls
<Callout type="warn">
**Critical**: Never use raw > or < in MDX text content - always escape as > and <
</Callout>
β **Wrong**:
```mdx
Typical benchmarks:
- **Good**: >80% accuracy
- **Excellent**: >90% accuracy
```
β
**Correct**:
```mdx
Typical benchmarks:
- **Good**: >80% accuracy
- **Excellent**: >90% accuracy
```
β **Don't commit without validation**:
```bash
git add . && git commit -m "add article" && git push
# May fail if MDX has syntax errors
```
β
**Do use skill-article-publisher**:
```bash
python scripts/publish_article.py content/docs/ --push
# Validates, builds, commits, and pushes safely
```
---
## Examples
### Example 1: Single Skill Analysis
**Publishing one skill analysis**:
```bash
python scripts/publish_article.py \
content/docs/en/development/analyzing-mcp-builder.mdx \
--push \
--type feat
```
**Generated commit**:
```
feat: publish analyzing-mcp-builder (en, zh, fr)
skill-analysis: analyzing-mcp-builder
Languages: en, zh, fr
```
### Example 2: Multiple Articles
**Publishing directory of changes**:
```bash
python scripts/publish_article.py \
content/docs/en/development/ \
--push \
--type docs
```
**Generated commit**:
```
docs: publish multiple articles (2 skill-analysis, 1 tutorial)
skill-analysis: analyzing-mcp-builder, analyzing-webapp-testing
tutorial: creating-first-skill
Languages: en, zh, fr
```
### Example 3: CI/CD Integration
**GitHub Actions workflow**:
```yaml
name: Publish Articles
on:
push:
branches: [main]
paths: ['content/docs/**/*.mdx']
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
npm ci
pip install -r requirements.txt
- name: Publish articles
run: |
cd .claude/skills/skill-article-publisher
python scripts/publish_article.py content/docs/ --push --type docs
```
---
## Troubleshooting
### Validation Errors
**Problem**: Build succeeds but validation shows errors
```
Error: Unescaped comparison operator found. Use > instead of >
```
**Solution**:
```bash
# Find and replace all > with > in text
# Find and replace all < with < in text
# Rerun validation
python scripts/validate_mdx.py path/to/file.mdx
```
**Problem**: Frontmatter validation warnings
```
Warning: Missing recommended field in frontmatter: description
```
**Solution**: Add missing field to YAML frontmatter at top of file.
### Git Errors
**Problem**: "No changes to commit"
**Cause**: Files not staged or already committed
**Solution**:
```bash
git status # Check current state
git add content/docs/ # Stage changes
python scripts/publish_article.py content/docs/ --push
```
**Problem**: Push fails with "rejected"
**Cause**: Remote has changes you don't have locally
**Solution**:
```bash
git pull --rebase origin $(git branch --show-current)
python scripts/publish_article.py content/docs/ --push
```
**Problem**: Authentication fails during push
**Cause**: Git credentials not configured
**Solution**:
```bash
# Configure git credentials
git config --global user.name "Your Name"
git config --global user.email "[email protected]"
# For HTTPS: use credential helper
git config --global credential.helper store
# For SSH: ensure SSH key is set up
ssh-keygen -t ed25519 -C "[email protected]"
```
### Build Failures
**Problem**: Build timeout (5 minutes)
**Cause**: Large project or slow machine
**Solution**:
```bash
# Skip build validation for faster publishing
python scripts/publish_article.py content/docs/ --push --skip-build
```
**Problem**: Build fails with MDX errors
**Cause**: Invalid MDX syntax
**Solution**:
```bash
# Run validation to see specific errors
python scripts/validate_mdx.py content/docs/
# Or check build output directly
npm run build 2>&1 | grep -A 5 -B 5 Error
```
---
## Integration with Other Skills
skill-article-publisher works well with:
1. **skill-article-writer** - Validate and publish articles generated by article-writer
2. **skill-creator** - Validate and commit new skills
3. **translator** - After translation, validate all language versions
4. **fumadocs-article-importer** - Validate imported external articles
---
## Next Steps
To use skill-article-publisher:
1. **Clone the repository**: `git clone https://github.com/anthropics/skills`
2. **Locate the skill**: `cd .claude/skills/skill-article-publisher`
3. **Run validation**: `python scripts/validate_mdx.py path/to/article.mdx`
4. **Fix any errors**: Update MDX files based on validation output
5. **Test dry run**: `python scripts/publish_article.py path/to/article.mdx`
6. **Review commit message**: Ensure it accurately describes changes
7. **Publish**: `python scripts/publish_article.py path/to/article.mdx --push`
### Related Resources
- **skill-article-writer**: /development/analyzing-skill-article-writer
- **Claude Skills Repository**: github.com/anthropics/skills
- **Semantic Commits**: semantic-release.gitbook.io/semantic-release/
- **MDX Documentation**: mdxjs.com
---
## Conclusion
skill-article-publisher demonstrates exceptional Claude skill design by:
β
**Automating Quality Assurance**: Systematic MDX validation prevents syntax errors
β
**Enforcing Best Practices**: Built-in rules for comparison operators and structure
β
**Semantic Commit Generation**: Intelligent detection of change types and languages
β
**Git Workflow Integration**: Seamless staging, committing, and pushing
β
**Safety Features**: Dry run mode, confirmation prompts, comprehensive validation
β
**Error Prevention**: Catches issues before they reach production
The key insights from this skill ensure that every article is validated, properly committed, and safely published with minimal manual intervention.
---
## Summary
This comprehensive guide covered:
- β
6-step publishing workflow from identification to push
- β
MDX syntax validation and common error prevention
- β
Build validation integration
- β
Change detection and categorization
- β
Automatic semantic commit generation
- β
Git staging, committing, and pushing
- β
Troubleshooting common issues
- β
CI/CD integration patterns
## Next Steps
Ready to automate your publishing workflow?
1. **Study the validation script**: Understand what it checks and why
2. **Test validation**: Run validate_mdx.py on existing articles
3. **Fix any issues**: Update articles based on validation output
4. **Try dry run**: Use publish_article.py without --push first
5. **Review commit message**: Ensure it accurately describes changes
6. **Publish with confidence**: Add --push when ready
7. **Integrate into workflow**: Use for all future article publishing
## βΉοΈ Source Information
**Created**: 2025-01-17
**Skill**: skill-article-publisher
**Author**: Anthropic
This skill provides production-ready automation for MDX article publishing with comprehensive validation and semantic commit generation.
---
## Appendix
### Complete Script Features
**validate_mdx.py** (250+ lines):
- β
YAML frontmatter validation
- β
Comparison operator detection
- β
Unescaped character detection
- β
MDX component tag balance
- β
HTML tag balance
- β
Build validation integration
- β
Recursive directory validation
- β
Detailed error/warning reporting
**publish_article.py** (350+ lines):
- β
Git change detection
- β
Change type classification
- β
Multi-language detection
- β
Semantic commit generation
- β
Build validation
- β
MDX validation integration
- β
Automatic staging and committing
- β
Push to remote
- β
Dry run mode
- β
Interactive confirmation
### Validation Checklist
Before publishing, ensure:
- [ ] All MDX files pass validation
- [ ] No unescaped > or < in text content
- [ ] Build completes successfully
- [ ] Commit message accurately describes changes
- [ ] Changes staged and ready
- [ ] (Optional) Dry run reviewed
- [ ] Remote branch exists
- [ ] Git credentials configured
### Common Escape Patterns
| Raw | Escaped | Context |
|-----|---------|---------|
| `>` | `>` | Text, comparisons, arrows |
| `<` | `<` | Text, comparisons, arrows |
| `&` | `&` | Text (not in entities) |
| `"` | `"` | In HTML attributes |
**Note**: Always use escapes in text content, never in code blocks or YAML frontmatter.
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### scripts/validate_mdx.py
```python
#!/usr/bin/env python3
"""
MDX file validator for Claude skills documentation.
Usage:
python validate_mdx.py <file-or-directory>
Examples:
python validate_mdx.py content/docs/en/development/article.mdx
python validate_mdx.py content/docs/en/development/
python validate_mdx.py content/docs/en/
"""
import re
import sys
import argparse
from pathlib import Path
from typing import List, Dict, Any
import subprocess
class MDXValidator:
"""Validator for MDX files with Claude skills documentation patterns."""
def __init__(self):
self.errors: List[Dict[str, Any]] = []
self.warnings: List[Dict[str, Any]] = []
self.files_checked: int = 0
self.files_valid: int = 0
def validate_file(self, file_path: Path) -> bool:
"""Validate a single MDX file."""
self.files_checked += 1
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
# Check 1: Validate frontmatter
self._validate_frontmatter(content, file_path)
# Check 2: Find unescaped comparison operators in text
self._validate_comparison_operators(content, file_path)
# Check 3: Check for common unescaped characters
self._validate_unescaped_characters(content, file_path)
# Check 4: Validate MDX component syntax
self._validate_mdx_components(content, file_path)
# Check 5: Check for unclosed tags
self._validate_tag_balance(content, file_path)
self.files_valid += 1
return True
except Exception as e:
self.errors.append({
'file': str(file_path),
'line': 0,
'message': f'Error reading file: {str(e)}'
})
return False
def _validate_frontmatter(self, content: str, file_path: Path):
"""Validate YAML frontmatter."""
if not content.startswith('---\n'):
self.warnings.append({
'file': str(file_path),
'line': 1,
'message': 'File does not start with YAML frontmatter (---)'
})
return
frontmatter_match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL)
if not frontmatter_match:
self.errors.append({
'file': str(file_path),
'line': 1,
'message': 'Invalid frontmatter format. Must be: ---\n...\n---'
})
return
frontmatter = frontmatter_match.group(1)
# Check required fields
required_fields = ['title', 'description', 'lang']
for field in required_fields:
if f'{field}:' not in frontmatter:
self.warnings.append({
'file': str(file_path),
'line': 1,
'message': f'Missing recommended field in frontmatter: {field}'
})
# Validate lang field
lang_match = re.search(r'^lang:\s*"?([a-z]{2})"?', frontmatter, re.MULTILINE)
if not lang_match:
self.errors.append({
'file': str(file_path),
'line': 1,
'message': 'Missing or invalid lang field in frontmatter. Use 2-letter code like "en", "zh", "fr"'
})
else:
lang = lang_match.group(1)
if lang not in ['en', 'zh', 'fr']:
self.warnings.append({
'file': str(file_path),
'line': 1,
'message': f'Lang code "{lang}" may not be supported. Consider using en, zh, or fr.'
})
def _validate_comparison_operators(self, content: str, file_path: Path):
"""Find unescaped comparison operators that should be HTML entities."""
# Skip frontmatter
content_without_frontmatter = re.sub(r'^---\n.*?\n---\n', '', content, flags=re.DOTALL)
# Patterns that commonly contain problematic comparison operators
problematic_patterns = [
(r'\*\*Good\*\*:\s*>(\d+%)', r'**Good**: >\1'),
(r'\*\*Excellent\*\*:\s*>(\d+%)', r'**Excellent**: >\1'),
(r'\*\*Outstanding\*\*:\s*>(\d+%)', r'**Outstanding**: >\1'),
(r'Typical benchmarks:\s*\n\s*- \*\*.*?\*\*:\s*>(\d+%)', None),
(r'Jalons typiques\s*:\s*\n\s*- \*\*.*?\*\*:\s*>(\d+%)', None),
(r'ε
ΈεεΊεοΌ\s*\n\s*- \*\*.*?\*\*οΌ\s*>(\d+%)', None),
]
lines = content_without_frontmatter.split('\n')
for line_num, line in enumerate(lines, start=1):
# Skip code blocks
if line.strip().startswith('```'):
continue
for pattern, _ in problematic_patterns:
if re.search(pattern, line):
if '>' not in line and '<' not in line:
self.warnings.append({
'file': str(file_path),
'line': line_num,
'message': f'Unescaped comparison operator found. Use > instead of > in: {line.strip()[:80]}'
})
def _validate_unescaped_characters(self, content: str, file_path: Path):
"""Check for other common unescaped characters in MDX."""
content_without_frontmatter = re.sub(r'^---\n.*?\n---\n', '', content, flags=re.DOTALL)
lines = content_without_frontmatter.split('\n')
for line_num, line in enumerate(lines, start=1):
# Skip code blocks
if line.strip().startswith('```'):
continue
# Check for unescaped < that might be interpreted as HTML tag
# But allow legitimate HTML entities and MDX components
if re.search(r'<[^/a-zA-Z]', line) and not re.search(r'<(Callout|Steps|Cards|Tab|Tabs|File|Folder|Files|CodeBlock|SourceAttribution)', line):
if not re.search(r'<', line):
self.warnings.append({
'file': str(file_path),
'line': line_num,
'message': f'Potentially unescaped < character. Consider using < or wrapping in code block: {line.strip()[:60]}'
})
def _validate_mdx_components(self, content: str, file_path: Path):
"""Validate MDX component syntax (simplified - build catches complex issues)."""
# Only check for obviously malformed components
# Complex validation is left to the build process
# Check for components without closing slash that might be typos
possible_typos = re.findall(r'<(/?)(\w+)[^>]*>', content)
for closing, name in possible_typos:
if name in ['SourceAttribution', 'Callout', 'Cards', 'Card', 'Steps', 'Step', 'Files', 'File', 'Folder', 'Tabs', 'Tab', 'CodeBlock']:
# Just do basic sanity check - don't enforce strict matching
# Build validation will catch real syntax errors
pass
def _validate_tag_balance(self, content: str, file_path: Path):
"""Check for unclosed HTML tags in non-MDX content."""
# Simple check for common HTML tags
simple_tags = ['b', 'i', 'strong', 'em', 'code', 'pre']
for tag in simple_tags:
open_count = len(re.findall(r'<{}\b[^>]*>'.format(tag), content))
close_count = len(re.findall(r'</{}>'.format(tag), content))
if open_count != close_count:
self.warnings.append({
'file': str(file_path),
'line': 0,
'message': f'Tag <{tag}> appears {open_count} times but </{tag}> appears {close_count} times (may be intentional in MDX)'
})
def run_build_check(self, dir_path: Path = None) -> bool:
"""Run npm build to validate MDX compilation."""
print("\nπ§ Running build validation (this may take a while)...")
# Determine project root
if dir_path:
# Try to find package.json in parent directories
project_root = dir_path
while project_root != project_root.parent:
if (project_root / 'package.json').exists():
break
project_root = project_root.parent
else:
project_root = Path.cwd()
if not (project_root / 'package.json').exists():
self.warnings.append({
'file': 'build',
'line': 0,
'message': 'Could not find package.json in project. Skipping build validation.'
})
return True
try:
# Run npm build
result = subprocess.run(
['npm', 'run', 'build'],
cwd=project_root,
capture_output=True,
text=True,
timeout=300 # 5 minute timeout
)
if result.returncode != 0:
self.errors.append({
'file': 'build',
'line': 0,
'message': 'Build failed. Check MDX syntax errors below:'
})
# Extract relevant error messages
for line in result.stderr.split('\n'):
if 'Error' in line or 'error' in line or 'mdx' in line.lower():
self.errors.append({
'file': 'build',
'line': 0,
'message': line.strip()
})
return False
else:
print("β
Build validation passed")
return True
except subprocess.TimeoutExpired:
self.errors.append({
'file': 'build',
'line': 0,
'message': 'Build timeout after 5 minutes. This may indicate a problem or just a large project.'
})
return False
except Exception as e:
self.errors.append({
'file': 'build',
'line': 0,
'message': f'Error running build: {str(e)}'
})
return False
def print_report(self):
"""Print validation report."""
print("\n" + "="*80)
print("MDX VALIDATION REPORT")
print("="*80)
if self.errors:
print(f"\nβ ERRORS ({len(self.errors)}):")
for error in self.errors:
print(f" File: {error['file']}:{error['line']}")
print(f" Error: {error['message']}")
print()
if self.warnings:
print(f"\nβ οΈ WARNINGS ({len(self.warnings)}):")
for warning in self.warnings[:10]: # Show first 10 warnings
print(f" File: {warning['file']}:{warning['line']}")
print(f" Warning: {warning['message']}")
print()
if len(self.warnings) > 10:
print(f" ... and {len(self.warnings) - 10} more warnings")
print(f"\nπ SUMMARY:")
print(f" Files checked: {self.files_checked}")
print(f" Files valid: {self.files_valid}")
print(f" Errors: {len(self.errors)}")
print(f" Warnings: {len(self.warnings)}")
if not self.errors and not self.warnings:
print("\nβ
All files passed validation with no issues!")
elif not self.errors:
print("\nβ
All files passed validation (with warnings)")
else:
print("\nβ Validation failed due to errors")
print("="*80)
def main():
parser = argparse.ArgumentParser(description='Validate MDX files for Claude skills documentation')
parser.add_argument('path', help='Path to MDX file or directory to validate')
parser.add_argument('--build', action='store_true', help='Run build validation (slower but more thorough)')
parser.add_argument('--no-build', action='store_true', help='Skip build validation')
args = parser.parse_args()
path = Path(args.path)
if not path.exists():
print(f"Error: Path does not exist: {path}")
sys.exit(1)
validator = MDXValidator()
# Validate MDX files
if path.is_file():
if path.suffix == '.mdx':
validator.validate_file(path)
else:
print(f"Skipping non-MDX file: {path}")
else:
# Recursively validate all MDX files in directory
mdx_files = list(path.rglob('*.mdx'))
if not mdx_files:
print(f"No MDX files found in: {path}")
sys.exit(0)
print(f"Found {len(mdx_files)} MDX files to validate")
for i, file_path in enumerate(mdx_files, 1):
print(f"\r[{i}/{len(mdx_files)}] Validating {file_path.name}...", end='')
validator.validate_file(file_path)
print() # New line after progress
# Run build validation if requested
if args.build and not args.no_build:
validator.run_build_check(path if path.is_dir() else path.parent)
elif not args.no_build and not args.build:
# Default: run build check for directories
if path.is_dir():
validator.run_build_check(path)
validator.print_report()
# Exit with error code if errors found
sys.exit(1 if validator.errors else 0)
if __name__ == '__main__':
main()
```
### scripts/publish_article.py
```python
#!/usr/bin/env python3
"""
Publish article with semantic commit and automated push.
Usage:
python publish_article.py <mdx-file-or-directory> [--push] [--type <commit-type>]
Examples:
python publish_article.py content/docs/en/development/article.mdx
python publish_article.py content/docs/en/development/article.mdx --push
python publish_article.py content/docs/en/development/article.mdx --push --type feat
"""
import re
import sys
import argparse
from pathlib import Path
from typing import List, Dict, Any
import subprocess
class ArticlePublisher:
"""Publishes MDX articles with semantic commits and automated push."""
def __init__(self, push: bool = False, commit_type: str = 'docs'):
self.push = push
self.commit_type = commit_type
self.changes: List[Dict[str, Any]] = []
self.project_root = None
def find_project_root(self, start_path: Path) -> Path:
"""Find the project root containing package.json."""
current = start_path if start_path.is_dir() else start_path.parent
while current != current.parent:
if (current / 'package.json').exists():
return current
current = current.parent
return Path.cwd()
def detect_changes(self, path: Path):
"""Detect changes in git repository."""
if path.is_file():
# Single file
if self._is_mdx_file(path):
rel_path = path.relative_to(self.project_root)
self.changes.append({
'file': str(rel_path),
'type': self._detect_change_type(path),
'languages': self._detect_languages(path)
})
else:
# Directory - find all modified/added MDX files
changed_files = self._get_git_changed_files()
for file_path in changed_files:
if self._is_mdx_file(Path(file_path)):
self.changes.append({
'file': file_path,
'type': self._detect_change_type(Path(file_path)),
'languages': self._detect_languages(Path(file_path))
})
def _is_mdx_file(self, path: Path) -> bool:
"""Check if file is an MDX file."""
return path.suffix == '.mdx'
def _get_git_changed_files(self) -> List[str]:
"""Get list of changed/added files from git."""
try:
result = subprocess.run(
['git', 'status', '--porcelain'],
cwd=self.project_root,
capture_output=True,
text=True,
check=True
)
files = []
for line in result.stdout.split('\n'):
if line.strip():
status = line[:2]
file_path = line[3:].strip()
# Include added (A), modified (M), and renamed (R) files
if status[0] in ['A', 'M', 'R'] or status[1] in ['A', 'M']:
files.append(file_path)
return files
except subprocess.CalledProcessError:
return []
def _detect_change_type(self, file_path: Path) -> str:
"""Detect the type of change from file path and content."""
path_str = str(file_path)
# Detect based on path patterns
if 'analyzing-' in path_str:
return 'skill-analysis'
elif any(x in path_str for x in ['mcp-', 'playwright', 'webapp-testing']):
return 'testing'
elif 'development' in path_str or 'tutorial' in path_str:
return 'tutorial'
else:
return 'article'
def _detect_languages(self, file_path: Path) -> List[str]:
"""Detect languages from file path."""
path_str = str(file_path)
languages = []
if '/en/' in path_str:
languages.append('en')
if '/zh/' in path_str:
languages.append('zh')
if '/fr/' in path_str:
languages.append('fr')
return languages if languages else ['en'] # Default to English
def validate_build(self) -> bool:
"""Run build to ensure files compile correctly."""
print("π§ Validating build...")
try:
result = subprocess.run(
['npm', 'run', 'build'],
cwd=self.project_root,
capture_output=True,
text=True,
timeout=300
)
if result.returncode != 0:
print("β Build validation failed:")
print(result.stderr)
return False
print("β
Build validation passed")
return True
except subprocess.TimeoutExpired:
print("β οΈ Build timeout (5 minutes)")
return False
except Exception as e:
print(f"β οΈ Could not run build: {str(e)}")
return True # Continue anyway
def validate_mdx(self, path: Path) -> bool:
"""Run MDX validation."""
print("π Running MDX validation...")
try:
result = subprocess.run(
['python', 'scripts/validate_mdx.py', str(path)],
cwd=self.project_root / '.claude/skills/skill-article-publisher',
capture_output=True,
text=True
)
# Print validation output
if result.stdout:
print(result.stdout)
if result.returncode != 0:
print("β MDX validation failed")
if result.stderr:
print(result.stderr)
return False
return True
except Exception as e:
print(f"β οΈ Could not run MDX validation: {str(e)}")
return True # Continue anyway
def generate_commit_message(self) -> str:
"""Generate semantic commit message based on detected changes."""
if not self.changes:
return f'{self.commit_type}: publish article'
# Group by type
by_type: Dict[str, List[str]] = {}
by_language: Dict[str, List[str]] = {}
for change in self.changes:
change_type = change['type']
file_name = Path(change['file']).name
if change_type not in by_type:
by_type[change_type] = []
by_type[change_type].append(file_name)
# Group by language
for lang in change['languages']:
if lang not in by_language:
by_language[lang] = []
by_language[lang].append(file_name)
# Determine primary change type
primary_type = self.commit_type
if 'skill-analysis' in by_type:
primary_type = 'feat'
elif len(self.changes) > 3:
primary_type = 'feat'
# Build message
message_lines = []
# Main commit line
if len(self.changes) == 1:
# Single file
change = self.changes[0]
file_name = Path(change['file']).name.replace('.mdx', '')
message_lines.append(f"{primary_type}: publish {file_name}")
else:
# Multiple files
type_summary = ', '.join(f"{len(files)} {t}" for t, files in by_type.items())
message_lines.append(f"{primary_type}: publish multiple articles ({type_summary})")
# Body with details
message_lines.append('')
# Group by type
for change_type, files in sorted(by_type.items()):
message_lines.append(f"{change_type.replace('-', ' ').title()}: {', '.join(files[:3])}{'...' if len(files) > 3 else ''}")
# Add language info if multiple languages
if len(by_language) > 1:
message_lines.append('')
message_lines.append('Languages: ' + ', '.join(sorted(by_language.keys())))
return '\n'.join(message_lines)
def stage_and_commit(self, message: str) -> bool:
"""Stage changes and create commit."""
print(f"\nπ Preparing commit...")
try:
# Stage all changes
subprocess.run(
['git', 'add', '-A'],
cwd=self.project_root,
check=True,
capture_output=True
)
# Check if there are staged changes
result = subprocess.run(
['git', 'diff', '--staged', '--name-only'],
cwd=self.project_root,
capture_output=True,
text=True,
check=True
)
if not result.stdout.strip():
print("β οΈ No changes to commit")
return False
# Create commit
subprocess.run(
['git', 'commit', '-m', message],
cwd=self.project_root,
check=True,
capture_output=True
)
print("β
Commit created successfully")
return True
except subprocess.CalledProcessError as e:
print(f"β Git operation failed: {str(e)}")
if e.stderr:
print(f"Error: {e.stderr.decode() if isinstance(e.stderr, bytes) else e.stderr}")
return False
def push_changes(self) -> bool:
"""Push changes to remote repository."""
print(f"\nπ Pushing to remote...")
try:
# Get current branch
result = subprocess.run(
['git', 'branch', '--show-current'],
cwd=self.project_root,
capture_output=True,
text=True,
check=True
)
branch = result.stdout.strip()
# Push
subprocess.run(
['git', 'push', 'origin', branch],
cwd=self.project_root,
check=True
)
print(f"β
Changes pushed to origin/{branch}")
return True
except subprocess.CalledProcessError as e:
print(f"β Push failed: {str(e)}")
return False
def print_summary(self):
"""Print summary of changes."""
print("\n" + "="*80)
print("ARTICLE PUBLISH SUMMARY")
print("="*80)
if self.changes:
print(f"\nπ Changes detected ({len(self.changes)} files):")
for change in self.changes:
languages = ', '.join(change['languages'])
print(f" - {change['file']} [{languages}]")
commit_msg = self.generate_commit_message()
print(f"\nπ Generated commit message:\n")
for line in commit_msg.split('\n'):
print(f" {line}")
print(f"\nπ¦ Actions:")
if self.changes:
if self.push:
print(" β
Validate MDX")
print(" β
Run build check")
print(" β
Create semantic commit")
print(" β
Push to remote")
else:
print(" β
Validate MDX")
print(" β
Run build check")
print(" β
Create semantic commit (dry run)")
print(" βοΈ Push (use --push to enable)")
else:
print(" β οΈ No changes detected")
print("="*80)
def main():
parser = argparse.ArgumentParser(description='Publish article with semantic commit and automated push')
parser.add_argument('path', help='Path to MDX file or directory to publish')
parser.add_argument('--push', action='store_true', help='Push changes to remote after commit')
parser.add_argument('--type', choices=['docs', 'feat', 'fix', 'chore'], default='docs',
help='Commit type for semantic commits (default: docs)')
parser.add_argument('--skip-build', action='store_true', help='Skip build validation')
parser.add_argument('--skip-mdx', action='store_true', help='Skip MDX validation')
args = parser.parse_args()
path = Path(args.path)
if not path.exists():
print(f"β Path does not exist: {path}")
sys.exit(1)
# Initialize publisher
publisher = ArticlePublisher(push=args.push, commit_type=args.type)
publisher.project_root = publisher.find_project_root(path)
print(f"π Project root: {publisher.project_root}")
# Detect changes
publisher.detect_changes(path)
if not publisher.changes:
print("β οΈ No MDX changes detected")
publisher.print_summary()
sys.exit(0)
# Validate build
if not args.skip_build:
if not publisher.validate_build():
print("\nβ Build validation failed. Fix errors before publishing.")
sys.exit(1)
else:
print("\nβοΈ Skipping build validation")
# Validate MDX
if not args.skip_mdx:
if not publisher.validate_mdx(path):
print("\nβ MDX validation failed. Fix errors before publishing.")
sys.exit(1)
else:
print("\nβοΈ Skipping MDX validation")
# Generate and show commit message
commit_msg = publisher.generate_commit_message()
publisher.print_summary()
# Confirm before commit
if args.push:
print("\n" + "="*80)
response = input("Proceed with commit and push? [y/N]: ").strip().lower()
if response not in ['y', 'yes']:
print("β Aborted")
sys.exit(0)
# Stage and commit
if publisher.stage_and_commit(commit_msg):
# Push if requested
if args.push:
if publisher.push_changes():
print("\nβ
Publish complete!")
else:
print("\nβ Commit succeeded but push failed")
sys.exit(1)
else:
print("\nβ
Commit created (dry run - use --push to actually push)")
else:
print("\nβ Commit failed")
sys.exit(1)
if __name__ == '__main__':
main()
```