diff --git a/.github/workflows/update-index.yml b/.github/workflows/update-index.yml new file mode 100644 index 00000000..7ed2bee6 --- /dev/null +++ b/.github/workflows/update-index.yml @@ -0,0 +1,90 @@ +name: Update marketplace index + +on: + push: + branches: [main] + paths: + - 'skills/**' + +jobs: + update-index: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Regenerate index.json + run: | + python3 << 'EOF' + import os, json, re + from datetime import datetime, timezone + from collections import Counter + + skills_dir = "skills" + skills = [] + subdomain_counts = Counter() + tag_counter = Counter() + + for skill_name in sorted(os.listdir(skills_dir)): + skill_md = os.path.join(skills_dir, skill_name, "SKILL.md") + if not os.path.isfile(skill_md): + continue + with open(skill_md, "r", encoding="utf-8") as f: + content = f.read() + fm_match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL) + if not fm_match: + continue + fm = fm_match.group(1) + def get_field(field, text): + m = re.search(rf"^{field}:\s*(.+)$", text, re.MULTILINE) + return m.group(1).strip().strip('"') if m else "" + def get_tags(text): + m = re.search(r"^tags:\s*\[(.+)\]", text, re.MULTILINE) + return [t.strip() for t in m.group(1).split(",")] if m else [] + + tags = get_tags(fm) + subdomain = get_field("subdomain", fm) + subdomain_counts[subdomain] += 1 + for t in tags: + tag_counter[t] += 1 + + skills.append({ + "name": get_field("name", fm), + "description": get_field("description", fm), + "domain": "cybersecurity", + "subdomain": subdomain, + "tags": tags, + "version": get_field("version", fm) or "1.0", + "author": "mukul975", + "license": "Apache-2.0", + "path": f"skills/{skill_name}" + }) + + top_tags = sorted(tag_counter.items(), key=lambda x: -x[1])[:20] + index = { + "version": "1.0.0", + "generated_at": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + "repository": "https://github.com/mukul975/Anthropic-Cybersecurity-Skills", + "total_skills": len(skills), + "total_domains": 1, + "total_subdomains": len(subdomain_counts), + "domain_stats": {"cybersecurity": len(skills)}, + "subdomain_stats": dict(subdomain_counts), + "top_tags": [{"tag": t, "count": c} for t, c in top_tags], + "skills": skills + } + + with open("index.json", "w", encoding="utf-8") as f: + json.dump(index, f, indent=2) + + print(f"Updated index.json: {len(skills)} skills, {len(subdomain_counts)} subdomains") + EOF + + - name: Commit updated index + run: | + git config user.name "mukul975" + git config user.email "mukul975@users.noreply.github.com" + git add index.json + git diff --staged --quiet || git commit -m "chore: auto-update index.json" + git push diff --git a/.github/workflows/validate-skills.yml b/.github/workflows/validate-skills.yml new file mode 100644 index 00000000..5497aef0 --- /dev/null +++ b/.github/workflows/validate-skills.yml @@ -0,0 +1,128 @@ +name: Validate SKILL.md files + +on: + push: + paths: + - 'skills/**' + pull_request: + paths: + - 'skills/**' + +jobs: + validate: + runs-on: ubuntu-latest + name: Validate SKILL.md frontmatter + steps: + - uses: actions/checkout@v4 + + - name: Validate SKILL.md frontmatter with Python + run: | + python3 << 'EOF' + import os + import re + import sys + + REQUIRED_FIELDS = ['name', 'description', 'domain', 'subdomain', 'tags', 'version', 'author', 'license'] + errors = [] + checked = 0 + + for root, dirs, files in os.walk('skills'): + for file in files: + if file == 'SKILL.md': + path = os.path.join(root, file) + checked += 1 + with open(path, 'r', encoding='utf-8') as f: + content = f.read() + + # Check frontmatter exists + fm_match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL) + if not fm_match: + errors.append(f"{path}: Missing YAML frontmatter") + continue + + fm = fm_match.group(1) + + # Check required fields + for field in REQUIRED_FIELDS: + if not re.search(rf'^{field}:', fm, re.MULTILINE): + errors.append(f"{path}: Missing required field '{field}'") + + # Check name format (kebab-case) + name_match = re.search(r'^name:\s*(.+)$', fm, re.MULTILINE) + if name_match: + name = name_match.group(1).strip().strip('"') + if not re.match(r'^[a-z0-9-]+$', name): + errors.append(f"{path}: Name '{name}' must be kebab-case") + if len(name) > 64: + errors.append(f"{path}: Name '{name}' exceeds 64 characters") + + print(f"Checked {checked} SKILL.md files") + + if errors: + print(f"\n{len(errors)} validation error(s):") + for e in errors: + print(f" ❌ {e}") + sys.exit(1) + else: + print(f"✅ All {checked} skills valid") + EOF + + - name: Check for duplicate skill names + run: | + python3 << 'EOF' + import os + import re + from collections import Counter + + names = [] + for root, dirs, files in os.walk('skills'): + for file in files: + if file == 'SKILL.md': + path = os.path.join(root, file) + with open(path, 'r', encoding='utf-8') as f: + content = f.read() + fm_match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL) + if fm_match: + name_match = re.search(r'^name:\s*(.+)$', fm_match.group(1), re.MULTILINE) + if name_match: + names.append(name_match.group(1).strip().strip('"')) + + duplicates = [name for name, count in Counter(names).items() if count > 1] + if duplicates: + print(f"❌ Duplicate skill names found: {duplicates}") + exit(1) + print(f"✅ No duplicate names in {len(names)} skills") + EOF + + - name: Report skill counts + if: always() + run: | + echo "## Skill Database Stats" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + python3 << 'EOF' + import os + import re + from collections import Counter + + subdomain_counts = Counter() + total = 0 + for root, dirs, files in os.walk('skills'): + for file in files: + if file == 'SKILL.md': + total += 1 + path = os.path.join(root, file) + with open(path, 'r', encoding='utf-8') as f: + content = f.read() + fm_match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL) + if fm_match: + sd_match = re.search(r'^subdomain:\s*(.+)$', fm_match.group(1), re.MULTILINE) + if sd_match: + subdomain_counts[sd_match.group(1).strip()] += 1 + + print(f"**Total Skills: {total}**") + print("") + print("| Subdomain | Count |") + print("|-----------|-------|") + for sd, count in sorted(subdomain_counts.items(), key=lambda x: -x[1]): + print(f"| {sd} | {count} |") + EOF