Revert "chore: force Node.js 24 for all GitHub Actions" #44
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 | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| env: | |
| DOTNET_VERSION: '10.0.x' | |
| DOTNET_NOLOGO: true | |
| DOTNET_CLI_TELEMETRY_OPTOUT: true | |
| jobs: | |
| validate-skills: | |
| name: Validate Skills | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Check SKILL.md frontmatter | |
| run: | | |
| errors=0 | |
| for skill in skills/*/SKILL.md; do | |
| # Check YAML frontmatter exists | |
| if ! head -1 "$skill" | grep -q "^---$"; then | |
| echo "ERROR: $skill missing YAML frontmatter" | |
| errors=$((errors + 1)) | |
| fi | |
| # Check required fields | |
| if ! grep -q "^name:" "$skill"; then | |
| echo "ERROR: $skill missing 'name' in frontmatter" | |
| errors=$((errors + 1)) | |
| fi | |
| if ! grep -q "^description:" "$skill"; then | |
| echo "ERROR: $skill missing 'description' in frontmatter" | |
| errors=$((errors + 1)) | |
| fi | |
| # Check line count (max 400) | |
| lines=$(wc -l < "$skill") | |
| if [ "$lines" -gt 400 ]; then | |
| echo "ERROR: $skill has $lines lines (max 400)" | |
| errors=$((errors + 1)) | |
| fi | |
| # Check required sections | |
| for section in "Core Principles" "Patterns" "Anti-patterns" "Decision Guide"; do | |
| if ! grep -q "## $section" "$skill"; then | |
| echo "WARNING: $skill missing '## $section' section" | |
| fi | |
| done | |
| done | |
| if [ $errors -gt 0 ]; then | |
| echo "$errors error(s) found" | |
| exit 1 | |
| fi | |
| echo "All skills validated successfully" | |
| validate-commands: | |
| name: Validate Commands | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Check command frontmatter and structure | |
| run: | | |
| errors=0 | |
| if [ ! -d "commands" ] || [ -z "$(ls commands/*.md 2>/dev/null)" ]; then | |
| echo "WARNING: No command files found in commands/" | |
| exit 0 | |
| fi | |
| for cmd in commands/*.md; do | |
| # Check YAML frontmatter exists | |
| if ! head -1 "$cmd" | grep -q "^---$"; then | |
| echo "ERROR: $cmd missing YAML frontmatter" | |
| errors=$((errors + 1)) | |
| fi | |
| # Check description field | |
| if ! grep -q "^description:" "$cmd"; then | |
| echo "ERROR: $cmd missing 'description' in frontmatter" | |
| errors=$((errors + 1)) | |
| fi | |
| # Check line count (max 200) | |
| lines=$(wc -l < "$cmd") | |
| if [ "$lines" -gt 200 ]; then | |
| echo "ERROR: $cmd has $lines lines (max 200)" | |
| errors=$((errors + 1)) | |
| fi | |
| # Check required sections | |
| for section in "What" "When" "How"; do | |
| if ! grep -q "## $section" "$cmd"; then | |
| echo "WARNING: $cmd missing '## $section' section" | |
| fi | |
| done | |
| done | |
| if [ $errors -gt 0 ]; then | |
| echo "$errors error(s) found" | |
| exit 1 | |
| fi | |
| count=$(ls commands/*.md 2>/dev/null | wc -l) | |
| echo "All $count commands validated successfully" | |
| validate-rules: | |
| name: Validate Rules | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Check rule frontmatter and size | |
| run: | | |
| errors=0 | |
| if [ ! -d ".claude/rules" ] && [ ! -d "rules" ]; then | |
| echo "WARNING: No rules directory found" | |
| exit 0 | |
| fi | |
| for rule in $(find .claude/rules rules -name "*.md" -type f 2>/dev/null); do | |
| # Check YAML frontmatter exists | |
| if ! head -1 "$rule" | grep -q "^---$"; then | |
| echo "ERROR: $rule missing YAML frontmatter" | |
| errors=$((errors + 1)) | |
| fi | |
| # Check alwaysApply: true | |
| if ! grep -q "^alwaysApply: true" "$rule"; then | |
| echo "ERROR: $rule missing 'alwaysApply: true' in frontmatter" | |
| errors=$((errors + 1)) | |
| fi | |
| # Check line count (max 100) | |
| lines=$(wc -l < "$rule") | |
| if [ "$lines" -gt 100 ]; then | |
| echo "ERROR: $rule has $lines lines (max 100)" | |
| errors=$((errors + 1)) | |
| fi | |
| done | |
| if [ $errors -gt 0 ]; then | |
| echo "$errors error(s) found" | |
| exit 1 | |
| fi | |
| count=$(find rules -name "*.md" -type f | wc -l) | |
| echo "All $count rules validated successfully" | |
| validate-agents: | |
| name: Validate Agents | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Check agent structure | |
| run: | | |
| errors=0 | |
| for agent in agents/*.md; do | |
| # Check required sections | |
| for section in "Role Definition" "Skill Dependencies" "MCP Tool Usage" "Boundaries"; do | |
| if ! grep -q "## $section" "$agent"; then | |
| echo "ERROR: $agent missing '## $section' section" | |
| errors=$((errors + 1)) | |
| fi | |
| done | |
| # Check that referenced skills exist | |
| while IFS= read -r skill; do | |
| if [ ! -f "skills/$skill/SKILL.md" ]; then | |
| echo "WARNING: $agent references skill '$skill' but skills/$skill/SKILL.md not found" | |
| fi | |
| done < <(grep -oP '`(\w[\w-]+)`' "$agent" | tr -d '`' | sort -u | while read s; do | |
| if [ -d "skills/$s" ]; then echo "$s"; fi | |
| done) | |
| done | |
| if [ $errors -gt 0 ]; then | |
| echo "$errors error(s) found" | |
| exit 1 | |
| fi | |
| count=$(ls agents/*.md 2>/dev/null | wc -l) | |
| echo "All $count agents validated successfully" | |
| validate-cross-references: | |
| name: Validate Cross-References | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Check skill references resolve | |
| run: | | |
| errors=0 | |
| # Check commands reference existing skills | |
| for cmd in commands/*.md; do | |
| while IFS= read -r skill; do | |
| if [ ! -f "skills/$skill/SKILL.md" ]; then | |
| echo "WARNING: $cmd references skill '$skill' — not found" | |
| fi | |
| done < <(grep -oP 'skills/[\w-]+' "$cmd" 2>/dev/null | sed 's|skills/||' | sort -u) | |
| done | |
| # Check AGENTS.md references existing skills and agents | |
| while IFS= read -r agent_file; do | |
| if [ ! -f "$agent_file" ]; then | |
| echo "ERROR: AGENTS.md references '$agent_file' but it does not exist" | |
| errors=$((errors + 1)) | |
| fi | |
| done < <(grep -oP 'agents/[\w-]+\.md' AGENTS.md 2>/dev/null | sort -u) | |
| if [ $errors -gt 0 ]; then | |
| echo "$errors error(s) found" | |
| exit 1 | |
| fi | |
| echo "Cross-references validated" | |
| validate-hooks: | |
| name: Validate Hooks | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Check hook scripts exist and are valid | |
| run: | | |
| errors=0 | |
| # Extract script paths from hooks.json | |
| scripts=$(python3 << 'PYEOF' | |
| import json, re | |
| d = json.load(open('hooks/hooks.json')) | |
| for event, entries in d.get('hooks', {}).items(): | |
| for entry in entries: | |
| for hook in entry.get('hooks', []): | |
| cmd = hook.get('command', '') | |
| clean = cmd.replace('${CLAUDE_PLUGIN_ROOT}/', '') | |
| for match in re.findall(r'[\w][\w/.-]*\.sh', clean): | |
| print(match) | |
| PYEOF | |
| ) | |
| for script in $scripts; do | |
| if [ ! -f "$script" ]; then | |
| echo "ERROR: hooks.json references '$script' but it does not exist" | |
| errors=$((errors + 1)) | |
| else | |
| echo "OK: $script" | |
| # Basic syntax check | |
| if command -v bash > /dev/null; then | |
| if ! bash -n "$script" 2>/dev/null; then | |
| echo "ERROR: $script has syntax errors" | |
| errors=$((errors + 1)) | |
| fi | |
| fi | |
| fi | |
| done | |
| if [ $errors -gt 0 ]; then | |
| echo "$errors error(s) found" | |
| exit 1 | |
| fi | |
| echo "All hook scripts validated" | |
| validate-plugin: | |
| name: Validate Plugin Manifests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Validate JSON syntax | |
| run: | | |
| errors=0 | |
| for f in .claude-plugin/plugin.json .claude-plugin/marketplace.json .mcp.json hooks/hooks.json; do | |
| if ! python3 -m json.tool "$f" > /dev/null 2>&1; then | |
| echo "ERROR: $f is not valid JSON" | |
| errors=$((errors + 1)) | |
| else | |
| echo "OK: $f" | |
| fi | |
| done | |
| if [ $errors -gt 0 ]; then | |
| echo "$errors JSON error(s) found" | |
| exit 1 | |
| fi | |
| - name: Validate plugin.json required keys | |
| run: | | |
| errors=0 | |
| for key in name description; do | |
| if ! python3 -c "import json,sys; d=json.load(open('.claude-plugin/plugin.json')); assert '$key' in d" 2>/dev/null; then | |
| echo "ERROR: plugin.json missing required key '$key'" | |
| errors=$((errors + 1)) | |
| fi | |
| done | |
| if [ $errors -gt 0 ]; then | |
| exit 1 | |
| fi | |
| echo "plugin.json keys validated" | |
| - name: Validate component directories exist | |
| run: | | |
| errors=0 | |
| for path in skills/ agents/ commands/ .claude/rules/ hooks/hooks.json .mcp.json; do | |
| if [ ! -e "$path" ]; then | |
| echo "ERROR: Expected '$path' but it does not exist" | |
| errors=$((errors + 1)) | |
| else | |
| echo "OK: $path" | |
| fi | |
| done | |
| if [ $errors -gt 0 ]; then | |
| exit 1 | |
| fi | |
| echo "All component directories exist" | |
| - name: Validate .mcp.json has server entries | |
| run: | | |
| count=$(python3 -c "import json; d=json.load(open('.mcp.json')); print(len(d.get('mcpServers',{})))") | |
| if [ "$count" -eq 0 ]; then | |
| echo "ERROR: .mcp.json has no MCP server entries" | |
| exit 1 | |
| fi | |
| echo ".mcp.json has $count server(s)" | |
| - name: Validate hooks.json structure | |
| run: | | |
| python3 -c " | |
| import json, sys | |
| d = json.load(open('hooks/hooks.json')) | |
| hooks = d.get('hooks', {}) | |
| if not hooks: | |
| print('ERROR: hooks.json has no hook events') | |
| sys.exit(1) | |
| for event, entries in hooks.items(): | |
| for entry in entries: | |
| if 'matcher' not in entry: | |
| print(f'ERROR: {event} entry missing matcher') | |
| sys.exit(1) | |
| if 'hooks' not in entry or not entry['hooks']: | |
| print(f'ERROR: {event} matcher \"{entry[\"matcher\"]}\" has no hooks') | |
| sys.exit(1) | |
| print('hooks.json structure validated') | |
| " | |
| build-mcp: | |
| name: Build MCP Server | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v5 | |
| with: | |
| dotnet-version: ${{ env.DOTNET_VERSION }} | |
| - name: Restore | |
| run: dotnet restore mcp/CWM.RoslynNavigator/CWM.RoslynNavigator.slnx | |
| - name: Build | |
| run: dotnet build mcp/CWM.RoslynNavigator/CWM.RoslynNavigator.slnx --no-restore -c Release | |
| test-mcp: | |
| name: Test MCP Server | |
| runs-on: ubuntu-latest | |
| needs: build-mcp | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v5 | |
| with: | |
| dotnet-version: ${{ env.DOTNET_VERSION }} | |
| - name: Restore | |
| run: dotnet restore mcp/CWM.RoslynNavigator/tests/CWM.RoslynNavigator.Tests.csproj | |
| - name: Test | |
| run: dotnet test mcp/CWM.RoslynNavigator/tests/CWM.RoslynNavigator.Tests.csproj --no-restore -c Release --logger trx --results-directory TestResults | |
| - name: Publish test results | |
| uses: actions/upload-artifact@v5 | |
| if: always() | |
| with: | |
| name: test-results | |
| path: TestResults/*.trx | |
| format-check: | |
| name: Format Check | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v5 | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v5 | |
| with: | |
| dotnet-version: ${{ env.DOTNET_VERSION }} | |
| - name: Restore | |
| run: dotnet restore mcp/CWM.RoslynNavigator/CWM.RoslynNavigator.slnx | |
| - name: Format check | |
| run: dotnet format mcp/CWM.RoslynNavigator/CWM.RoslynNavigator.slnx --verify-no-changes --no-restore |