-
Notifications
You must be signed in to change notification settings - Fork 1
✅ Fix marketplace validation with schema checks #19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| name: Validate Marketplace | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - ".claude-plugin/**" | ||
| - "plugins/**/.claude-plugin/**" | ||
| - "scripts/validate-marketplace.sh" | ||
|
|
||
| jobs: | ||
| validate: | ||
| runs-on: ubuntu-latest | ||
|
|
||
| steps: | ||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Validate marketplace configuration | ||
| run: ./scripts/validate-marketplace.sh |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,184 @@ | ||
| #!/usr/bin/env bash | ||
| # | ||
| # Validates marketplace.json against Claude Code's schema requirements | ||
| # and checks consistency with individual plugin.json files. | ||
| # | ||
| # Schema validations: | ||
| # - Required fields: name, source, description, version | ||
| # - source must start with "./" | ||
| # - plugins array must not be empty | ||
| # | ||
| # Consistency validations: | ||
| # - tags in marketplace.json should match keywords in plugin.json | ||
| # - plugin.json must exist at the source path | ||
| # | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| ROOT_DIR="$(dirname "$SCRIPT_DIR")" | ||
| MARKETPLACE_FILE="$ROOT_DIR/.claude-plugin/marketplace.json" | ||
|
|
||
| # Colors for output | ||
| RED='\033[0;31m' | ||
| GREEN='\033[0;32m' | ||
| YELLOW='\033[0;33m' | ||
| NC='\033[0m' # No Color | ||
|
|
||
| errors=0 | ||
|
|
||
| echo "Validating marketplace configuration..." | ||
| echo "" | ||
|
|
||
| # Check jq is available | ||
| if ! command -v jq &> /dev/null; then | ||
| echo -e "${RED}Error: jq is required but not installed${NC}" | ||
| exit 1 | ||
| fi | ||
|
|
||
| # Validate marketplace.json syntax | ||
| echo "=== JSON Syntax ===" | ||
| if ! jq empty "$MARKETPLACE_FILE" 2>/dev/null; then | ||
| echo -e "${RED}✗ Invalid JSON in marketplace.json${NC}" | ||
| exit 1 | ||
| fi | ||
| echo -e "${GREEN}✓ JSON syntax valid${NC}" | ||
| echo "" | ||
|
|
||
| # Validate required top-level fields | ||
| echo "=== Marketplace Schema ===" | ||
|
|
||
| # Check name field | ||
| name=$(jq -r '.name // empty' "$MARKETPLACE_FILE") | ||
| if [[ -z "$name" ]]; then | ||
| echo -e "${RED}✗ Missing required field: name${NC}" | ||
| errors=$((errors + 1)) | ||
| else | ||
| echo -e "${GREEN}✓ name: $name${NC}" | ||
| fi | ||
|
|
||
| # Check metadata.pluginRoot | ||
| plugin_root=$(jq -r '.metadata.pluginRoot // empty' "$MARKETPLACE_FILE") | ||
| if [[ -z "$plugin_root" ]]; then | ||
| echo -e "${YELLOW}⚠ Missing metadata.pluginRoot (using default '.')${NC}" | ||
| plugin_root="." | ||
| else | ||
| echo -e "${GREEN}✓ metadata.pluginRoot: $plugin_root${NC}" | ||
| fi | ||
|
|
||
| # Check plugins array exists and is not empty | ||
| plugin_count=$(jq '.plugins | length' "$MARKETPLACE_FILE") | ||
| if [[ "$plugin_count" -eq 0 ]]; then | ||
| echo -e "${RED}✗ plugins array is empty${NC}" | ||
| errors=$((errors + 1)) | ||
| else | ||
| echo -e "${GREEN}✓ plugins: $plugin_count entries${NC}" | ||
| fi | ||
| echo "" | ||
|
|
||
| # Validate each plugin's schema | ||
| echo "=== Plugin Schema Validation ===" | ||
| echo "" | ||
|
|
||
| for i in $(seq 0 $((plugin_count - 1))); do | ||
| plugin_name=$(jq -r ".plugins[$i].name // empty" "$MARKETPLACE_FILE") | ||
| plugin_source=$(jq -r ".plugins[$i].source // empty" "$MARKETPLACE_FILE") | ||
| plugin_desc=$(jq -r ".plugins[$i].description // empty" "$MARKETPLACE_FILE") | ||
| plugin_version=$(jq -r ".plugins[$i].version // empty" "$MARKETPLACE_FILE") | ||
|
|
||
| plugin_errors=0 | ||
| echo "Plugin: ${plugin_name:-"(unnamed)"}" | ||
|
|
||
| # Check required fields | ||
| if [[ -z "$plugin_name" ]]; then | ||
| echo -e " ${RED}✗ Missing required field: name${NC}" | ||
| plugin_errors=$((plugin_errors + 1)) | ||
| fi | ||
|
|
||
| if [[ -z "$plugin_source" ]]; then | ||
| echo -e " ${RED}✗ Missing required field: source${NC}" | ||
| plugin_errors=$((plugin_errors + 1)) | ||
| elif [[ "$plugin_source" != ./* ]]; then | ||
| echo -e " ${RED}✗ source must start with './' (got: $plugin_source)${NC}" | ||
| plugin_errors=$((plugin_errors + 1)) | ||
| fi | ||
|
|
||
| if [[ -z "$plugin_desc" ]]; then | ||
| echo -e " ${RED}✗ Missing required field: description${NC}" | ||
| plugin_errors=$((plugin_errors + 1)) | ||
| fi | ||
|
|
||
| if [[ -z "$plugin_version" ]]; then | ||
| echo -e " ${RED}✗ Missing required field: version${NC}" | ||
| plugin_errors=$((plugin_errors + 1)) | ||
| fi | ||
|
|
||
| if [[ $plugin_errors -eq 0 ]]; then | ||
| echo -e " ${GREEN}✓ Schema valid${NC}" | ||
| else | ||
| errors=$((errors + plugin_errors)) | ||
| fi | ||
| done | ||
| echo "" | ||
|
|
||
| # Validate plugin paths and keyword consistency | ||
| echo "=== Plugin Consistency ===" | ||
| echo "" | ||
|
|
||
| for i in $(seq 0 $((plugin_count - 1))); do | ||
| plugin_name=$(jq -r ".plugins[$i].name" "$MARKETPLACE_FILE") | ||
| plugin_source=$(jq -r ".plugins[$i].source" "$MARKETPLACE_FILE") | ||
| marketplace_tags=$(jq -c ".plugins[$i].tags // []" "$MARKETPLACE_FILE") | ||
|
|
||
| # Strip leading "./" from source for path construction | ||
| source_path="${plugin_source#./}" | ||
|
|
||
| # Construct path to plugin.json | ||
| plugin_json_path="$ROOT_DIR/$plugin_root/$source_path/.claude-plugin/plugin.json" | ||
|
|
||
| echo "Plugin: $plugin_name" | ||
|
|
||
| if [[ ! -f "$plugin_json_path" ]]; then | ||
| echo -e " ${RED}✗ plugin.json not found at: $plugin_json_path${NC}" | ||
| errors=$((errors + 1)) | ||
| continue | ||
| fi | ||
| echo -e " ${GREEN}✓ plugin.json exists${NC}" | ||
|
|
||
| # Get keywords from plugin.json | ||
| plugin_keywords=$(jq -c '.keywords // []' "$plugin_json_path") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Plugin JSON syntax errors produce cryptic jq error messagesThe script validates |
||
|
|
||
| # Sort both arrays for comparison | ||
| sorted_tags=$(echo "$marketplace_tags" | jq -c 'sort') | ||
| sorted_keywords=$(echo "$plugin_keywords" | jq -c 'sort') | ||
|
|
||
| if [[ "$sorted_tags" == "$sorted_keywords" ]]; then | ||
| echo -e " ${GREEN}✓ Keywords match${NC}" | ||
| else | ||
| echo -e " ${RED}✗ Keyword mismatch${NC}" | ||
| echo -e " marketplace tags: $marketplace_tags" | ||
| echo -e " plugin keywords: $plugin_keywords" | ||
|
|
||
| # Show the diff | ||
| tags_only=$(echo "$marketplace_tags" | jq -c --argjson kw "$plugin_keywords" '. - $kw') | ||
| keywords_only=$(echo "$plugin_keywords" | jq -c --argjson tags "$marketplace_tags" '. - $tags') | ||
|
|
||
| if [[ "$tags_only" != "[]" ]]; then | ||
| echo -e " ${YELLOW}Only in marketplace: $tags_only${NC}" | ||
| fi | ||
| if [[ "$keywords_only" != "[]" ]]; then | ||
| echo -e " ${YELLOW}Only in plugin: $keywords_only${NC}" | ||
| fi | ||
| errors=$((errors + 1)) | ||
| fi | ||
| done | ||
|
|
||
| echo "" | ||
| echo "=== Summary ===" | ||
| if [[ $errors -gt 0 ]]; then | ||
| echo -e "${RED}Validation failed with $errors error(s)${NC}" | ||
| exit 1 | ||
| else | ||
| echo -e "${GREEN}All validations passed${NC}" | ||
| exit 0 | ||
| fi | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing null handling for plugins array causes bash error
If the
marketplace.jsonfile doesn't have apluginsfield at all (or it's null),jq '.plugins | length'returns the string"null"rather than a number. The subsequent bash comparison[[ "$plugin_count" -eq 0 ]]then fails with a cryptic arithmetic syntax error due toset -e. While validation correctly fails, the error message is confusing instead of clearly stating the plugins array is missing. Adding a fallback likejq '.plugins // [] | length'would handle this edge case gracefully.