Skip to content
This repository was archived by the owner on Apr 5, 2026. It is now read-only.

docs(rules): fix [FE-CI] ESLint cmd, add fetchJSON check and persistence test requirement #526

docs(rules): fix [FE-CI] ESLint cmd, add fetchJSON check and persistence test requirement

docs(rules): fix [FE-CI] ESLint cmd, add fetchJSON check and persistence test requirement #526

Workflow file for this run

name: Lint
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
markdown:
name: Markdown lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: DavidAnson/markdownlint-cli2-action@v19
with:
globs: "**/*.md"
validate-skills:
name: Validate skill routing
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check skill routing targets exist
run: |
errors=0
for skill_md in claude/skills/*/SKILL.md; do
skill_dir=$(dirname "$skill_md")
for wf in $(grep -oP 'workflows/[a-z0-9-]+\.md' "$skill_md" | sort -u); do
if [ ! -f "$skill_dir/$wf" ]; then
echo "::error file=$skill_md::References $wf but file not found at $skill_dir/$wf"
errors=$((errors + 1))
fi
done
done
if [ "$errors" -gt 0 ]; then
echo "Found $errors missing workflow file(s)"
exit 1
fi
echo "All skill routing targets exist"
verify-skill-count:
name: Verify skill counts
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: README skill count matches directory count
run: |
ACTUAL=$(ls -d claude/skills/*/ | wc -l | tr -d ' ')
README_COUNT=$(grep -oP '\d+(?= skills)' README.md | head -1)
if [ "$ACTUAL" != "$README_COUNT" ]; then
echo "::error file=README.md::README says $README_COUNT skills but claude/skills/ has $ACTUAL directories. Update README."
exit 1
fi
echo "OK: $ACTUAL skills match README"
- name: Verify script skill list matches directories
run: |
LISTED=$(grep 'for skill in' setup/legacy/verify.sh | sed 's/.*for skill in //;s/;.*//' | tr ' ' '\n' | sort)
PRESENT=$(ls claude/skills/ | sort)
MISSING=$(comm -23 <(echo "$LISTED") <(echo "$PRESENT"))
if [ -n "$MISSING" ]; then
echo "::error file=setup/legacy/verify.sh::Skills listed in verify.sh but not found in claude/skills/: $MISSING"
exit 1
fi
UNLISTED=$(comm -13 <(echo "$LISTED") <(echo "$PRESENT"))
if [ -n "$UNLISTED" ]; then
echo "::warning::Skills in claude/skills/ not listed in verify.sh: $UNLISTED"
fi
echo "OK: verify.sh skill list is accurate"
validate-rule-metadata:
name: Validate rule metadata
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate lifecycle metadata in Tier 2 rules
run: |
errors=0
# Map of files to their entry prefix
declare -A FILE_PREFIX=(
["claude/rules/autolearn-patterns.md"]="AP"
["claude/rules/known-gotchas.md"]="KG"
)
# Collect all entry headings across both files for cross-reference validation
# Format: PREFIX#NUMBER for each ## N. heading found
declare -A ALL_ENTRIES
for file in "${!FILE_PREFIX[@]}"; do
prefix="${FILE_PREFIX[$file]}"
while IFS=: read -r num rest; do
ALL_ENTRIES["${prefix}#${num}"]=1
done < <(tr -d '\r' < "$file" | grep -P '^## (\d+)\.' | grep -oP '(?<=## )\d+')
done
for file in "${!FILE_PREFIX[@]}"; do
prefix="${FILE_PREFIX[$file]}"
echo "--- Validating $file (prefix: $prefix) ---"
# Strip Windows carriage returns for consistent parsing
# (files may be committed from Windows with CRLF endings)
clean_file=$(mktemp)
tr -d '\r' < "$file" > "$clean_file"
# -------------------------------------------------------
# (a) Validate **Added:** line format
# -------------------------------------------------------
while IFS=: read -r lineno line; do
# Expected: **Added:** YYYY-MM-DD | **Source:** <words> | **Status:** <status>
# Source accepts multi-word values (e.g., "Runbooks, RunNotes" or "Runbooks+RunNotes")
if ! echo "$line" | grep -qP '^\*\*Added:\*\* \d{4}-\d{2}-\d{2} \| \*\*Source:\*\* .+? \| \*\*Status:\*\* .+$'; then
echo "::error file=$file,line=$lineno::Malformed **Added:** line. Expected: **Added:** YYYY-MM-DD | **Source:** <name> | **Status:** <status>"
errors=$((errors + 1))
continue
fi
# Extract and validate date
date_val=$(echo "$line" | grep -oP '(?<=\*\*Added:\*\* )\d{4}-\d{2}-\d{2}')
if ! date -d "$date_val" >/dev/null 2>&1; then
echo "::error file=$file,line=$lineno::Invalid date '$date_val' in **Added:** line"
errors=$((errors + 1))
fi
# Extract and validate status
status_val=$(echo "$line" | grep -oP '(?<=\*\*Status:\*\* ).+$')
if [ "$status_val" = "active" ]; then
: # valid
elif echo "$status_val" | grep -qP '^deprecated \(\d{4}-\d{2}-\d{2}\) -- .+$'; then
: # valid deprecated format
elif echo "$status_val" | grep -qP '^superseded-by-(AP|KG)#\d+$'; then
: # valid superseded format -- note: uses PREFIX#N with hash
elif echo "$status_val" | grep -qP '^superseded-by-(AP|KG)\d+$'; then
: # also accept without hash for backward compat (superseded-by-KG17)
else
echo "::error file=$file,line=$lineno::Invalid status '$status_val'. Must be: active | deprecated (YYYY-MM-DD) -- reason | superseded-by-<PREFIX>#<N>"
errors=$((errors + 1))
fi
done < <(grep -nP '^\*\*Added:\*\*' "$clean_file")
# -------------------------------------------------------
# (b) Validate **See also:** references
# -------------------------------------------------------
while IFS=: read -r lineno line; do
# Extract all AP#N and KG#N references
refs=$(echo "$line" | grep -oP '(AP|KG)#\d+' || true)
for ref in $refs; do
ref_prefix=$(echo "$ref" | grep -oP '^(AP|KG)')
ref_num=$(echo "$ref" | grep -oP '\d+$')
# Determine which file to check
if [ "$ref_prefix" = "AP" ]; then
target_file="claude/rules/autolearn-patterns.md"
else
target_file="claude/rules/known-gotchas.md"
fi
# Verify the heading ## N. exists in the target file
# (use clean_file for the current file, read target fresh with tr)
if ! tr -d '\r' < "$target_file" | grep -qP "^## ${ref_num}\."; then
echo "::error file=$file,line=$lineno::Reference $ref points to non-existent entry (## ${ref_num}. not found in $target_file)"
errors=$((errors + 1))
fi
done
done < <(grep -nP '^\*\*See also:\*\*' "$clean_file")
# -------------------------------------------------------
# (c) Validate entry_count in frontmatter
# -------------------------------------------------------
# Read entry_count from YAML frontmatter
frontmatter_count=$(grep -P '^entry_count:\s*\d+' "$clean_file" | grep -oP '\d+' || echo "")
if [ -z "$frontmatter_count" ]; then
echo "::error file=$file,line=1::Missing entry_count in YAML frontmatter"
errors=$((errors + 1))
else
# Count all ## N. headings
total_entries=$(grep -cP '^## \d+\.' "$clean_file" || true)
# Count deprecated and superseded entries
inactive_entries=$(grep -cP '^\*\*Status:\*\* (deprecated|superseded)' "$clean_file" || true)
# Active = total - inactive
active_entries=$((total_entries - inactive_entries))
if [ "$active_entries" != "$frontmatter_count" ]; then
# Find the line number of entry_count for the annotation
ec_line=$(grep -nP '^entry_count:' "$clean_file" | head -1 | cut -d: -f1)
echo "::error file=$file,line=$ec_line::entry_count is $frontmatter_count but computed active entries = $active_entries (total=$total_entries, inactive=$inactive_entries)"
errors=$((errors + 1))
fi
fi
rm -f "$clean_file"
echo "--- Done: $file ---"
done
if [ "$errors" -gt 0 ]; then
echo "Found $errors metadata validation error(s)"
exit 1
fi
echo "All rule metadata is valid"
validate-templates:
name: Validate project templates
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Validate YAML templates
run: |
pip install pyyaml
python -c "
import yaml, glob, sys
errors = 0
for f in sorted(glob.glob('project-templates/*.yml')):
try:
with open(f) as fh:
yaml.safe_load(fh)
print(f'OK: {f}')
except yaml.YAMLError as e:
print(f'::error file={f}::Invalid YAML: {e}')
errors += 1
if errors:
sys.exit(1)
print(f'All {len(glob.glob(\"project-templates/*.yml\"))} YAML templates valid')
"
- name: Validate JSON templates
run: |
python -c "
import json, glob, sys
errors = 0
for f in sorted(glob.glob('project-templates/*.json')):
try:
with open(f) as fh:
json.load(fh)
print(f'OK: {f}')
except json.JSONDecodeError as e:
print(f'::error file={f}::Invalid JSON: {e}')
errors += 1
if errors:
sys.exit(1)
print(f'All {len(glob.glob(\"project-templates/*.json\"))} JSON templates valid')
"
- name: Validate other JSON configs
run: |
python -c "
import json, glob, sys
errors = 0
configs = glob.glob('claude/settings.template.json') + glob.glob('mcp/*.json')
for f in sorted(configs):
try:
with open(f) as fh:
json.load(fh)
print(f'OK: {f}')
except json.JSONDecodeError as e:
print(f'::error file={f}::Invalid JSON: {e}')
errors += 1
if errors:
sys.exit(1)
print(f'All {len(configs)} config JSON files valid')
"
powershell-lint:
name: PowerShell lint
runs-on: windows-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Install PSScriptAnalyzer
shell: pwsh
run: Install-Module -Name PSScriptAnalyzer -Force -Scope CurrentUser
- name: Lint PowerShell scripts
shell: pwsh
run: |
$files = Get-ChildItem -Path . -Recurse -Filter '*.ps1'
$results = $files | ForEach-Object {
Invoke-ScriptAnalyzer -Path $_.FullName `
-Severity Warning,Error `
-Settings '.\PSScriptAnalyzerSettings.psd1'
}
if ($results.Count -gt 0) {
$results | Format-Table -AutoSize
exit 1
}
Write-Host "PSScriptAnalyzer: no issues found"