fix: skill-validator invocation for .github/plugin/plugin.json convention (#1916)

* fix: skill-validator invocation for .github/plugin/plugin.json convention

The skill-validator --plugin mode looks for plugin.json at <dir>/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>
This commit is contained in:
Aaron Powell
2026-06-04 12:53:27 -07:00
committed by GitHub
parent d11fb21f3a
commit c66449b4fa
+78 -1
View File
@@ -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 };