Files
Anthropic-Cybersecurity-Skills/.github/workflows/validate-skills.yml
T
Mahipal 0161fb1c7f ci: only check index.json freshness on pull requests
The index-freshness gate raced update-index.yml. Both trigger on a push to
main touching skills/**, so a merge that adds a skill runs the check against
the pre-merge index.json while update-index.yml is regenerating it. Merging
#129 turned main red for about fourteen minutes before the next push cleared
it, with nothing actually wrong.

The check still does its job where it matters - on pull requests, where the
contributor is the one who has to regenerate. On main, update-index.yml is
the mechanism that keeps it current, so verifying it in parallel only ever
produces a false red.
2026-08-24 12:56:02 +02:00

127 lines
5.0 KiB
YAML

name: Validate SKILL.md files
on:
push:
paths:
- 'skills/**'
- 'tools/**'
- '.github/workflows/validate-skills.yml'
pull_request:
paths:
- 'skills/**'
- 'tools/**'
- '.github/workflows/validate-skills.yml'
workflow_dispatch:
jobs:
validate:
runs-on: ubuntu-latest
name: Validate SKILL.md frontmatter
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: pip install pyyaml
# All frontmatter is parsed by tools/skill_frontmatter.py (PyYAML). Any
# reintroduced regex parser silently truncates multi-line descriptions --
# that bug shipped 604/817 broken descriptions before it was caught.
- name: Guard against hand-rolled YAML parsers
run: |
if grep -rnE '(re\.(search|match|compile)\([^)]*description|^\s*description:.*\(\.\*\))' \
tools/ --include='*.py' ; then
echo "::error::Regex-based frontmatter parsing detected. Use tools/skill_frontmatter.py."
exit 1
fi
echo "OK: no regex frontmatter parsers"
# Single source of truth: tools/validate-skill.py validates required
# frontmatter fields, kebab-case name, description length, subdomain, and
# tag count. (Previously this step duplicated a weaker inline parser.)
- name: Validate SKILL.md frontmatter
run: python3 tools/validate-skill.py --all
# agentskills.io conformance: name==directory, 1..1024 description,
# reserved-word ban, angle-bracket injection check.
- name: Validate agentskills.io conformance
run: python3 tools/validate-agentskills.py --strict
# index.json is generated; a PR that changes a description must regenerate it.
# Pull requests only: on a push to main, update-index.yml regenerates
# index.json in parallel with this job, so checking here would race and go
# red on every merge that adds a skill before self-healing seconds later.
- name: Check index.json is current
if: github.event_name == 'pull_request'
run: python3 tools/generate-index.py --check
# Description quality gate. Pre-existing failures are grandfathered in
# tools/lint-baseline.json so this blocks NEW debt only; the baseline is
# allowed to shrink and never to grow.
- name: Lint descriptions
run: python3 tools/lint-descriptions.py --all --stats
# Ratchet: the number of unreviewed near-duplicate description pairs may
# never increase. Lower this cap as disambiguation lands.
- name: Detect skill collisions
run: python3 tools/detect-collisions.py --max-unreviewed 55
- 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