fix: use ASCII hyphens in Safe Destroy Protocol guidance (#45) #98
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Validate Skill | |
| on: | |
| pull_request: | |
| paths: | |
| - 'skills/**' | |
| - '.claude-plugin/**' | |
| - '.codex-plugin/**' | |
| - 'POWER.md' | |
| - 'mcp.json' | |
| - '.github/workflows/**' | |
| - '.github/release/**' | |
| push: | |
| branches: [master, main] | |
| paths: | |
| - 'skills/**' | |
| - '.claude-plugin/**' | |
| - '.codex-plugin/**' | |
| - 'POWER.md' | |
| - 'mcp.json' | |
| - '.github/workflows/**' | |
| - '.github/release/**' | |
| workflow_dispatch: | |
| jobs: | |
| validate: | |
| name: Validate Skill Files | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.x' | |
| - name: Install Dependencies | |
| run: pip install pyyaml | |
| - name: Check SKILL.md Frontmatter | |
| run: | | |
| python3 << 'EOF' | |
| import yaml | |
| import sys | |
| import re | |
| print("🔍 Validating SKILL.md frontmatter...") | |
| with open('skills/terraform-skill/SKILL.md', 'r') as f: | |
| content = f.read() | |
| if not content.startswith('---'): | |
| print("❌ ERROR: No frontmatter found") | |
| sys.exit(1) | |
| parts = content.split('---', 2) | |
| if len(parts) < 3: | |
| print("❌ ERROR: Invalid frontmatter format") | |
| sys.exit(1) | |
| frontmatter = yaml.safe_load(parts[1]) | |
| # Check required fields (but allow additional optional fields) | |
| required = {'name', 'description'} | |
| missing = required - set(frontmatter.keys()) | |
| if missing: | |
| print(f"❌ ERROR: Missing required fields: {missing}") | |
| sys.exit(1) | |
| # Log optional fields if present (informational only) | |
| optional_fields = set(frontmatter.keys()) - required | |
| if optional_fields: | |
| print(f"📋 Optional fields present: {optional_fields}") | |
| if 'license' in frontmatter: | |
| print(f" - license: {frontmatter['license']}") | |
| if 'metadata' in frontmatter: | |
| metadata = frontmatter['metadata'] | |
| if isinstance(metadata, dict): | |
| if 'version' in metadata: | |
| print(f" - metadata.version: {metadata['version']}") | |
| if 'author' in metadata: | |
| print(f" - metadata.author: {metadata['author']}") | |
| name = frontmatter['name'] | |
| if not re.match(r'^[a-zA-Z0-9-]+$', name): | |
| print(f"❌ ERROR: Invalid name: {name}") | |
| sys.exit(1) | |
| desc_len = len(frontmatter['description']) | |
| if desc_len > 1024: | |
| print(f"❌ ERROR: Description too long: {desc_len} chars") | |
| sys.exit(1) | |
| print(f"✅ Frontmatter valid ({desc_len} chars)") | |
| EOF | |
| - name: Check Codex Manifest Version Sync | |
| run: | | |
| python3 << 'EOF' | |
| import json | |
| import os | |
| import re | |
| import sys | |
| import yaml | |
| SEMVER = re.compile(r'^\d+\.\d+\.\d+$') | |
| manifest_path = '.codex-plugin/plugin.json' | |
| if not os.path.exists(manifest_path): | |
| print("ℹ️ No .codex-plugin/plugin.json; skipping sync check") | |
| sys.exit(0) | |
| with open('skills/terraform-skill/SKILL.md') as f: | |
| fm = yaml.safe_load(f.read().split('---', 2)[1]) | |
| skill_version = (fm.get('metadata') or {}).get('version') | |
| with open(manifest_path) as f: | |
| manifest_version = json.load(f).get('version') | |
| # Both must exist and be real semver, not merely equal: equality | |
| # alone passes when both are missing/empty. | |
| for label, value in ( | |
| ('SKILL.md metadata.version', skill_version), | |
| (f'{manifest_path} version', manifest_version), | |
| ): | |
| if not (isinstance(value, str) and SEMVER.match(value)): | |
| print( | |
| f"❌ ERROR: {label} is not a valid semver: {value!r}. " | |
| f"CI owns these; do not hand-edit (see CLAUDE.md).") | |
| sys.exit(1) | |
| if skill_version != manifest_version: | |
| print( | |
| f"❌ ERROR: version mismatch - SKILL.md metadata.version=" | |
| f"{skill_version!r} vs {manifest_path}={manifest_version!r}. " | |
| f"CI owns these; do not hand-edit (see CLAUDE.md).") | |
| sys.exit(1) | |
| print(f"✅ Codex manifest version in sync ({manifest_version})") | |
| EOF | |
| - name: Check POWER.md (Kiro) Sync | |
| run: | | |
| set -e | |
| echo "🔍 Regenerating POWER.md and checking it is committed in sync..." | |
| node .github/release/build-power.js --check | |
| python3 << 'EOF' | |
| import json, re, sys | |
| import yaml | |
| content = open('POWER.md').read() | |
| if not content.startswith('---'): | |
| print("❌ ERROR: POWER.md has no frontmatter") | |
| sys.exit(1) | |
| fm = yaml.safe_load(content.split('---', 2)[1]) | |
| required = {'name', 'displayName', 'description', 'keywords', | |
| 'author', 'version'} | |
| missing = required - set(fm.keys()) | |
| if missing: | |
| print(f"❌ ERROR: POWER.md frontmatter missing {missing}") | |
| sys.exit(1) | |
| if not isinstance(fm['keywords'], list) or not fm['keywords']: | |
| print("❌ ERROR: POWER.md keywords must be a non-empty list") | |
| sys.exit(1) | |
| if not re.match(r'^\d+\.\d+\.\d+$', str(fm['version'])): | |
| print(f"❌ ERROR: POWER.md version not semver: {fm['version']!r}") | |
| sys.exit(1) | |
| mcp = json.load(open('mcp.json')) | |
| servers = list((mcp.get('mcpServers') or {}).keys()) | |
| if not servers: | |
| print("❌ ERROR: mcp.json has no mcpServers") | |
| sys.exit(1) | |
| for s in servers: | |
| if s not in content: | |
| print(f"❌ ERROR: mcp.json server {s!r} not referenced " | |
| f"in POWER.md") | |
| sys.exit(1) | |
| print(f"✅ POWER.md in sync (v{fm['version']}, " | |
| f"{len(fm['keywords'])} keywords, MCP: {', '.join(servers)})") | |
| EOF | |
| - name: Check File Size | |
| run: | | |
| LINES=$(wc -l < skills/terraform-skill/SKILL.md) | |
| WORDS=$(wc -w < skills/terraform-skill/SKILL.md) | |
| echo "📊 SKILL.md: $LINES lines, $WORDS words" | |
| if [ $LINES -gt 500 ]; then | |
| echo "⚠️ WARNING: $LINES lines (guideline: <500)" | |
| else | |
| echo "✅ Size OK" | |
| fi | |
| - name: Check for Broken Links | |
| run: | | |
| echo "🔍 Checking internal links..." | |
| cd skills/terraform-skill | |
| broken=0 | |
| while read -r link; do | |
| if [ ! -f "$link" ]; then | |
| echo "❌ ERROR: Broken link: $link" | |
| broken=1 | |
| fi | |
| done < <(grep -oP '\[.*?\]\(references/.*?\.md.*?\)' SKILL.md references/*.md 2>/dev/null | \ | |
| sed 's/.*(//' | sed 's/).*//' | sed 's/#.*//') | |
| if [ "$broken" -ne 0 ]; then | |
| exit 1 | |
| fi | |
| echo "✅ No broken links" | |
| - name: Lint Markdown | |
| uses: DavidAnson/markdownlint-cli2-action@v16 | |
| with: | |
| globs: | | |
| skills/**/*.md | |
| README.md | |
| CONTRIBUTING.md | |
| continue-on-error: true | |
| - name: Summary | |
| if: success() | |
| run: | | |
| echo "## ✅ Validation Passed" >> $GITHUB_STEP_SUMMARY | |
| echo "All skill validation checks passed." >> $GITHUB_STEP_SUMMARY |