Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE/plugin.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ One-liner – What does this plugin contribution add or change?
## Plugin Documentation Checklist

- README Validation
- [ ] `README.md` file exists in the plugin root folder
- [ ] Clear installation instructions
- [ ] Usage examples with code snippets
- [ ] List of features and capabilities
- [ ] Troubleshooting guide (if applicable)
- [ ] Contribution guidelines (if applicable)

- Metadata Validation
- [ ] `plugin_metadata.yml` file exists in the plugin root folder
- [ ] Complete metadata provided in reference to [plugin metadata template](../.././plugins/plugin_metadata_template.yml)

## Dev Testing
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/pr-labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Auto Label PR
on:
pull_request:
branches: [ "main" ]
types: [opened, edited, reopened, synchronize]
types: [ opened, edited, reopened, synchronize ]

jobs:
label-plugin-pr:
Expand Down
188 changes: 188 additions & 0 deletions .github/workflows/validate-new-plugin-metadata.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
name: Validate New Plugin Metadata

on:
pull_request:
branches: [ "main" ]
paths: [ "plugins/**" ]
types: [ opened, edited, reopened, synchronize ]

jobs:
identify-new-plugins:
runs-on: ubuntu-latest
outputs:
plugin_dirs: ${{ steps.find_new_plugins.outputs.plugin_dirs }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.ref }}

- name: Identify New Plugin Directories
id: find_new_plugins
run: |
# Fetch latest base branch state
git fetch origin ${{ github.event.pull_request.base.ref }}
BASE_COMMIT=$(git merge-base origin/${{ github.event.pull_request.base.ref }} HEAD)

# Find newly added plugin directories
NEW_PLUGINS=()
for plugin_dir in $(git diff --diff-filter=A --name-only $BASE_COMMIT...HEAD | grep '^plugins/' | cut -d'/' -f1-2 | sort -u); do
# Ensure directory is completely new (does not exist in base branch)
if ! git rev-parse --verify origin/${{ github.event.pull_request.base.ref }}:"$plugin_dir" &>/dev/null; then
NEW_PLUGINS+=("$plugin_dir")
fi
done

# Exit early if no new plugins were found
if [[ ${#NEW_PLUGINS[@]} -eq 0 ]]; then
echo "plugin_dirs=[]" >> $GITHUB_OUTPUT
exit 0
fi

# Convert plugin directory list to JSON format for use in next job
echo "plugin_dirs=$(jq -nc --argjson arr "$(printf '%s\n' "${NEW_PLUGINS[@]}" | jq -R . | jq -s .)" '$arr')" >> $GITHUB_OUTPUT

validate-individual-plugins:
needs: identify-new-plugins
if: ${{ needs.identify-new-plugins.outputs.plugin_dirs != '[]' }}
runs-on: ubuntu-latest
strategy:
matrix:
plugin_dir: ${{ fromJson(needs.identify-new-plugins.outputs.plugin_dirs) }}
steps:
- name: Checkout Repository
uses: actions/checkout@v4

- name: Validate Plugin Metadata
run: |
set +e # Disable exit on error to allow all fields to be validated
metadata_file="${{ matrix.plugin_dir }}/plugin_metadata.yml"
if [[ ! -f "$metadata_file" ]]; then
echo "::error file=$metadata_file::Missing plugin_metadata.yml"
exit 1
fi

echo "::group::Validating $metadata_file"

metadata=$(yq '.' "$metadata_file")
errors=0

# Regex pattern for a valid URL
url_regex='^https?:\/\/[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}(:[0-9]{1,5})?(\/.*)?$'
email_regex='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
yyyy_mm_regex='^202[0-9]{1}-[0-9]{2}$'
x_account_handle_regex='^@[a-zA-Z0-9_]{1,15}$'

# Function to print missing field error
missing_field_error() {
local field="$1"
echo "::error file=$metadata_file::'$field' is required but missing"
((errors++))
}

# Function to check if a required field is missing
check_required_field() {
local field="$1"
local value=$(echo "$metadata" | yq -r ".${field}")
if [[ -z "$value" || "$value" == "null" ]]; then
missing_field_error "$field"
fi
}

# Function to validate a URL field (supports single values and arrays, optional by default, add "required" as second parameter to make it required, ie: check_valid_url "community_url" "required")
check_valid_url() {
local field="$1"
local required="${2:-optional}" # Default to "optional" if not specified
local value
local field_type
local non_empty_value=0

value=$(echo "$metadata" | yq -r ".${field}" 2>/dev/null || echo "")
field_type=$(echo "$metadata" | yq -r ".${field} | type" 2>/dev/null || echo "")

# If field is missing or empty
if [[ "$value" == "null" || -z "$value" ]]; then
if [[ "$required" == "required" ]]; then
missing_field_error "$field"
fi
return 0 # Skip validation if optional
fi

# Handle arrays of URLs
if [[ "$field_type" == "!!seq" ]]; then
mapfile -t urls < <(echo "$metadata" | yq -r ".${field} | .[]")

for url in "${urls[@]}"; do
[[ -z "$url" ]] && continue
if [[ ! "$url" =~ $url_regex ]]; then
echo "::error file=$metadata_file::'$field' contains an invalid URL: $url"
((errors++))
else
((non_empty_value++))
fi
done

if [[ $non_empty_value -eq 0 && "$required" == "required" ]]; then
echo "::error file=$metadata_file::'$field' is required but missing valid values"
((errors++))
fi
else
# Single value validation
if [[ ! "$value" =~ $url_regex ]]; then
echo "::error file=$metadata_file::'$field' is not a valid URL: $value"
((errors++))
fi
fi
}

# Validate required fields
check_required_field "plugin_name"
check_required_field "author"
check_required_field "short_description"
check_required_field "detailed_description"

# Validate URLs (optional fields but must be valid if provided, add "required" as second parameter to make them required, ie: check_valid_url "community_url" "required")
check_valid_url "logo_url"
check_valid_url "plugin_logo_url"
check_valid_url "demo_video_url"
check_valid_url "documentation_url"
check_valid_url "changelog_url"
check_valid_url "community_url"
check_valid_url "screenshots"

# Validate date format (YYYY-MM)
release_date=$(echo "$metadata" | yq -r '.release_date')
if [[ -z "$release_date" || "$release_date" == "null" ]]; then
missing_field_error "release_date"
else
if [[ ! "$release_date" =~ $yyyy_mm_regex ]]; then
echo "::error file=$metadata_file::'release_date' should be in YYYY-MM format"
((errors++))
fi
fi

# Validate X account handle format (@username)
x_account_handle=$(echo "$metadata" | yq '.x_account_handle')
if [[ -z "$x_account_handle" || "$x_account_handle" == "null" ]]; then
missing_field_error "x_account_handle"
else
if [[ -n "$x_account_handle" && ! "$x_account_handle" =~ $x_account_handle_regex ]]; then
echo "::error file=$metadata_file::'x_account_handle' is not a valid X (Twitter) handle"
((errors++))
fi
fi

# Validate support contact (must be a valid URL or email)
support_contact=$(echo "$metadata" | yq '.support_contact')
if [[ -z "$support_contact" || "$support_contact" == "null" ]]; then
missing_field_error "support_contact"
else
if [[ ! "$support_contact" =~ ($url_regex|$email_regex) ]]; then
echo "::error file=$metadata_file::'support_contact' must be a valid URL or email address"
((errors++))
fi
fi

echo "::endgroup::"
exit $errors
12 changes: 6 additions & 6 deletions plugins/plugin_metadata_template.yml
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
# General Information
plugin_name: "" # Name of the plugin
author: "" # Author and team name
logo_url: "" # URL to the author photo or team logo (512x512 recommended)
release_date: "" # Release date (DD-MM-YYYY)
logo_url: "" # URL to the author photo or team logo (512x512 recommended) (if any)
release_date: "" # Release date (YYYY-MM)

# Description
short_description: "" # One-liner description for listings
detailed_description: "" # Full description with features and benefits

# Media & Assets
plugin_logo_url: "" # URL to the plugin logo (512x512 recommended) (if any or fallback to logo_url)
screenshots: # List of screenshots showcasing the plugin
screenshots: # List of screenshots showcasing the plugin (if any)
- "" # e.g., "https://example.com/screenshot1.png"
- ""
demo_video_url: "" # Link to a demo or walkthrough video (if available)
documentation_url: "" # Link to the plugin's official documentation (if available)
demo_video_url: "" # Link to a demo or walkthrough video (if any)
documentation_url: "" # Link to the plugin's official documentation (if any)
changelog_url: "" # Link to the changelog (if maintained)

# Contact & Support
x_account_handle: "" # X (formerly known as Twitter) account handle (ie: @GAME_Virtuals)
support_contact: "" # Email or Slack/Discord link for user support
community_link: "" # Forum or community link (if any)
community_url: "" # Forum or community link (if any)