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
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: 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" |