mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-14 13:16:54 +00:00
Accept nested extensions/<name>/extension.mjs in external-plugin canvas checks (#2403)
* Accept nested extensions/<name>/extension.mjs in external-plugin canvas checks The external-plugin canvas structure check (quality gate) and intake validation both hardcoded a flat extensions/extension.mjs entry point, falsely rejecting the documented nested extensions/<name>/extension.mjs layout that installs and runs fine. Scan the extensions/ directory for a nested subfolder containing extension.mjs while still accepting the flat form for backward compatibility. Applied to both runCanvasStructureGate (git-object lookups) and validateCanvasPluginMetadata (Contents API), keeping them behaviorally aligned. Added regression coverage for both paths. Fixes #2402 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Harden nested canvas extension detection after adversarial review Address multi-model review findings on the nested canvas extension fix: - Quality gate: enumerate extensions/ via 'git ls-tree -z' with spawnSync (NUL-delimited, untruncated) so large directories no longer drop the real entry past the 12KB output cap. - Intake: decouple the flat extensions/extension.mjs check from the directory listing, require an array listing (Array.isArray) before treating it as a directory, and surface an unverifiable (warning) result instead of a false rejection when the listing or a nested lookup hits a transient API error. - Add regression tests: nested entry beyond the legacy output cap (gate) and unverifiable/flat-still-accepted paths when the listing errors (intake). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Enumerate canvas extensions via a single recursive Git Trees call in intake Address PR review: the Contents API caps directory listings at 1,000 entries and required one request per extension subfolder, so a nested entry beyond the cap could be falsely rejected (the same truncation class the git gate avoids) and large repos risked latency / rate-limit exhaustion. Replace the per-subfolder Contents API enumeration with one recursive 'git/trees/<locator>?recursive=1' fetch and inspect 'extensions/extension.mjs' and immediate 'extensions/<name>/extension.mjs' paths locally. A truncated tree without a located entry point is reported as unverifiable (warning) rather than rejected, and refs are normalized so 'refs/tags/<tag>' resolves as a tree-ish. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Bound canvas extension discovery to plugin scope Address reviewer feedback on unbounded scaling for untrusted/large repos: - Quality gate: replace the per-candidate-directory git cat-file spawns in locateCanvasEntryPoint with a single recursive git ls-tree over the extensions subtree, classifying flat/nested entry points in memory. Process count is now constant regardless of how many folders live under extensions/. - Intake: stop fetching the recursive git tree from the repo root (which a large unrelated monorepo can push past the Trees API truncation limit and never validate). Walk to the plugin's extensions directory one level at a time to resolve its tree SHA, then fetch only that subtree recursively, so verifiability depends on the plugin's own size, not the whole repository. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: aaronpowell <434140+aaronpowell@users.noreply.github.com>
This commit is contained in:
@@ -100,6 +100,90 @@ test("runCanvasStructureGate fails when extension entrypoint path is a directory
|
||||
assert.match(result.output, /"extensions\/extension\.mjs" must be a file/);
|
||||
});
|
||||
|
||||
test("runCanvasStructureGate passes when extension lives in a nested subfolder", () => {
|
||||
const repoDir = createTempRepo();
|
||||
fs.mkdirSync(path.join(repoDir, "extensions", "modernize-dashboard"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(repoDir, "extensions", "modernize-dashboard", "extension.mjs"),
|
||||
"export default {};\n",
|
||||
);
|
||||
const sha = commitAll(repoDir, "Add nested canvas extension");
|
||||
|
||||
const plugin = {
|
||||
name: "canvas-plugin",
|
||||
keywords: ["canvas"],
|
||||
source: {
|
||||
source: "github",
|
||||
repo: "owner/repo",
|
||||
sha,
|
||||
},
|
||||
};
|
||||
|
||||
const result = runCanvasStructureGate(repoDir, plugin, sha);
|
||||
assert.equal(result.status, "pass");
|
||||
assert.match(result.output, /entry point "extensions\/modernize-dashboard\/extension\.mjs"/);
|
||||
});
|
||||
|
||||
test("runCanvasStructureGate fails when no extension.mjs exists flat or nested", () => {
|
||||
const repoDir = createTempRepo();
|
||||
fs.mkdirSync(path.join(repoDir, "extensions", "modernize-dashboard"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(repoDir, "extensions", "modernize-dashboard", "index.mjs"),
|
||||
"export default {};\n",
|
||||
);
|
||||
const sha = commitAll(repoDir, "Add extensions directory without entry point");
|
||||
|
||||
const plugin = {
|
||||
name: "canvas-plugin",
|
||||
keywords: ["canvas"],
|
||||
source: {
|
||||
source: "github",
|
||||
repo: "owner/repo",
|
||||
sha,
|
||||
},
|
||||
};
|
||||
|
||||
const result = runCanvasStructureGate(repoDir, plugin, sha);
|
||||
assert.equal(result.status, "fail");
|
||||
assert.match(result.output, /missing required canvas extension entry point/);
|
||||
});
|
||||
|
||||
test("runCanvasStructureGate finds a nested extension listed past the legacy output cap", () => {
|
||||
const repoDir = createTempRepo();
|
||||
// Many sibling directories push the real extension past the ~12 KB stdout cap that the
|
||||
// previous truncating implementation applied, which would silently drop it from the listing.
|
||||
// Long names inflate each git ls-tree record so fewer directories are needed to exceed the cap.
|
||||
for (let index = 0; index < 160; index += 1) {
|
||||
const filler = path.join(
|
||||
repoDir,
|
||||
"extensions",
|
||||
`filler-directory-that-pads-the-tree-listing-${String(index).padStart(4, "0")}`,
|
||||
);
|
||||
fs.mkdirSync(filler, { recursive: true });
|
||||
fs.writeFileSync(path.join(filler, "readme.txt"), "filler\n");
|
||||
}
|
||||
fs.mkdirSync(path.join(repoDir, "extensions", "zzz-real-extension"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(repoDir, "extensions", "zzz-real-extension", "extension.mjs"),
|
||||
"export default {};\n",
|
||||
);
|
||||
const sha = commitAll(repoDir, "Add nested extension after many siblings");
|
||||
|
||||
const plugin = {
|
||||
name: "canvas-plugin",
|
||||
keywords: ["canvas"],
|
||||
source: {
|
||||
source: "github",
|
||||
repo: "owner/repo",
|
||||
sha,
|
||||
},
|
||||
};
|
||||
|
||||
const result = runCanvasStructureGate(repoDir, plugin, sha);
|
||||
assert.equal(result.status, "pass");
|
||||
assert.match(result.output, /entry point "extensions\/zzz-real-extension\/extension\.mjs"/);
|
||||
});
|
||||
|
||||
// Regression tests for issue #2397: a tag-name locator (e.g. "v1.0.0") must be
|
||||
// readable by the version-match and canvas-structure gates. `git fetch origin <tag>`
|
||||
// only updates FETCH_HEAD and never creates a local `refs/tags/<tag>`, so reading via
|
||||
|
||||
Reference in New Issue
Block a user