diff --git a/.github/ISSUE_TEMPLATE/external-plugin.yml b/.github/ISSUE_TEMPLATE/external-plugin.yml index 3a0587d5..ccbb3a77 100644 --- a/.github/ISSUE_TEMPLATE/external-plugin.yml +++ b/.github/ISSUE_TEMPLATE/external-plugin.yml @@ -44,8 +44,8 @@ body: id: plugin-path attributes: label: Plugin path inside the repository - description: Optional if the plugin lives at the repository root. - placeholder: .github/plugins/my-plugin + description: Optional if the plugin lives at the repository root. Otherwise, enter the folder where the plugin structure starts, not the plugin.json file. + placeholder: plugins/my-plugin validations: required: false - type: input diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index 090ecefb..b0b1e1b0 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -126,7 +126,6 @@ "source": { "source": "github", "repo": "ChromeDevTools/chrome-devtools-mcp", - "path": ".github/plugin/plugin.json", "ref": "chrome-devtools-mcp-v1.0.1" } }, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0edc60d6..80e03c97 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -221,7 +221,7 @@ For entries committed to `plugins/external.json`, the current marketplace valida - `repository` as an HTTPS GitHub URL - `keywords` as lowercase hyphenated tags - `source.source: "github"` plus `source.repo` in `owner/repo` format -- optional `source.path` values to stay relative to the repository root +- optional `source.path` values of `/` for repository root, or a repository-relative folder where the plugin structure starts (do not point to `plugin.json` directly) The public-submission policy builds on those rules and also requires `license` plus an immutable `source.ref`. diff --git a/eng/external-plugin-validation.mjs b/eng/external-plugin-validation.mjs index 660c0530..100ce1f4 100644 --- a/eng/external-plugin-validation.mjs +++ b/eng/external-plugin-validation.mjs @@ -23,6 +23,12 @@ export const EXTERNAL_PLUGIN_POLICIES = Object.freeze({ }), }); +const EXTERNAL_PLUGIN_ROOT_MANIFEST_PATHS = Object.freeze([ + "plugin.json", + ".github/plugin/plugin.json", + ".plugin/plugin.json", +]); + function resolvePolicy(policy) { if (!policy) { return EXTERNAL_PLUGIN_POLICIES.marketplace; @@ -203,12 +209,20 @@ function validateHomepage(homepage, prefix, errors) { validateHttpsUrl(homepage, "homepage", prefix, errors); } +function formatExpectedPluginRootMessage() { + return EXTERNAL_PLUGIN_ROOT_MANIFEST_PATHS.map((manifestPath) => `"${manifestPath}"`).join(", "); +} + function validateRelativePath(pathValue, prefix, errors) { if (!isNonEmptyString(pathValue)) { errors.push(`${prefix}: "source.path" must be a non-empty string when provided`); return; } + if (pathValue === "/") { + return; + } + const normalized = path.posix.normalize(pathValue); const segments = pathValue.split("/"); @@ -219,6 +233,16 @@ function validateRelativePath(pathValue, prefix, errors) { if (pathValue.includes("\\")) { errors.push(`${prefix}: "source.path" must use forward slashes`); } + + if (normalized === ".") { + errors.push(`${prefix}: "source.path" must be "/" for the repository root or a plugin root directory relative to the repository root`); + } + + if (path.posix.basename(normalized) === "plugin.json") { + errors.push( + `${prefix}: "source.path" must point to the plugin root directory, not the manifest file; relative to "source.path", expected one of ${formatExpectedPluginRootMessage()}` + ); + } } function validateImmutableRef(ref, prefix, errors) { diff --git a/plugins/external.json b/plugins/external.json index 19c83007..a374c45d 100644 --- a/plugins/external.json +++ b/plugins/external.json @@ -73,7 +73,6 @@ "source": { "source": "github", "repo": "ChromeDevTools/chrome-devtools-mcp", - "path": ".github/plugin/plugin.json", "ref": "chrome-devtools-mcp-v1.0.1" } }, @@ -194,13 +193,7 @@ "url": "https://www.figma.com" }, "homepage": "https://github.com/figma/mcp-server-guide", - "keywords": [ - "figma", - "design", - "mcp", - "ui", - "code-connect" - ], + "keywords": ["figma", "design", "mcp", "ui", "code-connect"], "repository": "https://github.com/figma/mcp-server-guide", "source": { "source": "github", @@ -272,14 +265,7 @@ "url": "https://www.microsoft.com" }, "homepage": "https://github.com/microsoft/Build-CLI", - "keywords": [ - "microsoft", - "build", - "ignite", - "events", - "sessions", - "learn" - ], + "keywords": ["microsoft", "build", "ignite", "events", "sessions", "learn"], "license": "Apache-2.0", "repository": "https://github.com/microsoft/Build-CLI", "source": { @@ -296,12 +282,7 @@ "url": "https://www.microsoft.com" }, "homepage": "https://github.com/dotnet/modernize-dotnet", - "keywords": [ - "modernization", - "upgrade", - "migration", - "dotnet" - ], + "keywords": ["modernization", "upgrade", "migration", "dotnet"], "license": "MIT", "repository": "https://github.com/dotnet/modernize-dotnet", "source": { diff --git a/website/src/scripts/modal.ts b/website/src/scripts/modal.ts index 95e499c9..bfdabcdb 100644 --- a/website/src/scripts/modal.ts +++ b/website/src/scripts/modal.ts @@ -1055,7 +1055,7 @@ async function openPluginModal( function getExternalPluginUrl(plugin: Plugin): string { if (plugin.source?.source === "github" && plugin.source.repo) { const base = `https://github.com/${plugin.source.repo}`; - return plugin.source.path + return plugin.source.path && plugin.source.path !== "/" ? `${base}/tree/main/${plugin.source.path}` : base; } diff --git a/website/src/scripts/pages/plugins-render.ts b/website/src/scripts/pages/plugins-render.ts index b8a13c15..190a5ce8 100644 --- a/website/src/scripts/pages/plugins-render.ts +++ b/website/src/scripts/pages/plugins-render.ts @@ -49,7 +49,7 @@ export function sortPlugins( function getExternalPluginUrl(plugin: RenderablePlugin): string { if (plugin.source?.source === 'github' && plugin.source.repo) { const base = `https://github.com/${plugin.source.repo}`; - return plugin.source.path ? `${base}/tree/main/${plugin.source.path}` : base; + return plugin.source.path && plugin.source.path !== '/' ? `${base}/tree/main/${plugin.source.path}` : base; } return sanitizeUrl(plugin.repository || plugin.homepage);