mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-07-17 05:05:16 +03:00
Add GitHub Actions CI for SKILL.md validation and index updates
This commit is contained in:
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user