From c66449b4fac86d6d7e38428db5b8883d14f23f66 Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Thu, 4 Jun 2026 12:53:27 -0700 Subject: [PATCH] fix: skill-validator invocation for .github/plugin/plugin.json convention (#1916) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: skill-validator invocation for .github/plugin/plugin.json convention The skill-validator --plugin mode looks for plugin.json at /plugin.json, but external plugins (and the Copilot CLI) place it at .github/plugin/plugin.json. This caused every external plugin with skills or agents to fail the skill-validator gate with a misleading 'No plugin.json found' error, even when the install smoke test passed correctly. Extract buildSkillValidatorArgs() which reads plugin.json from .github/plugin/plugin.json, resolves skills/agents paths relative to the plugin root, and invokes skill-validator with --skills/--agents instead of --plugin. Falls back to --plugin if the conventional path is not present. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: also check .plugins/plugin.json and root plugin.json locations Extend buildSkillValidatorArgs to probe three candidate plugin.json locations in priority order before falling back to --plugin: 1. .github/plugin/plugin.json (Copilot CLI convention) 2. .plugins/plugin.json 3. plugin.json (root — also the skill-validator's native --plugin expectation) Extract findPluginJson() and PLUGIN_JSON_CANDIDATES constant so the list is easy to extend. Paths in plugin.json are always resolved relative to the plugin root regardless of where the manifest lives. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/external-plugin-quality-gates.mjs | 79 ++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/eng/external-plugin-quality-gates.mjs b/eng/external-plugin-quality-gates.mjs index de4f2b37..6bf6ed77 100644 --- a/eng/external-plugin-quality-gates.mjs +++ b/eng/external-plugin-quality-gates.mjs @@ -156,10 +156,87 @@ function downloadSkillValidator(workDir) { return binaryPath; } +// Ordered list of candidate locations for plugin.json, from most to least specific. +// The skill-validator --plugin mode expects plugin.json at the plugin root, but +// both the Copilot CLI and many external repos use nested conventions. We read the +// manifest ourselves so skill/agent paths can be resolved from the plugin root +// consistently, regardless of where the manifest lives. +const PLUGIN_JSON_CANDIDATES = [ + [".github", "plugin", "plugin.json"], + [".plugins", "plugin.json"], + ["plugin.json"], +]; + +function findPluginJson(pluginRoot) { + for (const segments of PLUGIN_JSON_CANDIDATES) { + const candidate = path.join(pluginRoot, ...segments); + if (fs.existsSync(candidate)) { + return candidate; + } + } + return null; +} + +function buildSkillValidatorArgs(pluginRoot) { + const pluginJsonPath = findPluginJson(pluginRoot); + if (!pluginJsonPath) { + // No recognised plugin.json location found — let the validator fail with its + // own diagnostic (covers exotic layouts and surfaces the real error to submitters). + return ["check", "--verbose", "--plugin", pluginRoot]; + } + + let pluginJson; + try { + pluginJson = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8")); + } catch { + // Malformed plugin.json — let the validator surface the parse error. + return ["check", "--verbose", "--plugin", pluginRoot]; + } + + const args = ["check", "--verbose"]; + + // Paths in plugin.json are relative to the plugin root regardless of where + // plugin.json itself lives. Use [].concat() to accept both string and array values. + const skillPaths = [].concat(pluginJson.skills ?? []) + .map((s) => path.resolve(pluginRoot, s)) + .filter((p) => fs.existsSync(p)); + + // Agent entries may be directory paths or explicit file paths; normalise to directories + // so AgentDiscovery.DiscoverAgentsInDirectory can discover agents within them. + // Deduplicate in case multiple file entries share the same parent directory. + const agentPaths = [...new Set( + [].concat(pluginJson.agents ?? []) + .map((a) => { + const resolved = path.resolve(pluginRoot, a); + if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) { + return path.dirname(resolved); + } + return resolved; + }) + .filter((p) => fs.existsSync(p)) + )]; + + if (skillPaths.length > 0) { + args.push("--skills", ...skillPaths); + } + if (agentPaths.length > 0) { + args.push("--agents", ...agentPaths); + } + + if (skillPaths.length === 0 && agentPaths.length === 0) { + // plugin.json found but no resolvable skills/agents — fall back to --plugin so the + // validator can surface the specific validation error to the submitter. + return ["check", "--verbose", "--plugin", pluginRoot]; + } + + return args; +} + function runSkillValidatorGate(workDir, pluginRoot) { try { const validatorBinary = downloadSkillValidator(workDir); - const check = runCommand(validatorBinary, ["check", "--verbose", "--plugin", pluginRoot]); + const args = buildSkillValidatorArgs(pluginRoot); + const check = runCommand(validatorBinary, args); if (check.exitCode === 0) { return { status: "pass", output: check.output };