Просмотр исходного кода

Add CI workflow to validate marketplace.json on PRs

Add a GitHub Actions workflow that validates marketplace.json is
well-formed JSON with a plugins array whenever PRs modify it. Includes:
- validate-marketplace.ts: Bun script that parses and validates the JSON
- validate-marketplace.yml: GH Actions workflow triggered on PR changes
- test-marketplace-check.js: Unit tests for the validation logic

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Noah Zweben MacBook 7 месяцев назад
Родитель
Сommit
7d7f29cf27

+ 49 - 0
.github/scripts/validate-marketplace.ts

@@ -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 - 0
.github/workflows/test-marketplace-check.js

@@ -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 - 0
.github/workflows/validate-marketplace.yml

@@ -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