mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-05 00:37:39 +00:00
chore: publish from main
This commit is contained in:
+124
-29
@@ -380,7 +380,92 @@ async function validateRemoteRepository(repo, { ref, sha }, errors, warnings, to
|
||||
}
|
||||
}
|
||||
|
||||
async function validateCanvasPluginMetadata(plugin, errors, warnings, token) {
|
||||
function buildGitTreePath(repo, treeish, { recursive = false } = {}) {
|
||||
const encodedRepo = encodeRepoPath(repo);
|
||||
const query = recursive ? "?recursive=1" : "";
|
||||
return `/repos/${encodedRepo}/git/trees/${encodeURIComponent(treeish)}${query}`;
|
||||
}
|
||||
|
||||
function normalizeTreeish(locator) {
|
||||
const value = String(locator ?? "").trim();
|
||||
// The Git Trees API takes the tree-ish as a single path segment. A full "refs/tags/<tag>"
|
||||
// ref would break that, so reduce it to the bare tag name; commit SHAs and simple tag
|
||||
// names pass through unchanged.
|
||||
return value.startsWith("refs/tags/") ? value.slice("refs/tags/".length) : value;
|
||||
}
|
||||
|
||||
// Resolve the tree SHA of a directory by walking the path one level at a time. Each hop is a
|
||||
// non-recursive tree fetch of a single directory, so the work is bounded by the path depth and
|
||||
// is independent of the overall repository size — unlike a root recursive fetch, which a large
|
||||
// unrelated monorepo can push over the API's truncation limit and never validate.
|
||||
async function resolveDirectoryTreeSha(repo, treeish, segments, token) {
|
||||
let currentTreeish = treeish;
|
||||
for (const segment of segments) {
|
||||
const response = await fetchGitHubJson(buildGitTreePath(repo, currentTreeish), token);
|
||||
if (response.kind !== "found" || !Array.isArray(response.data?.tree)) {
|
||||
return { status: "apiError" };
|
||||
}
|
||||
if (response.data.truncated) {
|
||||
// A single directory level exceeded the response limit; presence is unverifiable.
|
||||
return { status: "apiError" };
|
||||
}
|
||||
|
||||
const match = response.data.tree.find((entry) => entry?.path === segment);
|
||||
if (!match) {
|
||||
return { status: "missing" };
|
||||
}
|
||||
if (match.type !== "tree") {
|
||||
return { status: "notDirectory" };
|
||||
}
|
||||
currentTreeish = match.sha;
|
||||
}
|
||||
|
||||
return { status: "found", treeSha: currentTreeish };
|
||||
}
|
||||
|
||||
// Inspect the (recursively fetched) "extensions" subtree for the plugin's canvas extension
|
||||
// entry point. Paths are relative to "extensions/", so the flat form is "extension.mjs" and a
|
||||
// nested form is "<name>/extension.mjs". Scoping the recursive fetch to this subtree keeps the
|
||||
// lookup complete without depending on the size of the rest of the repository.
|
||||
function analyzeCanvasExtensionSubtree(subtreeEntries) {
|
||||
let flatIsBlob = false;
|
||||
let flatIsTree = false;
|
||||
let nestedEntryPath = null;
|
||||
|
||||
for (const entry of subtreeEntries) {
|
||||
const entryPath = entry?.path;
|
||||
if (typeof entryPath !== "string") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entryPath === "extension.mjs") {
|
||||
if (entry.type === "blob") {
|
||||
flatIsBlob = true;
|
||||
} else if (entry.type === "tree") {
|
||||
flatIsTree = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const segments = entryPath.split("/");
|
||||
if (segments.length === 2 && segments[1] === "extension.mjs" && entry.type === "blob") {
|
||||
nestedEntryPath = nestedEntryPath ?? `extensions/${entryPath}`;
|
||||
}
|
||||
}
|
||||
|
||||
if (flatIsBlob) {
|
||||
return { status: "found", entryPath: "extensions/extension.mjs" };
|
||||
}
|
||||
if (nestedEntryPath) {
|
||||
return { status: "found", entryPath: nestedEntryPath };
|
||||
}
|
||||
if (flatIsTree) {
|
||||
return { status: "notFile" };
|
||||
}
|
||||
return { status: "notFound" };
|
||||
}
|
||||
|
||||
export async function validateCanvasPluginMetadata(plugin, errors, warnings, token) {
|
||||
const repo = plugin?.source?.repo;
|
||||
const sha = plugin?.source?.sha;
|
||||
const ref = plugin?.source?.ref;
|
||||
@@ -471,41 +556,51 @@ async function validateCanvasPluginMetadata(plugin, errors, warnings, token) {
|
||||
);
|
||||
}
|
||||
|
||||
const extensionContainerPath = joinRepoPath(pluginRoot, "extensions");
|
||||
const extensionContainerResponse = await fetchGitHubFile(repo, extensionContainerPath, releaseLocator, token);
|
||||
if (extensionContainerResponse.kind === "notFound") {
|
||||
const unverifiableEntryPointWarning =
|
||||
`submission: could not verify the canvas extension entry point in GitHub repository "${repo}" at ${releaseLocatorDescription}; a maintainer should re-run intake`;
|
||||
const extensionsSegments = [...(pluginRoot ? pluginRoot.split("/") : []), "extensions"];
|
||||
const extensionsTree = await resolveDirectoryTreeSha(
|
||||
repo,
|
||||
normalizeTreeish(releaseLocator),
|
||||
extensionsSegments,
|
||||
token,
|
||||
);
|
||||
if (extensionsTree.status === "apiError") {
|
||||
warnings.push(unverifiableEntryPointWarning);
|
||||
} else if (extensionsTree.status === "missing") {
|
||||
errors.push(
|
||||
`submission: plugins tagged with "canvas" must include an "extensions" directory at ${releaseLocatorDescription}`,
|
||||
);
|
||||
} else if (extensionContainerResponse.kind === "apiError") {
|
||||
warnings.push(
|
||||
`submission: could not verify "extensions" directory in GitHub repository "${repo}" at ${releaseLocatorDescription}; a maintainer should re-run intake`,
|
||||
);
|
||||
} else if (
|
||||
!(
|
||||
extensionContainerResponse.data?.type === "dir"
|
||||
|| Array.isArray(extensionContainerResponse.data)
|
||||
)
|
||||
) {
|
||||
} else if (extensionsTree.status === "notDirectory") {
|
||||
errors.push(
|
||||
`submission: "extensions" must be a directory in ${releaseLocatorDescription}`,
|
||||
);
|
||||
}
|
||||
|
||||
const extensionEntryPath = joinRepoPath(pluginRoot, "extensions", "extension.mjs");
|
||||
const extensionEntryResponse = await fetchGitHubFile(repo, extensionEntryPath, releaseLocator, token);
|
||||
if (extensionEntryResponse.kind === "notFound") {
|
||||
errors.push(
|
||||
`submission: plugins tagged with "canvas" must include "extensions/extension.mjs" at ${releaseLocatorDescription}`,
|
||||
);
|
||||
} else if (extensionEntryResponse.kind === "apiError") {
|
||||
warnings.push(
|
||||
`submission: could not verify "extensions/extension.mjs" in GitHub repository "${repo}" at ${releaseLocatorDescription}; a maintainer should re-run intake`,
|
||||
);
|
||||
} else if (extensionEntryResponse.data?.type !== "file") {
|
||||
errors.push(
|
||||
`submission: "extensions/extension.mjs" must be a file in ${releaseLocatorDescription}`,
|
||||
} else {
|
||||
const subtreeResponse = await fetchGitHubJson(
|
||||
buildGitTreePath(repo, extensionsTree.treeSha, { recursive: true }),
|
||||
token,
|
||||
);
|
||||
if (subtreeResponse.kind !== "found" || !Array.isArray(subtreeResponse.data?.tree)) {
|
||||
warnings.push(unverifiableEntryPointWarning);
|
||||
} else {
|
||||
const canvasStructure = analyzeCanvasExtensionSubtree(subtreeResponse.data.tree);
|
||||
if (canvasStructure.status === "found") {
|
||||
// Entry point located (flat or nested); nothing to report.
|
||||
} else if (subtreeResponse.data.truncated) {
|
||||
// Absence is only inconclusive if the (already extensions-scoped) subtree itself is
|
||||
// truncated, which would take an implausibly large extensions directory; flag it as
|
||||
// unverifiable rather than falsely rejecting.
|
||||
warnings.push(unverifiableEntryPointWarning);
|
||||
} else if (canvasStructure.status === "notFile") {
|
||||
errors.push(
|
||||
`submission: "extensions/extension.mjs" must be a file in ${releaseLocatorDescription}`,
|
||||
);
|
||||
} else {
|
||||
errors.push(
|
||||
`submission: plugins tagged with "canvas" must include a canvas extension entry point at "extensions/extension.mjs" or "extensions/<extension>/extension.mjs" at ${releaseLocatorDescription}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const previewPath = joinRepoPath(pluginRoot, EXTERNAL_CANVAS_PREVIEW_PATH);
|
||||
|
||||
Reference in New Issue
Block a user