Skip to content

Merge pull request #1 from kossakovsky/add-claude-github-actions-1772… #9

Merge pull request #1 from kossakovsky/add-claude-github-actions-1772…

Merge pull request #1 from kossakovsky/add-claude-github-actions-1772… #9

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"