mirror of
https://github.com/cursor/plugins.git
synced 2026-09-14 20:00:00 +08:00
Add plugin validation workflow
Adds a GitHub Actions workflow that validates marketplace.json and each plugin's plugin.json against their JSON schemas on PRs. Includes a Node.js validation script and adds missing category/tags fields to the plugin schema. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
name: Validate plugins
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".cursor-plugin/marketplace.json"
|
||||
- "**/plugin.json"
|
||||
- "schemas/**"
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install --no-save ajv ajv-formats
|
||||
|
||||
- name: Validate plugin definitions
|
||||
run: node scripts/validate-plugins.mjs
|
||||
@@ -57,6 +57,15 @@
|
||||
"items": { "type": "string" },
|
||||
"description": "Keywords for discovery and search."
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "Plugin category for marketplace classification."
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"description": "Tags for filtering and discovery."
|
||||
},
|
||||
"commands": {
|
||||
"$ref": "#/$defs/stringOrStringArray",
|
||||
"description": "Glob pattern(s) or path(s) to command files."
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFileSync, existsSync } from "fs";
|
||||
import { resolve, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import Ajv from "ajv";
|
||||
import addFormats from "ajv-formats";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const root = resolve(__dirname, "..");
|
||||
|
||||
function loadJSON(path) {
|
||||
return JSON.parse(readFileSync(path, "utf-8"));
|
||||
}
|
||||
|
||||
const marketplaceSchema = loadJSON(
|
||||
resolve(root, "schemas/marketplace.schema.json")
|
||||
);
|
||||
const pluginSchema = loadJSON(resolve(root, "schemas/plugin.schema.json"));
|
||||
|
||||
const ajv = new Ajv({ allErrors: true });
|
||||
addFormats(ajv);
|
||||
|
||||
const validateMarketplace = ajv.compile(marketplaceSchema);
|
||||
const validatePlugin = ajv.compile(pluginSchema);
|
||||
|
||||
let errors = 0;
|
||||
|
||||
function fail(message) {
|
||||
console.error(`ERROR: ${message}`);
|
||||
errors++;
|
||||
}
|
||||
|
||||
// 1. Validate marketplace.json
|
||||
const marketplacePath = resolve(root, ".cursor-plugin/marketplace.json");
|
||||
|
||||
if (!existsSync(marketplacePath)) {
|
||||
fail(".cursor-plugin/marketplace.json not found");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const marketplace = loadJSON(marketplacePath);
|
||||
|
||||
if (!validateMarketplace(marketplace)) {
|
||||
fail("marketplace.json schema validation failed:");
|
||||
for (const err of validateMarketplace.errors) {
|
||||
console.error(` ${err.instancePath || "/"}: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Validate each plugin
|
||||
for (const entry of marketplace.plugins ?? []) {
|
||||
const pluginDir = resolve(root, entry.source);
|
||||
const pluginJsonPath = resolve(pluginDir, ".cursor-plugin/plugin.json");
|
||||
|
||||
// Check source directory exists
|
||||
if (!existsSync(pluginDir)) {
|
||||
fail(
|
||||
`Plugin "${entry.name}": source directory "${entry.source}" does not exist`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check plugin.json exists
|
||||
if (!existsSync(pluginJsonPath)) {
|
||||
fail(
|
||||
`Plugin "${entry.name}": missing .cursor-plugin/plugin.json in "${entry.source}"`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const pluginJson = loadJSON(pluginJsonPath);
|
||||
|
||||
if (!validatePlugin(pluginJson)) {
|
||||
fail(
|
||||
`Plugin "${entry.name}": plugin.json schema validation failed (${entry.source}/.cursor-plugin/plugin.json):`
|
||||
);
|
||||
for (const err of validatePlugin.errors) {
|
||||
const detail =
|
||||
err.keyword === "additionalProperties"
|
||||
? `${err.message}: "${err.params.additionalProperty}"`
|
||||
: err.message;
|
||||
console.error(` ${err.instancePath || "/"}: ${detail}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that marketplace name matches plugin name
|
||||
if (pluginJson.name && pluginJson.name !== entry.name) {
|
||||
fail(
|
||||
`Plugin "${entry.name}": marketplace name does not match plugin.json name "${pluginJson.name}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Report results
|
||||
if (errors > 0) {
|
||||
console.error(`\nValidation failed with ${errors} error(s).`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log("All plugins validated successfully.");
|
||||
process.exit(0);
|
||||
}
|
||||
Reference in New Issue
Block a user