Skip to content
Open
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
49 changes: 49 additions & 0 deletions .github/scripts/validate-marketplace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env bun
/**
* Validates that marketplace.json is well-formed JSON with a plugins array.
*
* Usage:
* bun validate-marketplace.ts <path-to-marketplace.json>
*/

import { readFile } from "fs/promises";

async function main() {
const filePath = process.argv[2];
if (!filePath) {
console.error("Usage: validate-marketplace.ts <path-to-marketplace.json>");
process.exit(2);
}

const content = await readFile(filePath, "utf-8");

let parsed: unknown;
try {
parsed = JSON.parse(content);
} catch (err) {
console.error(
`ERROR: ${filePath} is not valid JSON: ${err instanceof Error ? err.message : err}`
);
process.exit(1);
}

if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
console.error(`ERROR: ${filePath} must be a JSON object`);
process.exit(1);
}

const marketplace = parsed as Record<string, unknown>;
if (!Array.isArray(marketplace.plugins)) {
console.error(`ERROR: ${filePath} missing "plugins" array`);
process.exit(1);
}

console.log(
`marketplace.json is valid (${marketplace.plugins.length} plugins)`
);
}

main().catch((err) => {
console.error("Fatal error:", err);
process.exit(2);
});
174 changes: 174 additions & 0 deletions .github/workflows/test-marketplace-check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#!/usr/bin/env node

/**
* Test script for marketplace.json PR validation logic.
* Run with: node .github/workflows/test-marketplace-check.js
*/

function checkMarketplaceViolations(mainPlugins, prPlugins) {
const mainSourceByName = new Map(
mainPlugins.map(p => [p.name, JSON.stringify(p.source)])
);

const violations = [];
for (const plugin of prPlugins) {
if (!mainSourceByName.has(plugin.name)) {
violations.push(`- Adding new plugin: \`${plugin.name}\``);
} else if (mainSourceByName.get(plugin.name) !== JSON.stringify(plugin.source)) {
violations.push(`- Changing source for plugin: \`${plugin.name}\``);
}
}

return violations;
}

// Test cases
const tests = [
{
name: "No changes - should allow",
main: [
{ name: "foo", source: "./plugins/foo", description: "Foo plugin" }
],
pr: [
{ name: "foo", source: "./plugins/foo", description: "Foo plugin" }
],
expectBlocked: false
},
{
name: "Description change only - should allow",
main: [
{ name: "foo", source: "./plugins/foo", description: "Old description" }
],
pr: [
{ name: "foo", source: "./plugins/foo", description: "New description" }
],
expectBlocked: false
},
{
name: "Version/category change - should allow",
main: [
{ name: "foo", source: "./plugins/foo", version: "1.0.0", category: "dev" }
],
pr: [
{ name: "foo", source: "./plugins/foo", version: "2.0.0", category: "productivity" }
],
expectBlocked: false
},
{
name: "New plugin added - should block",
main: [
{ name: "foo", source: "./plugins/foo" }
],
pr: [
{ name: "foo", source: "./plugins/foo" },
{ name: "bar", source: "./plugins/bar" }
],
expectBlocked: true,
expectedViolation: "Adding new plugin: `bar`"
},
{
name: "Source changed (string) - should block",
main: [
{ name: "foo", source: "./plugins/foo" }
],
pr: [
{ name: "foo", source: "./plugins/evil" }
],
expectBlocked: true,
expectedViolation: "Changing source for plugin: `foo`"
},
{
name: "Source changed (string to object) - should block",
main: [
{ name: "foo", source: "./plugins/foo" }
],
pr: [
{ name: "foo", source: { source: "url", url: "https://evil.com/repo.git" } }
],
expectBlocked: true,
expectedViolation: "Changing source for plugin: `foo`"
},
{
name: "Source changed (object URL) - should block",
main: [
{ name: "foo", source: { source: "url", url: "https://github.com/good/repo.git" } }
],
pr: [
{ name: "foo", source: { source: "url", url: "https://github.com/evil/repo.git" } }
],
expectBlocked: true,
expectedViolation: "Changing source for plugin: `foo`"
},
{
name: "Plugin removed - should allow",
main: [
{ name: "foo", source: "./plugins/foo" },
{ name: "bar", source: "./plugins/bar" }
],
pr: [
{ name: "foo", source: "./plugins/foo" }
],
expectBlocked: false
},
{
name: "Multiple violations - should block with all listed",
main: [
{ name: "foo", source: "./plugins/foo" }
],
pr: [
{ name: "foo", source: "./plugins/evil" },
{ name: "bar", source: "./plugins/bar" }
],
expectBlocked: true,
expectedViolationCount: 2
},
{
name: "Object source unchanged - should allow",
main: [
{ name: "foo", source: { source: "url", url: "https://github.com/org/repo.git" } }
],
pr: [
{ name: "foo", source: { source: "url", url: "https://github.com/org/repo.git" }, description: "Updated" }
],
expectBlocked: false
}
];

// Run tests
console.log("Running marketplace.json validation tests\n");
console.log("=".repeat(50));

let passed = 0;
let failed = 0;

for (const test of tests) {
const violations = checkMarketplaceViolations(test.main, test.pr);
const blocked = violations.length > 0;

let success = blocked === test.expectBlocked;

if (success && test.expectedViolation) {
success = violations.some(v => v.includes(test.expectedViolation));
}

if (success && test.expectedViolationCount) {
success = violations.length === test.expectedViolationCount;
}

if (success) {
console.log(`✓ ${test.name}`);
passed++;
} else {
console.log(`✗ ${test.name}`);
console.log(` Expected blocked: ${test.expectBlocked}, got: ${blocked}`);
if (violations.length > 0) {
console.log(` Violations: ${violations.join(", ")}`);
}
failed++;
}
}

console.log("=".repeat(50));
console.log(`\nResults: ${passed} passed, ${failed} failed`);

process.exit(failed > 0 ? 1 : 0);
17 changes: 17 additions & 0 deletions .github/workflows/validate-marketplace.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
name: Validate Marketplace JSON

on:
pull_request:
paths:
- '.claude-plugin/marketplace.json'

jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: oven-sh/setup-bun@v2

- name: Validate marketplace.json
run: bun .github/scripts/validate-marketplace.ts .claude-plugin/marketplace.json
Loading