mirror of
https://github.com/github/awesome-copilot.git
synced 2026-05-29 18:11:45 +00:00
chore: publish from staged
This commit is contained in:
@@ -0,0 +1,355 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "fs";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { spawnSync } from "child_process";
|
||||
|
||||
const MAX_OUTPUT_LENGTH = 12000;
|
||||
const SKILL_VALIDATOR_ARCHIVE_URL = "https://github.com/dotnet/skills/releases/download/skill-validator-nightly/skill-validator-linux-x64.tar.gz";
|
||||
|
||||
const INFRA_ERROR_PATTERNS = [
|
||||
/\b401\b/,
|
||||
/\b403\b/,
|
||||
/authentication (required|failed|error)/,
|
||||
/unauthenticated/,
|
||||
/unauthorized/,
|
||||
/not logged in/,
|
||||
/please (log in|authenticate|sign in)/,
|
||||
/invalid (access |auth )?token/,
|
||||
/credentials? (are )?expired/,
|
||||
/dns.*(resolve|lookup|fail)/,
|
||||
/network.*unreachable/,
|
||||
/connection (refused|reset)/,
|
||||
/\btimeout\b/,
|
||||
/enotfound/,
|
||||
/econnrefused/,
|
||||
/etimedout/,
|
||||
];
|
||||
|
||||
function truncateOutput(value) {
|
||||
const normalized = String(value ?? "").replace(/\x1b\[[0-9;]*m/g, "").trim();
|
||||
if (normalized.length <= MAX_OUTPUT_LENGTH) {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
return `${normalized.slice(0, MAX_OUTPUT_LENGTH)}\n...output truncated...`;
|
||||
}
|
||||
|
||||
function runCommand(command, args, options = {}) {
|
||||
const result = spawnSync(command, args, {
|
||||
encoding: "utf8",
|
||||
...options,
|
||||
});
|
||||
|
||||
return {
|
||||
exitCode: typeof result.status === "number" ? result.status : 1,
|
||||
stdout: truncateOutput(result.stdout),
|
||||
stderr: truncateOutput(result.stderr),
|
||||
output: truncateOutput(`${result.stdout ?? ""}\n${result.stderr ?? ""}`),
|
||||
error: result.error ? String(result.error.message ?? result.error) : "",
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePluginPath(pluginPath) {
|
||||
if (!pluginPath || pluginPath === "/") {
|
||||
return "";
|
||||
}
|
||||
|
||||
const normalized = String(pluginPath).trim().replace(/^\/+|\/+$/g, "");
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (normalized.includes("..") || normalized.includes("\\")) {
|
||||
throw new Error(`Invalid plugin path "${pluginPath}"`);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function resolveFetchSpec(pluginSource) {
|
||||
if (pluginSource.sha) {
|
||||
return pluginSource.sha;
|
||||
}
|
||||
|
||||
if (!pluginSource.ref) {
|
||||
throw new Error("source.ref or source.sha is required for quality gates");
|
||||
}
|
||||
|
||||
const ref = String(pluginSource.ref).trim();
|
||||
if (!ref) {
|
||||
throw new Error("source.ref or source.sha is required for quality gates");
|
||||
}
|
||||
|
||||
if (ref.startsWith("refs/")) {
|
||||
return ref;
|
||||
}
|
||||
|
||||
return ref;
|
||||
}
|
||||
|
||||
function classifySmokeFailure(output) {
|
||||
const normalized = String(output ?? "").toLowerCase();
|
||||
if (INFRA_ERROR_PATTERNS.some((pattern) => pattern.test(normalized))) {
|
||||
return "infra_error";
|
||||
}
|
||||
|
||||
return "fail";
|
||||
}
|
||||
|
||||
function ensureDirectory(dirPath) {
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
|
||||
function cloneSubmissionRepository(workDir, plugin) {
|
||||
const repoDir = path.join(workDir, "submission");
|
||||
ensureDirectory(repoDir);
|
||||
|
||||
const sourceRepo = plugin.source?.repo;
|
||||
const fetchSpec = resolveFetchSpec(plugin.source ?? {});
|
||||
|
||||
const init = runCommand("git", ["init", "-q"], { cwd: repoDir });
|
||||
if (init.exitCode !== 0) {
|
||||
throw new Error(`git init failed: ${init.output}`);
|
||||
}
|
||||
|
||||
const addRemote = runCommand("git", ["remote", "add", "origin", `https://github.com/${sourceRepo}.git`], { cwd: repoDir });
|
||||
if (addRemote.exitCode !== 0) {
|
||||
throw new Error(`git remote add failed: ${addRemote.output}`);
|
||||
}
|
||||
|
||||
const fetch = runCommand("git", ["fetch", "--depth=1", "origin", fetchSpec], { cwd: repoDir });
|
||||
if (fetch.exitCode !== 0) {
|
||||
throw new Error(`git fetch failed for ${fetchSpec}: ${fetch.output}`);
|
||||
}
|
||||
|
||||
const checkout = runCommand("git", ["checkout", "--detach", "FETCH_HEAD"], { cwd: repoDir });
|
||||
if (checkout.exitCode !== 0) {
|
||||
throw new Error(`git checkout failed: ${checkout.output}`);
|
||||
}
|
||||
|
||||
return repoDir;
|
||||
}
|
||||
|
||||
function downloadSkillValidator(workDir) {
|
||||
const validatorDir = path.join(workDir, "skill-validator");
|
||||
ensureDirectory(validatorDir);
|
||||
const archivePath = path.join(validatorDir, "skill-validator-linux-x64.tar.gz");
|
||||
|
||||
const download = runCommand("curl", ["-fsSL", SKILL_VALIDATOR_ARCHIVE_URL, "-o", archivePath]);
|
||||
if (download.exitCode !== 0) {
|
||||
throw new Error(`Failed to download skill-validator: ${download.output}`);
|
||||
}
|
||||
|
||||
const untar = runCommand("tar", ["-xzf", archivePath, "-C", validatorDir]);
|
||||
if (untar.exitCode !== 0) {
|
||||
throw new Error(`Failed to extract skill-validator: ${untar.output}`);
|
||||
}
|
||||
|
||||
const binaryPath = path.join(validatorDir, "skill-validator");
|
||||
if (!fs.existsSync(binaryPath)) {
|
||||
throw new Error("skill-validator binary was not found after extraction");
|
||||
}
|
||||
|
||||
runCommand("chmod", ["+x", binaryPath]);
|
||||
return binaryPath;
|
||||
}
|
||||
|
||||
function runSkillValidatorGate(workDir, pluginRoot) {
|
||||
try {
|
||||
const validatorBinary = downloadSkillValidator(workDir);
|
||||
const check = runCommand(validatorBinary, ["check", "--verbose", "--plugin", pluginRoot]);
|
||||
|
||||
if (check.exitCode === 0) {
|
||||
return { status: "pass", output: check.output };
|
||||
}
|
||||
|
||||
return { status: "fail", output: check.output };
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "infra_error",
|
||||
output: truncateOutput(error.message),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function buildEphemeralMarketplace(workDir, plugin) {
|
||||
const marketplaceDir = path.join(workDir, "marketplace");
|
||||
ensureDirectory(marketplaceDir);
|
||||
|
||||
const marketplace = {
|
||||
name: "external-plugin-intake",
|
||||
metadata: {
|
||||
description: "Temporary marketplace for external plugin intake smoke tests",
|
||||
version: "1.0.0",
|
||||
pluginRoot: ".",
|
||||
},
|
||||
owner: {
|
||||
name: "awesome-copilot-intake",
|
||||
email: "noreply@github.com",
|
||||
},
|
||||
plugins: [plugin],
|
||||
};
|
||||
|
||||
fs.writeFileSync(path.join(marketplaceDir, "marketplace.json"), `${JSON.stringify(marketplace, null, 2)}\n`);
|
||||
return marketplaceDir;
|
||||
}
|
||||
|
||||
function runInstallSmokeGate(workDir, plugin) {
|
||||
if (runCommand("bash", ["-lc", "command -v copilot"]).exitCode !== 0) {
|
||||
return {
|
||||
status: "infra_error",
|
||||
output: "copilot CLI is not available on this runner.",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const homeDir = path.join(workDir, "copilot-home");
|
||||
ensureDirectory(homeDir);
|
||||
const marketplaceDir = buildEphemeralMarketplace(workDir, plugin);
|
||||
|
||||
const env = {
|
||||
...process.env,
|
||||
HOME: homeDir,
|
||||
XDG_CONFIG_HOME: path.join(homeDir, ".config"),
|
||||
XDG_CACHE_HOME: path.join(homeDir, ".cache"),
|
||||
XDG_DATA_HOME: path.join(homeDir, ".local", "share"),
|
||||
};
|
||||
|
||||
const marketplaceAdd = runCommand("copilot", ["plugin", "marketplace", "add", marketplaceDir], { env });
|
||||
if (marketplaceAdd.exitCode !== 0) {
|
||||
const status = classifySmokeFailure(marketplaceAdd.output);
|
||||
return { status, output: marketplaceAdd.output };
|
||||
}
|
||||
|
||||
const install = runCommand("copilot", ["plugin", "install", `${plugin.name}@external-plugin-intake`], { env });
|
||||
if (install.exitCode !== 0) {
|
||||
const status = classifySmokeFailure(install.output);
|
||||
return { status, output: install.output };
|
||||
}
|
||||
|
||||
const installedPluginPath = path.join(homeDir, ".copilot", "installed-plugins", "external-plugin-intake", plugin.name);
|
||||
const pluginManifestPath = path.join(installedPluginPath, ".github", "plugin", "plugin.json");
|
||||
if (!fs.existsSync(installedPluginPath) || !fs.existsSync(pluginManifestPath)) {
|
||||
return {
|
||||
status: "fail",
|
||||
output: `Plugin installed but expected files were missing at ${installedPluginPath}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "pass",
|
||||
output: `Install smoke test succeeded. Verified ${pluginManifestPath}.`,
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
status: "infra_error",
|
||||
output: truncateOutput(error.message),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function toOverallStatus(skillStatus, smokeStatus) {
|
||||
const states = [skillStatus, smokeStatus];
|
||||
if (states.includes("infra_error")) {
|
||||
return "infra_error";
|
||||
}
|
||||
if (states.includes("fail")) {
|
||||
return "fail";
|
||||
}
|
||||
if (states.every((state) => state === "not_run")) {
|
||||
return "not_run";
|
||||
}
|
||||
return "pass";
|
||||
}
|
||||
|
||||
function toFailureClass(overallStatus) {
|
||||
if (overallStatus === "infra_error") {
|
||||
return "infra";
|
||||
}
|
||||
if (overallStatus === "fail") {
|
||||
return "submitter_fixes";
|
||||
}
|
||||
return "none";
|
||||
}
|
||||
|
||||
export function runExternalPluginQualityGates(plugin) {
|
||||
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "external-plugin-quality-"));
|
||||
const result = {
|
||||
overall_status: "not_run",
|
||||
skill_validator_status: "not_run",
|
||||
smoke_status: "not_run",
|
||||
failure_class: "none",
|
||||
summary: "",
|
||||
skill_validator_output: "",
|
||||
smoke_output: "",
|
||||
};
|
||||
|
||||
try {
|
||||
const repoDir = cloneSubmissionRepository(workDir, plugin);
|
||||
const normalizedPluginPath = normalizePluginPath(plugin.source?.path || "/");
|
||||
const pluginRoot = normalizedPluginPath ? path.join(repoDir, normalizedPluginPath) : repoDir;
|
||||
|
||||
if (!fs.existsSync(pluginRoot) || !fs.statSync(pluginRoot).isDirectory()) {
|
||||
result.skill_validator_status = "fail";
|
||||
result.smoke_status = "fail";
|
||||
result.overall_status = "fail";
|
||||
result.failure_class = "submitter_fixes";
|
||||
result.summary = `Plugin path "${plugin.source?.path || "/"}" was not found in the submitted repository snapshot.`;
|
||||
return result;
|
||||
}
|
||||
|
||||
const skillResult = runSkillValidatorGate(workDir, pluginRoot);
|
||||
result.skill_validator_status = skillResult.status;
|
||||
result.skill_validator_output = skillResult.output;
|
||||
|
||||
const smokeResult = runInstallSmokeGate(workDir, plugin);
|
||||
result.smoke_status = smokeResult.status;
|
||||
result.smoke_output = smokeResult.output;
|
||||
|
||||
result.overall_status = toOverallStatus(result.skill_validator_status, result.smoke_status);
|
||||
result.failure_class = toFailureClass(result.overall_status);
|
||||
result.summary = [
|
||||
`- skill-validator: ${result.skill_validator_status}`,
|
||||
`- install smoke test: ${result.smoke_status}`,
|
||||
`- overall: ${result.overall_status}`,
|
||||
].join("\n");
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
result.overall_status = "infra_error";
|
||||
result.failure_class = "infra";
|
||||
result.summary = truncateOutput(error.message);
|
||||
result.skill_validator_output = truncateOutput(error.stack || error.message);
|
||||
return result;
|
||||
} finally {
|
||||
fs.rmSync(workDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
function parseCliArgs(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;
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
const args = parseCliArgs(process.argv.slice(2));
|
||||
if (!args["plugin-json"]) {
|
||||
console.error("Usage: node ./eng/external-plugin-quality-gates.mjs --plugin-json '<json>'");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const plugin = JSON.parse(args["plugin-json"]);
|
||||
const result = runExternalPluginQualityGates(plugin);
|
||||
process.stdout.write(`${JSON.stringify(result)}\n`);
|
||||
}
|
||||
Reference in New Issue
Block a user