Merge pull request #1 from kossakovsky/add-claude-github-actions-1772… #9
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 Plugins | |
| on: | |
| push: | |
| branches: [ main, develop ] | |
| pull_request: | |
| branches: [ main ] | |
| jobs: | |
| validate-structure: | |
| name: Validate Repository Structure | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Setup jq | |
| uses: dcarbone/install-jq-action@v3 | |
| - name: Validate marketplace.json | |
| run: | | |
| echo "Validating marketplace.json..." | |
| jq empty .claude-plugin/marketplace.json | |
| echo "✓ Marketplace JSON is valid" | |
| - name: Check required fields in marketplace | |
| run: | | |
| echo "Checking required marketplace fields..." | |
| # Check top-level required fields | |
| if ! jq -e ".name" .claude-plugin/marketplace.json > /dev/null; then | |
| echo "✗ Missing required field: name" | |
| exit 1 | |
| fi | |
| echo "✓ Field 'name' present" | |
| if ! jq -e ".owner" .claude-plugin/marketplace.json > /dev/null; then | |
| echo "✗ Missing required field: owner" | |
| exit 1 | |
| fi | |
| echo "✓ Field 'owner' present" | |
| # Check owner.name exists | |
| if ! jq -e ".owner.name" .claude-plugin/marketplace.json > /dev/null; then | |
| echo "✗ Missing required field: owner.name" | |
| exit 1 | |
| fi | |
| echo "✓ Field 'owner.name' present" | |
| if ! jq -e ".plugins" .claude-plugin/marketplace.json > /dev/null; then | |
| echo "✗ Missing required field: plugins" | |
| exit 1 | |
| fi | |
| echo "✓ Field 'plugins' present" | |
| # Check plugins is an array | |
| if ! jq -e ".plugins | type == \"array\"" .claude-plugin/marketplace.json > /dev/null; then | |
| echo "✗ Field 'plugins' must be an array" | |
| exit 1 | |
| fi | |
| echo "✓ Field 'plugins' is an array" | |
| validate-marketplace-plugins: | |
| name: Validate Marketplace Plugin Entries | |
| needs: validate-structure | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Setup jq | |
| uses: dcarbone/install-jq-action@v3 | |
| - name: Validate marketplace plugin entries | |
| run: | | |
| echo "Validating marketplace plugin entries..." | |
| # Check each plugin entry in marketplace | |
| plugin_count=$(jq '.plugins | length' .claude-plugin/marketplace.json) | |
| for ((i=0; i<$plugin_count; i++)); do | |
| echo "" | |
| echo "Checking marketplace entry $((i+1))..." | |
| # Check required fields for marketplace entries | |
| if ! jq -e ".plugins[$i].name" .claude-plugin/marketplace.json > /dev/null; then | |
| echo "✗ Plugin entry $((i+1)): Missing required field 'name'" | |
| exit 1 | |
| fi | |
| if ! jq -e ".plugins[$i].source" .claude-plugin/marketplace.json > /dev/null; then | |
| echo "✗ Plugin entry $((i+1)): Missing required field 'source'" | |
| exit 1 | |
| fi | |
| if ! jq -e ".plugins[$i].description" .claude-plugin/marketplace.json > /dev/null; then | |
| echo "✗ Plugin entry $((i+1)): Missing required field 'description'" | |
| exit 1 | |
| fi | |
| if ! jq -e ".plugins[$i].version" .claude-plugin/marketplace.json > /dev/null; then | |
| echo "✗ Plugin entry $((i+1)): Missing required field 'version'" | |
| exit 1 | |
| fi | |
| # Validate SemVer format in marketplace entry | |
| mp_version=$(jq -r ".plugins[$i].version" .claude-plugin/marketplace.json) | |
| if ! echo "$mp_version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then | |
| echo "✗ Plugin entry $((i+1)): version '$mp_version' is not valid SemVer (expected X.Y.Z)" | |
| exit 1 | |
| fi | |
| echo "✓ Plugin entry $((i+1)): version '$mp_version' is valid SemVer" | |
| # Validate source path format | |
| source=$(jq -r ".plugins[$i].source" .claude-plugin/marketplace.json) | |
| if ! echo "$source" | grep -qE '^\./plugins/[a-z0-9-]+$'; then | |
| echo "✗ Plugin entry $((i+1)): source '$source' must match pattern ./plugins/<kebab-case-name>" | |
| exit 1 | |
| fi | |
| echo "✓ Plugin entry $((i+1)): source path format is valid" | |
| # Check author format if present | |
| if jq -e ".plugins[$i].author" .claude-plugin/marketplace.json > /dev/null; then | |
| if ! jq -e ".plugins[$i].author | type == \"object\"" .claude-plugin/marketplace.json > /dev/null; then | |
| echo "✗ Plugin entry $((i+1)): Field 'author' must be an object" | |
| exit 1 | |
| fi | |
| if ! jq -e ".plugins[$i].author.name" .claude-plugin/marketplace.json > /dev/null; then | |
| echo "✗ Plugin entry $((i+1)): Field 'author.name' is required when 'author' is present" | |
| exit 1 | |
| fi | |
| echo "✓ Plugin entry $((i+1)): Field 'author' is properly formatted" | |
| fi | |
| plugin_name=$(jq -r ".plugins[$i].name" .claude-plugin/marketplace.json) | |
| echo "✓ Marketplace entry for '$plugin_name' is valid" | |
| done | |
| echo "" | |
| echo "✅ All marketplace plugin entries are valid" | |
| validate-plugins: | |
| name: Validate Individual Plugins | |
| needs: validate-structure | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Setup jq | |
| uses: dcarbone/install-jq-action@v3 | |
| - name: Validate each plugin | |
| run: | | |
| echo "Validating plugin files..." | |
| # Get all plugin paths | |
| plugins=$(jq -r '.plugins[] | select(.source | type == "string") | .source' .claude-plugin/marketplace.json) | |
| for plugin_path in $plugins; do | |
| plugin_name=$(basename "$plugin_path") | |
| echo "" | |
| echo "Checking plugin: $plugin_name" | |
| echo "================================" | |
| # Check directory exists | |
| if [ ! -d "$plugin_path" ]; then | |
| echo "✗ Directory not found: $plugin_path" | |
| exit 1 | |
| fi | |
| echo "✓ Directory exists" | |
| # Check plugin.json exists | |
| plugin_json="$plugin_path/.claude-plugin/plugin.json" | |
| if [ ! -f "$plugin_json" ]; then | |
| echo "✗ plugin.json not found at $plugin_json" | |
| exit 1 | |
| fi | |
| echo "✓ plugin.json exists" | |
| # Validate JSON syntax | |
| if ! jq empty "$plugin_json" 2>/dev/null; then | |
| echo "✗ Invalid JSON in $plugin_json" | |
| exit 1 | |
| fi | |
| echo "✓ plugin.json is valid JSON" | |
| # Check required fields | |
| if ! jq -e ".name" "$plugin_json" > /dev/null; then | |
| echo "✗ Missing required field: name" | |
| exit 1 | |
| fi | |
| echo "✓ Required field 'name' present" | |
| # Validate kebab-case naming | |
| plugin_name_val=$(jq -r ".name" "$plugin_json") | |
| if ! echo "$plugin_name_val" | grep -qE '^[a-z0-9]([a-z0-9-]*[a-z0-9])?$'; then | |
| echo "✗ Plugin name '$plugin_name_val' must be kebab-case (lowercase letters, numbers, hyphens)" | |
| exit 1 | |
| fi | |
| echo "✓ Plugin name '$plugin_name_val' is valid kebab-case" | |
| # Cross-validate: plugin.json name must match directory name | |
| if [ "$plugin_name_val" != "$plugin_name" ]; then | |
| echo "✗ Plugin name '$plugin_name_val' in plugin.json does not match directory name '$plugin_name'" | |
| exit 1 | |
| fi | |
| echo "✓ Plugin name matches directory name" | |
| # Cross-validate: marketplace.json name must match plugin.json name | |
| marketplace_name=$(jq -r --arg src "$plugin_path" '.plugins[] | select(.source == $src) | .name' .claude-plugin/marketplace.json) | |
| if [ -n "$marketplace_name" ] && [ "$marketplace_name" != "$plugin_name_val" ]; then | |
| echo "✗ Marketplace name '$marketplace_name' does not match plugin.json name '$plugin_name_val'" | |
| exit 1 | |
| fi | |
| echo "✓ Marketplace name matches plugin.json name" | |
| # Check version field | |
| if ! jq -e ".version" "$plugin_json" > /dev/null; then | |
| echo "✗ Missing required field: version" | |
| exit 1 | |
| fi | |
| echo "✓ Required field 'version' present" | |
| # Validate SemVer format | |
| plugin_version=$(jq -r ".version" "$plugin_json") | |
| if ! echo "$plugin_version" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then | |
| echo "✗ Version '$plugin_version' is not valid SemVer (expected X.Y.Z)" | |
| exit 1 | |
| fi | |
| echo "✓ Version '$plugin_version' is valid SemVer" | |
| # Cross-validate: marketplace.json version must match plugin.json version | |
| marketplace_version=$(jq -r --arg src "$plugin_path" '.plugins[] | select(.source == $src) | .version' .claude-plugin/marketplace.json) | |
| if [ -n "$marketplace_version" ] && [ "$marketplace_version" != "$plugin_version" ]; then | |
| echo "✗ Marketplace version '$marketplace_version' does not match plugin.json version '$plugin_version'" | |
| exit 1 | |
| fi | |
| echo "✓ Marketplace version matches plugin.json version" | |
| # Check description field | |
| if ! jq -e ".description" "$plugin_json" > /dev/null; then | |
| echo "✗ Missing required field: description" | |
| exit 1 | |
| fi | |
| echo "✓ Required field 'description' present" | |
| # Check optional but recommended fields | |
| if jq -e ".author" "$plugin_json" > /dev/null; then | |
| # If author exists, check it's an object | |
| if ! jq -e ".author | type == \"object\"" "$plugin_json" > /dev/null; then | |
| echo "✗ Field 'author' must be an object with 'name' field" | |
| exit 1 | |
| fi | |
| if ! jq -e ".author.name" "$plugin_json" > /dev/null; then | |
| echo "✗ Field 'author.name' is required when 'author' is present" | |
| exit 1 | |
| fi | |
| echo "✓ Field 'author' is properly formatted" | |
| fi | |
| # Validate repository field if present (should be string, not object) | |
| if jq -e ".repository" "$plugin_json" > /dev/null; then | |
| if ! jq -e ".repository | type == \"string\"" "$plugin_json" > /dev/null; then | |
| echo "✗ Field 'repository' must be a string URL, not an object" | |
| exit 1 | |
| fi | |
| echo "✓ Field 'repository' is properly formatted" | |
| fi | |
| # Check README exists (required) | |
| if [ ! -f "$plugin_path/README.md" ]; then | |
| echo "✗ README.md not found (required)" | |
| exit 1 | |
| fi | |
| echo "✓ README.md exists" | |
| # Check commands directory and validate command files | |
| if [ -d "$plugin_path/commands" ]; then | |
| echo "✓ Commands directory exists" | |
| # Find all markdown files in commands directory | |
| command_files=$(find "$plugin_path/commands" -name "*.md" 2>/dev/null) | |
| if [ -n "$command_files" ]; then | |
| for cmd_file in $command_files; do | |
| cmd_name=$(basename "$cmd_file") | |
| # Check if file has frontmatter (starts with ---) | |
| if head -n 1 "$cmd_file" | grep -q "^---$"; then | |
| echo "✓ Command '$cmd_name' has frontmatter" | |
| else | |
| echo "⚠️ Warning: Command '$cmd_name' missing frontmatter (recommended)" | |
| fi | |
| # Check if frontmatter contains description | |
| if head -n 10 "$cmd_file" | grep -q "^description:"; then | |
| echo "✓ Command '$cmd_name' has description in frontmatter" | |
| else | |
| echo "⚠️ Warning: Command '$cmd_name' missing 'description' in frontmatter (recommended)" | |
| fi | |
| done | |
| fi | |
| fi | |
| echo "✓ Plugin $plugin_name is valid" | |
| done | |
| echo "" | |
| echo "================================" | |
| echo "✅ All plugins validated successfully!" | |
| check-duplicates: | |
| name: Check for Duplicate Plugin Names | |
| needs: validate-structure | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| - name: Setup jq | |
| uses: dcarbone/install-jq-action@v3 | |
| - name: Check for duplicate names | |
| run: | | |
| echo "Checking for duplicate plugin names..." | |
| duplicates=$(jq -r '.plugins[].name' .claude-plugin/marketplace.json | sort | uniq -d) | |
| if [ -n "$duplicates" ]; then | |
| echo "✗ Duplicate plugin names found:" | |
| echo "$duplicates" | |
| exit 1 | |
| fi | |
| echo "✓ No duplicate plugin names" |