feat: add public external plugin workflows (#1723)

* 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>
This commit is contained in:
Aaron Powell
2026-05-15 15:37:18 +10:00
committed by GitHub
parent ca8412356a
commit e66aa80240
13 changed files with 2767 additions and 102 deletions
+12 -81
View File
@@ -3,84 +3,11 @@
import fs from "fs";
import path from "path";
import { ROOT_FOLDER } from "./constants.mjs";
import { readExternalPlugins } from "./external-plugin-validation.mjs";
const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
const EXTERNAL_PLUGINS_FILE = path.join(ROOT_FOLDER, "plugins", "external.json");
const MARKETPLACE_FILE = path.join(ROOT_FOLDER, ".github/plugin", "marketplace.json");
/**
* Validate an external plugin entry has required fields and a non-local source
* @param {object} plugin - External plugin entry
* @param {number} index - Index in the array (for error messages)
* @returns {string[]} - Array of validation error messages
*/
function validateExternalPlugin(plugin, index) {
const errors = [];
const prefix = `external.json[${index}]`;
if (!plugin.name || typeof plugin.name !== "string") {
errors.push(`${prefix}: "name" is required and must be a string`);
}
if (!plugin.description || typeof plugin.description !== "string") {
errors.push(`${prefix}: "description" is required and must be a string`);
}
if (!plugin.version || typeof plugin.version !== "string") {
errors.push(`${prefix}: "version" is required and must be a string`);
}
if (!plugin.source) {
errors.push(`${prefix}: "source" is required`);
} else if (typeof plugin.source === "string") {
errors.push(`${prefix}: "source" must be an object (local file paths are not allowed for external plugins)`);
} else if (typeof plugin.source === "object") {
if (!plugin.source.source) {
errors.push(`${prefix}: "source.source" is required (e.g. "github", "url", "npm", "pip")`);
}
} else {
errors.push(`${prefix}: "source" must be an object`);
}
return errors;
}
/**
* Read external plugin entries from external.json
* @returns {Array} - Array of external plugin entries (merged as-is)
*/
function readExternalPlugins() {
if (!fs.existsSync(EXTERNAL_PLUGINS_FILE)) {
return [];
}
try {
const content = fs.readFileSync(EXTERNAL_PLUGINS_FILE, "utf8");
const plugins = JSON.parse(content);
if (!Array.isArray(plugins)) {
console.warn("Warning: external.json must contain an array");
return [];
}
// Validate each entry
let hasErrors = false;
for (let i = 0; i < plugins.length; i++) {
const errors = validateExternalPlugin(plugins[i], i);
if (errors.length > 0) {
errors.forEach(e => console.error(`Error: ${e}`));
hasErrors = true;
}
}
if (hasErrors) {
console.error("Error: external.json contains invalid entries");
process.exit(1);
}
return plugins;
} catch (error) {
console.error(`Error reading external.json: ${error.message}`);
return [];
}
}
/**
* Read plugin metadata from plugin.json file
* @param {string} pluginDir - Path to plugin directory
@@ -142,16 +69,20 @@ function generateMarketplace() {
}
// Read external plugins and merge as-is
const externalPlugins = readExternalPlugins();
const { plugins: externalPlugins, errors: externalErrors, warnings: externalWarnings } = readExternalPlugins({
localPluginNames: plugins.map((plugin) => plugin.name),
policy: "marketplace",
});
externalWarnings.forEach((warning) => console.warn(`Warning: ${warning}`));
if (externalErrors.length > 0) {
externalErrors.forEach((error) => console.error(`Error: ${error}`));
console.error("Error: external.json contains invalid entries");
process.exit(1);
}
if (externalPlugins.length > 0) {
console.log(`\nFound ${externalPlugins.length} external plugins`);
// Warn on duplicate names
const localNames = new Set(plugins.map(p => p.name));
for (const ext of externalPlugins) {
if (localNames.has(ext.name)) {
console.warn(`Warning: external plugin "${ext.name}" has the same name as a local plugin`);
}
plugins.push(ext);
console.log(`✓ Added external plugin: ${ext.name}`);
}