mirror of
https://github.com/github/awesome-copilot.git
synced 2026-05-27 17:11:44 +00:00
e66aa80240
* feat: add external plugin submission workflows Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * minor adjustment to contributing guide * fix: address external plugin review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Reverting some changes to the readme.agents.md file * fix: address follow-up review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: tighten external plugin workflows Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
189 lines
5.0 KiB
JavaScript
189 lines
5.0 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import fs from "fs";
|
|
import path from "path";
|
|
import { fileURLToPath } from "url";
|
|
import { ROOT_FOLDER } from "./constants.mjs";
|
|
import {
|
|
EXTERNAL_PLUGINS_FILE,
|
|
readExternalPlugins,
|
|
validateExternalPlugins,
|
|
} from "./external-plugin-validation.mjs";
|
|
import { evaluateExternalPluginIssue } from "./external-plugin-intake.mjs";
|
|
|
|
export const DECISION_COMMANDS = Object.freeze({
|
|
approve: "/approve",
|
|
reject: "/reject",
|
|
});
|
|
|
|
function normalizeValue(value) {
|
|
return String(value ?? "").trim().toLowerCase();
|
|
}
|
|
|
|
function normalizeRepositoryUrl(value) {
|
|
const normalized = normalizeValue(value);
|
|
if (!normalized) {
|
|
return undefined;
|
|
}
|
|
|
|
return normalized
|
|
.replace(/^https:\/\/github\.com\//, "")
|
|
.replace(/\.git$/i, "")
|
|
.replace(/^\/+|\/+$/g, "");
|
|
}
|
|
|
|
function normalizePathValue(value) {
|
|
return String(value ?? "")
|
|
.trim()
|
|
.replace(/^\/+|\/+$/g, "")
|
|
.toLowerCase();
|
|
}
|
|
|
|
export function parseDecisionCommand(body) {
|
|
const match = String(body ?? "").match(/(?:^|\n)\s*\/(approve|reject)(?=\s|$)([\s\S]*)$/i);
|
|
if (!match) {
|
|
return undefined;
|
|
}
|
|
|
|
const command = match[1].toLowerCase();
|
|
const reason = match[2]?.trim() || undefined;
|
|
|
|
return {
|
|
command,
|
|
reason: command === "reject" ? reason : undefined,
|
|
};
|
|
}
|
|
|
|
export function slugifyPluginName(value) {
|
|
const slug = String(value ?? "")
|
|
.trim()
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "");
|
|
|
|
return slug || "external-plugin";
|
|
}
|
|
|
|
function readLocalPluginNames() {
|
|
const pluginsDir = path.join(ROOT_FOLDER, "plugins");
|
|
if (!fs.existsSync(pluginsDir)) {
|
|
return [];
|
|
}
|
|
|
|
return fs.readdirSync(pluginsDir, { withFileTypes: true })
|
|
.filter((entry) => entry.isDirectory())
|
|
.map((entry) => entry.name);
|
|
}
|
|
|
|
function pluginsMatch(left, right) {
|
|
const leftName = normalizeValue(left?.name);
|
|
const rightName = normalizeValue(right?.name);
|
|
const leftRepo = normalizeValue(left?.source?.repo);
|
|
const rightRepo = normalizeValue(right?.source?.repo);
|
|
const leftPath = normalizePathValue(left?.source?.path);
|
|
const rightPath = normalizePathValue(right?.source?.path);
|
|
const leftRepository = normalizeRepositoryUrl(left?.repository);
|
|
const rightRepository = normalizeRepositoryUrl(right?.repository);
|
|
|
|
if (leftName && rightName && leftName === rightName) {
|
|
return true;
|
|
}
|
|
|
|
const repoMatches = leftRepo && rightRepo && leftRepo === rightRepo;
|
|
const repositoryMatches = leftRepository && rightRepository && leftRepository === rightRepository;
|
|
const pathKnown = Boolean(leftPath || rightPath);
|
|
const pathMatches = leftPath === rightPath;
|
|
|
|
if ((repoMatches || repositoryMatches) && pathKnown && pathMatches) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export function upsertExternalPlugin(plugin, { filePath = EXTERNAL_PLUGINS_FILE } = {}) {
|
|
const { plugins, errors } = readExternalPlugins({
|
|
filePath,
|
|
localPluginNames: readLocalPluginNames(),
|
|
policy: "marketplace",
|
|
});
|
|
|
|
if (errors.length > 0) {
|
|
throw new Error(errors.join("\n"));
|
|
}
|
|
|
|
const updatedPlugins = [...plugins];
|
|
const existingIndex = updatedPlugins.findIndex((existingPlugin) => pluginsMatch(existingPlugin, plugin));
|
|
const action = existingIndex === -1 ? "inserted" : "updated";
|
|
|
|
if (existingIndex === -1) {
|
|
updatedPlugins.push(plugin);
|
|
} else {
|
|
updatedPlugins[existingIndex] = plugin;
|
|
}
|
|
|
|
updatedPlugins.sort((left, right) => left.name.localeCompare(right.name, undefined, { sensitivity: "base" }));
|
|
|
|
const { errors: validationErrors } = validateExternalPlugins(updatedPlugins, {
|
|
localPluginNames: readLocalPluginNames(),
|
|
policy: "marketplace",
|
|
});
|
|
|
|
if (validationErrors.length > 0) {
|
|
throw new Error(validationErrors.join("\n"));
|
|
}
|
|
|
|
const changed = JSON.stringify(updatedPlugins) !== JSON.stringify(plugins);
|
|
if (changed) {
|
|
fs.writeFileSync(filePath, `${JSON.stringify(updatedPlugins, null, 2)}\n`);
|
|
}
|
|
|
|
return {
|
|
action,
|
|
changed,
|
|
plugin,
|
|
};
|
|
}
|
|
|
|
function readCliArgs(argv) {
|
|
const args = {};
|
|
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
const key = argv[index];
|
|
if (!key.startsWith("--")) {
|
|
continue;
|
|
}
|
|
|
|
args[key.slice(2)] = argv[index + 1];
|
|
index += 1;
|
|
}
|
|
|
|
return args;
|
|
}
|
|
|
|
const isCli = process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
|
|
|
if (isCli) {
|
|
const [command, eventPath] = process.argv.slice(2);
|
|
|
|
if (command !== "approve" || !eventPath) {
|
|
console.error("Usage: node ./eng/external-plugin-approval.mjs approve <github-event.json> [--file <path>]");
|
|
process.exit(1);
|
|
}
|
|
|
|
const args = readCliArgs(process.argv.slice(4));
|
|
const event = JSON.parse(fs.readFileSync(eventPath, "utf8"));
|
|
const evaluation = await evaluateExternalPluginIssue({
|
|
issue: event.issue,
|
|
token: process.env.GITHUB_TOKEN,
|
|
});
|
|
|
|
if (!evaluation.valid) {
|
|
console.error(evaluation.errors.join("\n"));
|
|
process.exit(1);
|
|
}
|
|
|
|
const result = upsertExternalPlugin(evaluation.plugin, { filePath: args.file });
|
|
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
}
|