Skip to content

Commit 4a91a6e

Browse files
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>
1 parent a65002e commit 4a91a6e

3 files changed

Lines changed: 135 additions & 0 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Validate plugins
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- ".cursor-plugin/marketplace.json"
7+
- "**/plugin.json"
8+
- "schemas/**"
9+
10+
jobs:
11+
validate:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- uses: actions/setup-node@v4
17+
with:
18+
node-version: 20
19+
20+
- name: Install dependencies
21+
run: npm install --no-save ajv ajv-formats
22+
23+
- name: Validate plugin definitions
24+
run: node scripts/validate-plugins.mjs

schemas/plugin.schema.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@
5757
"items": { "type": "string" },
5858
"description": "Keywords for discovery and search."
5959
},
60+
"category": {
61+
"type": "string",
62+
"description": "Plugin category for marketplace classification."
63+
},
64+
"tags": {
65+
"type": "array",
66+
"items": { "type": "string" },
67+
"description": "Tags for filtering and discovery."
68+
},
6069
"commands": {
6170
"$ref": "#/$defs/stringOrStringArray",
6271
"description": "Glob pattern(s) or path(s) to command files."

scripts/validate-plugins.mjs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
#!/usr/bin/env node
2+
3+
import { readFileSync, existsSync } from "fs";
4+
import { resolve, dirname } from "path";
5+
import { fileURLToPath } from "url";
6+
import Ajv from "ajv";
7+
import addFormats from "ajv-formats";
8+
9+
const __dirname = dirname(fileURLToPath(import.meta.url));
10+
const root = resolve(__dirname, "..");
11+
12+
function loadJSON(path) {
13+
return JSON.parse(readFileSync(path, "utf-8"));
14+
}
15+
16+
const marketplaceSchema = loadJSON(
17+
resolve(root, "schemas/marketplace.schema.json")
18+
);
19+
const pluginSchema = loadJSON(resolve(root, "schemas/plugin.schema.json"));
20+
21+
const ajv = new Ajv({ allErrors: true });
22+
addFormats(ajv);
23+
24+
const validateMarketplace = ajv.compile(marketplaceSchema);
25+
const validatePlugin = ajv.compile(pluginSchema);
26+
27+
let errors = 0;
28+
29+
function fail(message) {
30+
console.error(`ERROR: ${message}`);
31+
errors++;
32+
}
33+
34+
// 1. Validate marketplace.json
35+
const marketplacePath = resolve(root, ".cursor-plugin/marketplace.json");
36+
37+
if (!existsSync(marketplacePath)) {
38+
fail(".cursor-plugin/marketplace.json not found");
39+
process.exit(1);
40+
}
41+
42+
const marketplace = loadJSON(marketplacePath);
43+
44+
if (!validateMarketplace(marketplace)) {
45+
fail("marketplace.json schema validation failed:");
46+
for (const err of validateMarketplace.errors) {
47+
console.error(` ${err.instancePath || "/"}: ${err.message}`);
48+
}
49+
}
50+
51+
// 2. Validate each plugin
52+
for (const entry of marketplace.plugins ?? []) {
53+
const pluginDir = resolve(root, entry.source);
54+
const pluginJsonPath = resolve(pluginDir, ".cursor-plugin/plugin.json");
55+
56+
// Check source directory exists
57+
if (!existsSync(pluginDir)) {
58+
fail(
59+
`Plugin "${entry.name}": source directory "${entry.source}" does not exist`
60+
);
61+
continue;
62+
}
63+
64+
// Check plugin.json exists
65+
if (!existsSync(pluginJsonPath)) {
66+
fail(
67+
`Plugin "${entry.name}": missing .cursor-plugin/plugin.json in "${entry.source}"`
68+
);
69+
continue;
70+
}
71+
72+
const pluginJson = loadJSON(pluginJsonPath);
73+
74+
if (!validatePlugin(pluginJson)) {
75+
fail(
76+
`Plugin "${entry.name}": plugin.json schema validation failed (${entry.source}/.cursor-plugin/plugin.json):`
77+
);
78+
for (const err of validatePlugin.errors) {
79+
const detail =
80+
err.keyword === "additionalProperties"
81+
? `${err.message}: "${err.params.additionalProperty}"`
82+
: err.message;
83+
console.error(` ${err.instancePath || "/"}: ${detail}`);
84+
}
85+
}
86+
87+
// Check that marketplace name matches plugin name
88+
if (pluginJson.name && pluginJson.name !== entry.name) {
89+
fail(
90+
`Plugin "${entry.name}": marketplace name does not match plugin.json name "${pluginJson.name}"`
91+
);
92+
}
93+
}
94+
95+
// 3. Report results
96+
if (errors > 0) {
97+
console.error(`\nValidation failed with ${errors} error(s).`);
98+
process.exit(1);
99+
} else {
100+
console.log("All plugins validated successfully.");
101+
process.exit(0);
102+
}

0 commit comments

Comments
 (0)