feat(hooks): CI annotations and SARIF upload for scanner findings #103
Workflow file for this run
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 Agent Skills | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - "agent-skills/**" | |
| - ".claude-plugin/**" | |
| pull_request: | |
| branches: [main] | |
| paths: | |
| - "agent-skills/**" | |
| - ".claude-plugin/**" | |
| jobs: | |
| validate-skills: | |
| name: Validate agent skills structure and size | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Validate AGENTS.md exists | |
| run: | | |
| if [ ! -f "agent-skills/AGENTS.md" ]; then | |
| echo "::error::Missing agent-skills/AGENTS.md" | |
| exit 1 | |
| fi | |
| echo "AGENTS.md exists" | |
| - name: Validate plugin directory structure | |
| run: | | |
| ERRORS=0 | |
| if [ ! -f "agent-skills/.claude-plugin/plugin.json" ]; then | |
| echo "::error::Missing agent-skills/.claude-plugin/plugin.json" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| if [ ! -d "agent-skills/skills" ]; then | |
| echo "::error::Missing agent-skills/skills/ directory" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| if [ $ERRORS -gt 0 ]; then | |
| echo "Found $ERRORS plugin structure error(s)" | |
| exit 1 | |
| fi | |
| echo "Plugin directory has correct structure" | |
| - name: Validate marketplace.json exists | |
| run: | | |
| if [ ! -f ".claude-plugin/marketplace.json" ]; then | |
| echo "::error::Missing .claude-plugin/marketplace.json" | |
| exit 1 | |
| fi | |
| echo "marketplace.json exists" | |
| - name: Validate SKILL.md frontmatter | |
| run: | | |
| ERRORS=0 | |
| while IFS= read -r -d '' f; do | |
| FIRST_LINE=$(head -1 "$f") | |
| if [ "$FIRST_LINE" != "---" ]; then | |
| echo "::error file=$f::Missing frontmatter delimiter (first line must be ---)" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| if ! grep -q "^name:" "$f"; then | |
| echo "::error file=$f::Missing required 'name:' field in frontmatter" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| if ! grep -q "^description:" "$f"; then | |
| echo "::error file=$f::Missing required 'description:' field in frontmatter" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| done < <(find agent-skills -name "SKILL.md" -print0) | |
| if [ $ERRORS -gt 0 ]; then | |
| echo "Found $ERRORS SKILL.md validation error(s)" | |
| exit 1 | |
| fi | |
| echo "All SKILL.md files have valid frontmatter" | |
| - name: Validate skill directories contain SKILL.md | |
| run: | | |
| ERRORS=0 | |
| while IFS= read -r -d '' dir; do | |
| SKILL_NAME=$(basename "$dir") | |
| if [[ "$SKILL_NAME" == atmos-* ]] && [ ! -f "$dir/SKILL.md" ]; then | |
| echo "::error::Missing SKILL.md in $dir" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| done < <(find agent-skills/skills -mindepth 1 -maxdepth 1 -type d -print0) | |
| if [ $ERRORS -gt 0 ]; then | |
| echo "Found $ERRORS missing SKILL.md file(s)" | |
| exit 1 | |
| fi | |
| echo "All skill directories contain SKILL.md" | |
| - name: Check SKILL.md line count (max 500 lines per spec) | |
| run: | | |
| # Agent Skills spec recommends SKILL.md under 500 lines | |
| # https://agentskills.io/specification#progressive-disclosure | |
| MAX_LINES=500 | |
| ERRORS=0 | |
| while IFS= read -r -d '' f; do | |
| LINES=$(wc -l < "$f") | |
| if [ "$LINES" -gt "$MAX_LINES" ]; then | |
| echo "::error file=$f::$LINES lines exceeds ${MAX_LINES}-line limit. Move detailed content to references/" | |
| ERRORS=$((ERRORS + 1)) | |
| elif [ "$LINES" -gt 450 ]; then | |
| echo "::warning file=$f::$LINES lines, approaching ${MAX_LINES}-line limit" | |
| fi | |
| done < <(find agent-skills -name "SKILL.md" -print0) | |
| if [ $ERRORS -gt 0 ]; then | |
| echo "Found $ERRORS SKILL.md file(s) exceeding the ${MAX_LINES}-line limit" | |
| exit 1 | |
| fi | |
| echo "All SKILL.md files are within the ${MAX_LINES}-line limit" | |
| - name: Check file sizes (max 20KB for SKILL.md, max 25KB for references) | |
| run: | | |
| # Agent Skills spec recommends < 5000 tokens for SKILL.md body (~20KB) | |
| # Reference files are loaded on demand so get a slightly higher limit | |
| SKILL_MAX=20480 # 20KB for SKILL.md | |
| REF_MAX=25600 # 25KB for reference files | |
| SKILL_WARN=18432 # 18KB warning threshold | |
| REF_WARN=23552 # 23KB warning threshold | |
| ERRORS=0 | |
| # Check SKILL.md files | |
| while IFS= read -r -d '' f; do | |
| SIZE=$(wc -c < "$f") | |
| if [ "$SIZE" -gt "$SKILL_MAX" ]; then | |
| echo "::error file=$f::${SIZE} bytes exceeds ${SKILL_MAX}-byte limit. Move detailed content to references/" | |
| ERRORS=$((ERRORS + 1)) | |
| elif [ "$SIZE" -gt "$SKILL_WARN" ]; then | |
| echo "::warning file=$f::${SIZE} bytes, approaching ${SKILL_MAX}-byte limit" | |
| fi | |
| done < <(find agent-skills -name "SKILL.md" -print0) | |
| # Check reference files | |
| while IFS= read -r -d '' f; do | |
| SIZE=$(wc -c < "$f") | |
| if [ "$SIZE" -gt "$REF_MAX" ]; then | |
| echo "::error file=$f::${SIZE} bytes exceeds ${REF_MAX}-byte limit. Split into smaller reference files" | |
| ERRORS=$((ERRORS + 1)) | |
| elif [ "$SIZE" -gt "$REF_WARN" ]; then | |
| echo "::warning file=$f::${SIZE} bytes, approaching ${REF_MAX}-byte limit" | |
| fi | |
| done < <(find agent-skills/skills/*/references -name "*.md" -print0 2>/dev/null) | |
| # Check AGENTS.md (same limit as reference files) | |
| if [ -f "agent-skills/AGENTS.md" ]; then | |
| SIZE=$(wc -c < "agent-skills/AGENTS.md") | |
| if [ "$SIZE" -gt "$REF_MAX" ]; then | |
| echo "::error file=agent-skills/AGENTS.md::${SIZE} bytes exceeds ${REF_MAX}-byte limit" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| fi | |
| if [ $ERRORS -gt 0 ]; then | |
| echo "Found $ERRORS file(s) exceeding size limits" | |
| exit 1 | |
| fi | |
| echo "All skill files are within size limits" | |
| - name: Validate code fence language tags | |
| run: | | |
| ERRORS=0 | |
| while IFS= read -r -d '' f; do | |
| # Find opening code fences without language tags | |
| # Track open/close state to skip closing fences | |
| UNLABELED=$(awk ' | |
| /^```$/ { | |
| if (!in_block) { print NR": "$0 } | |
| in_block = !in_block | |
| next | |
| } | |
| /^```[a-zA-Z]/ { in_block = 1; next } | |
| /^```[^a-zA-Z]/ { print NR": "$0; next } | |
| /^``` / { print NR": "$0; next } | |
| ' "$f") | |
| if [ -n "$UNLABELED" ]; then | |
| echo "::error file=$f::Unlabeled code fences found (add language tags like yaml, bash, text, json)" | |
| echo "$UNLABELED" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| done < <(find agent-skills -name "*.md" -print0) | |
| if [ $ERRORS -gt 0 ]; then | |
| echo "Found $ERRORS file(s) with unlabeled code fences" | |
| exit 1 | |
| fi | |
| echo "All code fences have language tags" | |
| - name: Validate plugin.json files | |
| run: | | |
| ERRORS=0 | |
| while IFS= read -r -d '' f; do | |
| if ! python3 -c "import json; json.load(open('$f'))" 2>/dev/null; then | |
| echo "::error file=$f::Invalid JSON syntax" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| done < <(find agent-skills -name "plugin.json" -print0) | |
| # Also validate marketplace.json | |
| if [ -f ".claude-plugin/marketplace.json" ]; then | |
| if ! python3 -c "import json; json.load(open('.claude-plugin/marketplace.json'))" 2>/dev/null; then | |
| echo "::error file=.claude-plugin/marketplace.json::Invalid JSON syntax" | |
| ERRORS=$((ERRORS + 1)) | |
| fi | |
| fi | |
| if [ $ERRORS -gt 0 ]; then | |
| echo "Found $ERRORS invalid JSON file(s)" | |
| exit 1 | |
| fi | |
| echo "All JSON manifests are valid" |