chore: publish from main

This commit is contained in:
github-actions[bot]
2026-07-17 08:04:30 +00:00
parent 818c0f7c53
commit b1aaa69f0a
286 changed files with 51076 additions and 67 deletions
+44
View File
@@ -6,6 +6,7 @@ import { fileURLToPath } from "url";
import { ROOT_FOLDER } from "./constants.mjs";
const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
const EXTENSIONS_DIR = path.join(ROOT_FOLDER, "extensions");
const MATERIALIZED_SPECS = {
agents: {
path: "agents",
@@ -89,6 +90,30 @@ function cleanPlugin(pluginPath) {
return { removed, manifestUpdated };
}
function cleanMaterializedExtensionPlugin(extensionPath) {
const pluginJsonPath = path.join(extensionPath, ".github", "plugin", "plugin.json");
let manifestUpdated = false;
if (fs.existsSync(pluginJsonPath)) {
const plugin = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8"));
if (plugin.extensions === "extensions") {
plugin.extensions = ".";
fs.writeFileSync(pluginJsonPath, JSON.stringify(plugin, null, 2) + "\n", "utf8");
manifestUpdated = true;
console.log(` Updated ${path.basename(extensionPath)}/.github/plugin/plugin.json`);
}
}
const target = path.join(extensionPath, "extensions");
if (!fs.existsSync(target) || !fs.statSync(target).isDirectory()) {
return { removed: 0, manifestUpdated };
}
const count = countFiles(target);
fs.rmSync(target, { recursive: true, force: true });
console.log(` Removed ${path.basename(extensionPath)}/extensions/ (${count} files)`);
return { removed: count, manifestUpdated };
}
function countFiles(dir) {
let count = 0;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
@@ -171,6 +196,25 @@ function main() {
}
}
if (fs.existsSync(EXTENSIONS_DIR)) {
const extensionDirs = fs.readdirSync(EXTENSIONS_DIR, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort();
for (const dirName of extensionDirs) {
const extensionPath = path.join(EXTENSIONS_DIR, dirName);
if (!fs.existsSync(path.join(extensionPath, "extension.mjs"))) {
continue;
}
const { removed, manifestUpdated } = cleanMaterializedExtensionPlugin(extensionPath);
total += removed;
if (manifestUpdated) {
manifestsUpdated++;
}
}
}
console.log();
if (total === 0 && manifestsUpdated === 0) {
console.log("✅ No materialized files found. Plugins are already clean.");
+68
View File
@@ -459,6 +459,55 @@ async function validateCanvasPluginMetadata(plugin, errors, warnings, token) {
);
}
if (manifest.extenions !== undefined) {
errors.push(
`submission: plugins tagged with "canvas" must use "extensions" (found misspelled key "extenions") in "${manifestPath}"`,
);
}
if (manifest.extensions !== undefined && manifest.extensions !== "extensions") {
errors.push(
`submission: plugins tagged with "canvas" may omit "extensions", but if provided it must be "extensions" in "${manifestPath}"`,
);
}
const extensionContainerPath = joinRepoPath(pluginRoot, "extensions");
const extensionContainerResponse = await fetchGitHubFile(repo, extensionContainerPath, releaseLocator, token);
if (extensionContainerResponse.kind === "notFound") {
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)
)
) {
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}`,
);
}
const previewPath = joinRepoPath(pluginRoot, EXTERNAL_CANVAS_PREVIEW_PATH);
const previewResponse = await fetchGitHubFile(repo, previewPath, releaseLocator, token);
if (previewResponse.kind === "notFound") {
@@ -580,11 +629,13 @@ function normalizeQualityGateResult(rawResult) {
vally_lint_status: "not_run",
smoke_status: "not_run",
version_match_status: "not_run",
canvas_structure_status: "not_run",
failure_class: "none",
summary: "",
vally_lint_output: "",
smoke_output: "",
version_match_output: "",
canvas_structure_output: "",
};
if (!rawResult || typeof rawResult !== "object" || Array.isArray(rawResult)) {
@@ -601,6 +652,7 @@ function buildQualityGatesCommentSection(qualityResult) {
const vallyState = qualityResult.vally_lint_status || "not_run";
const smokeState = qualityResult.smoke_status || "not_run";
const versionMatchState = qualityResult.version_match_status || "not_run";
const canvasStructureState = qualityResult.canvas_structure_status || "not_run";
const summaryText = String(qualityResult.summary || "").trim() || "_No quality gate details were provided._";
const sections = [
@@ -611,6 +663,7 @@ function buildQualityGatesCommentSection(qualityResult) {
`| vally lint | ${vallyState} |`,
`| install smoke test | ${smokeState} |`,
`| version match | ${versionMatchState} |`,
`| canvas structure | ${canvasStructureState} |`,
"",
summaryText,
];
@@ -660,6 +713,21 @@ function buildQualityGatesCommentSection(qualityResult) {
);
}
const canvasStructureOutput = String(qualityResult.canvas_structure_output || "").trim();
if (canvasStructureOutput) {
sections.push(
"",
"<details>",
"<summary>Canvas structure output</summary>",
"",
"```text",
canvasStructureOutput,
"```",
"",
"</details>",
);
}
return sections.join("\n");
}
+1 -1
View File
@@ -86,7 +86,7 @@ export async function runExternalPluginPrQualityGates(plugins) {
? "No changed external plugin entries were detected in plugins/external.json."
: checkedPlugins
.map((entry) =>
`- ${entry.name}: vally-lint=${entry.quality.vally_lint_status}, install-smoke=${entry.quality.smoke_status}, version-match=${entry.quality.version_match_status}, overall=${entry.quality.overall_status}`
`- ${entry.name}: vally-lint=${entry.quality.vally_lint_status}, install-smoke=${entry.quality.smoke_status}, version-match=${entry.quality.version_match_status}, canvas-structure=${entry.quality.canvas_structure_status}, overall=${entry.quality.overall_status}`
)
.join("\n");
+166 -1
View File
@@ -8,6 +8,7 @@ import { spawnSync } from "child_process";
import { runLint, LintConsoleReporter } from "@microsoft/vally";
const MAX_OUTPUT_LENGTH = 12000;
const EXTERNAL_CANVAS_KEYWORD = "canvas";
const INFRA_ERROR_PATTERNS = [
/\b401\b/,
@@ -69,6 +70,12 @@ function normalizePluginPath(pluginPath) {
return normalized;
}
function hasCanvasKeyword(plugin) {
return (plugin?.keywords ?? []).some(
(keyword) => String(keyword).trim().toLowerCase() === EXTERNAL_CANVAS_KEYWORD,
);
}
function resolveFetchSpec(pluginSource) {
if (pluginSource.sha) {
return pluginSource.sha;
@@ -467,6 +474,148 @@ function runVersionMatchGate(repoDir, plugin, primaryFetchSpec) {
};
}
function checkPathExistsAtLocator(repoDir, locator, repoPath, expectedType) {
const result = runCommand("git", ["cat-file", "-e", `${locator}:${repoPath}`], { cwd: repoDir });
if (result.exitCode === 0) {
if (!expectedType) {
return { exists: true, output: "" };
}
const typeResult = runCommand("git", ["cat-file", "-t", `${locator}:${repoPath}`], { cwd: repoDir });
if (typeResult.exitCode !== 0) {
return {
exists: false,
output: `Unable to verify path "${repoPath}" type at "${locator}": ${typeResult.output}`,
};
}
const actualType = String(typeResult.stdout ?? "").trim();
if (actualType !== expectedType) {
return {
exists: false,
output: "",
kindMismatch: true,
actualType,
};
}
return { exists: true, output: "" };
}
const normalizedOutput = String(result.output ?? "").toLowerCase();
if (
normalizedOutput.includes("not a valid object name")
|| normalizedOutput.includes("path '")
|| normalizedOutput.includes("does not exist")
) {
return { exists: false, output: "" };
}
return {
exists: false,
output: `Unable to verify path "${repoPath}" at "${locator}": ${result.output}`,
};
}
export function runCanvasStructureGate(repoDir, plugin, primaryFetchSpec) {
if (!hasCanvasKeyword(plugin)) {
return {
status: "not_run",
output: "Canvas structure gate skipped because plugin is not tagged with \"canvas\".",
};
}
const normalizedPluginPath = normalizePluginPath(plugin?.source?.path || "/");
const locators = [plugin?.source?.ref, plugin?.source?.sha]
.filter((value) => typeof value === "string" && value.trim().length > 0)
.map((value) => value.trim())
.filter((value, index, values) => values.indexOf(value) === index);
if (locators.length === 0) {
return {
status: "not_run",
output: "Canvas structure gate skipped because neither source.ref nor source.sha was provided.",
};
}
const extensionsDir = toPosixPath(normalizedPluginPath, "extensions");
const extensionEntryPoint = toPosixPath(extensionsDir, "extension.mjs");
let hasFailure = false;
let hasInfraError = false;
const messages = [];
for (const locator of locators) {
if (locator !== primaryFetchSpec) {
const fetchResult = fetchLocatorIntoRepo(repoDir, locator);
if (fetchResult.status === "fail") {
hasFailure = true;
messages.push(`- ${locator}: ${fetchResult.output}`);
continue;
}
if (fetchResult.status === "infra_error") {
hasInfraError = true;
messages.push(`- ${locator}: ${fetchResult.output}`);
continue;
}
}
const extensionDirCheck = checkPathExistsAtLocator(repoDir, locator, extensionsDir, "tree");
if (extensionDirCheck.output) {
hasInfraError = true;
messages.push(`- ${locator}: ${extensionDirCheck.output}`);
continue;
}
if (!extensionDirCheck.exists) {
hasFailure = true;
if (extensionDirCheck.kindMismatch) {
messages.push(`- ${locator}: "${extensionsDir}" must be a directory.`);
} else {
messages.push(`- ${locator}: missing required canvas extension directory "${extensionsDir}".`);
}
continue;
}
const extensionEntryCheck = checkPathExistsAtLocator(repoDir, locator, extensionEntryPoint, "blob");
if (extensionEntryCheck.output) {
hasInfraError = true;
messages.push(`- ${locator}: ${extensionEntryCheck.output}`);
continue;
}
if (!extensionEntryCheck.exists) {
hasFailure = true;
if (extensionEntryCheck.kindMismatch) {
messages.push(`- ${locator}: "${extensionEntryPoint}" must be a file.`);
} else {
messages.push(`- ${locator}: missing required canvas extension entry point "${extensionEntryPoint}".`);
}
continue;
}
messages.push(`- ${locator}: found "${extensionsDir}" with entry point "${extensionEntryPoint}".`);
}
if (hasInfraError) {
return {
status: "infra_error",
output: messages.join("\n"),
};
}
if (hasFailure) {
return {
status: "fail",
output: messages.join("\n"),
};
}
return {
status: "pass",
output: messages.join("\n"),
};
}
function toOverallStatus(states) {
if (states.includes("infra_error")) {
return "infra_error";
@@ -497,11 +646,13 @@ export async function runExternalPluginQualityGates(plugin) {
vally_lint_status: "not_run",
smoke_status: "not_run",
version_match_status: "not_run",
canvas_structure_status: "not_run",
failure_class: "none",
summary: "",
vally_lint_output: "",
smoke_output: "",
version_match_output: "",
canvas_structure_output: "",
};
try {
@@ -513,10 +664,14 @@ export async function runExternalPluginQualityGates(plugin) {
result.vally_lint_status = "fail";
result.smoke_status = "fail";
result.version_match_status = "fail";
result.canvas_structure_status = hasCanvasKeyword(plugin) ? "fail" : "not_run";
result.overall_status = "fail";
result.failure_class = "submitter_fixes";
result.summary = `Plugin path "${plugin.source?.path || "/"}" was not found in the submitted repository snapshot.`;
result.version_match_output = result.summary;
if (hasCanvasKeyword(plugin)) {
result.canvas_structure_output = result.summary;
}
return result;
}
@@ -524,6 +679,10 @@ export async function runExternalPluginQualityGates(plugin) {
result.version_match_status = versionMatchResult.status;
result.version_match_output = versionMatchResult.output;
const canvasStructureResult = runCanvasStructureGate(repoDir, plugin, fetchSpec);
result.canvas_structure_status = canvasStructureResult.status;
result.canvas_structure_output = canvasStructureResult.output;
const vallyResult = await runVallyLintGate(pluginRoot);
result.vally_lint_status = vallyResult.status;
result.vally_lint_output = vallyResult.output;
@@ -532,12 +691,18 @@ export async function runExternalPluginQualityGates(plugin) {
result.smoke_status = smokeResult.status;
result.smoke_output = smokeResult.output;
result.overall_status = toOverallStatus([result.vally_lint_status, result.smoke_status, result.version_match_status]);
result.overall_status = toOverallStatus([
result.vally_lint_status,
result.smoke_status,
result.version_match_status,
result.canvas_structure_status,
]);
result.failure_class = toFailureClass(result.overall_status);
result.summary = [
`- vally lint: ${result.vally_lint_status}`,
`- install smoke test: ${result.smoke_status}`,
`- version match: ${result.version_match_status}`,
`- canvas structure: ${result.canvas_structure_status}`,
`- overall: ${result.overall_status}`,
].join("\n");
+101
View File
@@ -0,0 +1,101 @@
import assert from "node:assert/strict";
import fs from "fs";
import os from "os";
import path from "path";
import { spawnSync } from "child_process";
import { after, test } from "node:test";
import { runCanvasStructureGate } from "./external-plugin-quality-gates.mjs";
const tempDirs = [];
after(() => {
for (const dir of tempDirs) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
function runGit(repoDir, ...args) {
const result = spawnSync("git", args, { cwd: repoDir, encoding: "utf8" });
if (result.status !== 0) {
throw new Error(`git ${args.join(" ")} failed: ${result.stdout}\n${result.stderr}`);
}
return String(result.stdout ?? "").trim();
}
function createTempRepo() {
const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), "external-plugin-quality-"));
tempDirs.push(repoDir);
runGit(repoDir, "init", "-q");
runGit(repoDir, "config", "user.name", "Copilot Test");
runGit(repoDir, "config", "user.email", "copilot@example.com");
return repoDir;
}
function commitAll(repoDir, message) {
runGit(repoDir, "add", "-A");
runGit(repoDir, "commit", "-m", message, "--quiet");
return runGit(repoDir, "rev-parse", "HEAD");
}
test("runCanvasStructureGate passes when extensions/extension.mjs exists", () => {
const repoDir = createTempRepo();
fs.mkdirSync(path.join(repoDir, "extensions"), { recursive: true });
fs.writeFileSync(path.join(repoDir, "extensions", "extension.mjs"), "export default {};\n");
const sha = commitAll(repoDir, "Add canvas extension container");
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, /found "extensions"/);
});
test("runCanvasStructureGate fails when extension entrypoint is only at repo root", () => {
const repoDir = createTempRepo();
fs.writeFileSync(path.join(repoDir, "extension.mjs"), "export default {};\n");
const sha = commitAll(repoDir, "Add root extension entrypoint");
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 directory "extensions"/);
});
test("runCanvasStructureGate fails when extension entrypoint path is a directory", () => {
const repoDir = createTempRepo();
fs.mkdirSync(path.join(repoDir, "extensions", "extension.mjs"), { recursive: true });
fs.writeFileSync(path.join(repoDir, "extensions", "extension.mjs", "placeholder.txt"), "not-a-module\n");
const sha = commitAll(repoDir, "Add invalid extension entrypoint directory");
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, /"extensions\/extension\.mjs" must be a file/);
});
+92 -2
View File
@@ -2,9 +2,11 @@
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { ROOT_FOLDER } from "./constants.mjs";
const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
const EXTENSIONS_DIR = path.join(ROOT_FOLDER, "extensions");
/**
* Recursively copy a directory.
@@ -22,6 +24,17 @@ function copyDirRecursive(src, dest) {
}
}
function copyEntryRecursive(srcPath, destPath) {
const stats = fs.statSync(srcPath);
if (stats.isDirectory()) {
copyDirRecursive(srcPath, destPath);
return;
}
fs.mkdirSync(path.dirname(destPath), { recursive: true });
fs.copyFileSync(srcPath, destPath);
}
/**
* Resolve a plugin-relative path to the repo-root source file.
*
@@ -45,6 +58,48 @@ function resolveSource(relPath) {
return null;
}
export function materializeExtensionPlugin(extensionPath) {
const pluginJsonPath = path.join(extensionPath, ".github", "plugin", "plugin.json");
if (!fs.existsSync(pluginJsonPath)) {
return { copiedEntries: 0, manifestUpdated: false, skipped: true };
}
let metadata;
try {
metadata = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8"));
} catch (err) {
throw new Error(`Failed to parse ${pluginJsonPath}: ${err.message}`);
}
const extensionContainerPath = path.join(extensionPath, "extensions");
fs.rmSync(extensionContainerPath, { recursive: true, force: true });
fs.mkdirSync(extensionContainerPath, { recursive: true });
let copiedEntries = 0;
for (const entry of fs.readdirSync(extensionPath, { withFileTypes: true })) {
if (entry.name === ".github" || entry.name === "extensions") {
continue;
}
copyEntryRecursive(
path.join(extensionPath, entry.name),
path.join(extensionContainerPath, entry.name)
);
copiedEntries++;
}
let manifestUpdated = false;
if (metadata.extensions !== "extensions") {
metadata.extensions = "extensions";
manifestUpdated = true;
}
if (manifestUpdated) {
fs.writeFileSync(pluginJsonPath, JSON.stringify(metadata, null, 2) + "\n", "utf8");
}
return { copiedEntries, manifestUpdated, skipped: false };
}
function materializePlugins() {
console.log("Materializing plugin files...\n");
@@ -61,6 +116,8 @@ function materializePlugins() {
let totalAgents = 0;
let totalSkills = 0;
let totalExtensions = 0;
let totalExtensionPlugins = 0;
let totalExtensionPluginEntries = 0;
let warnings = 0;
let errors = 0;
@@ -185,7 +242,36 @@ function materializePlugins() {
}
}
console.log(`\nDone. Copied ${totalAgents} agents, ${totalSkills} skills, ${totalExtensions} extensions.`);
if (fs.existsSync(EXTENSIONS_DIR)) {
const extensionDirs = fs.readdirSync(EXTENSIONS_DIR, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.sort();
for (const dirName of extensionDirs) {
const extensionPath = path.join(EXTENSIONS_DIR, dirName);
if (!fs.existsSync(path.join(extensionPath, "extension.mjs"))) {
continue;
}
try {
const result = materializeExtensionPlugin(extensionPath);
if (result.skipped) {
continue;
}
totalExtensionPlugins++;
totalExtensionPluginEntries += result.copiedEntries;
console.log(`${dirName}: materialized extension bundle into ./extensions (${result.copiedEntries} entries)`);
} catch (err) {
console.error(`Error: Failed to materialize extension plugin ${dirName}: ${err.message}`);
errors++;
}
}
}
console.log(`\nDone. Copied ${totalAgents} agents, ${totalSkills} skills, ${totalExtensions} plugin extension refs.`);
console.log(`Materialized ${totalExtensionPlugins} extension plugins (${totalExtensionPluginEntries} top-level entries).`);
if (warnings > 0) {
console.log(`${warnings} warning(s).`);
}
@@ -195,4 +281,8 @@ function materializePlugins() {
}
}
materializePlugins();
export { materializePlugins };
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
materializePlugins();
}
+48
View File
@@ -0,0 +1,48 @@
import assert from "node:assert/strict";
import fs from "fs";
import os from "os";
import path from "path";
import { after, test } from "node:test";
import { materializeExtensionPlugin } from "./materialize-plugins.mjs";
const tempDirs = [];
after(() => {
for (const dir of tempDirs) {
fs.rmSync(dir, { recursive: true, force: true });
}
});
test("materializeExtensionPlugin writes extension bundles to ./extensions and rewrites manifest", () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "materialize-extension-plugin-"));
tempDirs.push(tempDir);
const pluginDir = path.join(tempDir, "extension-plugin");
fs.mkdirSync(path.join(pluginDir, ".github", "plugin"), { recursive: true });
fs.mkdirSync(path.join(pluginDir, "assets"), { recursive: true });
fs.writeFileSync(path.join(pluginDir, ".github", "plugin", "plugin.json"), JSON.stringify({
name: "test-extension-plugin",
description: "test plugin",
version: "1.0.0",
logo: "assets/preview.png",
extensions: ".",
}, null, 2));
fs.writeFileSync(path.join(pluginDir, "extension.mjs"), "export default {};\n");
fs.writeFileSync(path.join(pluginDir, "README.md"), "# test\n");
fs.writeFileSync(path.join(pluginDir, "assets", "preview.png"), "fake-image-bytes");
const result = materializeExtensionPlugin(pluginDir);
assert.equal(result.skipped, false);
assert.equal(result.manifestUpdated, true);
assert.equal(result.copiedEntries, 3);
assert.equal(fs.existsSync(path.join(pluginDir, "extensions", "extension.mjs")), true);
assert.equal(fs.existsSync(path.join(pluginDir, "extensions", "assets", "preview.png")), true);
assert.equal(fs.existsSync(path.join(pluginDir, "extensions", "README.md")), true);
assert.equal(fs.existsSync(path.join(pluginDir, "extensions", ".github")), false);
const pluginManifest = JSON.parse(
fs.readFileSync(path.join(pluginDir, ".github", "plugin", "plugin.json"), "utf8")
);
assert.equal(pluginManifest.extensions, "extensions");
});
+7 -2
View File
@@ -305,9 +305,14 @@ function validateExtensionManifest(folderName) {
errors.push("x-awesome-copilot field must not be present (use convention-based logo instead)");
}
// Extension convention: extensions field must be "."
if (parsed.extenions !== undefined) {
errors.push('use "extensions" field (found misspelled key "extenions")');
}
// Extension convention: source manifests keep extensions at repository root.
// Materialization rewrites this to "extensions" on distribution branches.
if (parsed.extensions !== ".") {
errors.push('extensions field must be exactly "." (extension convention)');
errors.push('extensions field must be exactly "." in source manifests (extension convention)');
}
return { errors, plugin: parsedPlugin };