mirror of
https://github.com/github/awesome-copilot.git
synced 2026-07-16 10:53:25 +00:00
chore: publish from main
This commit is contained in:
@@ -15,6 +15,8 @@ body:
|
||||
- Public submissions are **GitHub-only** in v1.
|
||||
- The plugin must live in a **public GitHub repository**.
|
||||
- Provide an immutable **ref**, **sha**, or both for review.
|
||||
- If your plugin includes a canvas extension, include the **canvas** keyword.
|
||||
- Canvas plugins are validated for `logo: "assets/preview.png"` using the submitted immutable **sha** or **ref**.
|
||||
- Do **not** open a PR that edits `plugins/external.json` directly.
|
||||
- type: input
|
||||
id: plugin-name
|
||||
@@ -106,7 +108,7 @@ body:
|
||||
id: keywords
|
||||
attributes:
|
||||
label: Keywords
|
||||
description: Comma-separated or newline-separated lowercase tags.
|
||||
description: Comma-separated or newline-separated lowercase tags. Include `canvas` if the plugin contains a canvas extension.
|
||||
placeholder: |
|
||||
automation
|
||||
github
|
||||
|
||||
@@ -559,7 +559,11 @@ jobs:
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issueNumber: context.issue.number,
|
||||
desiredLabels: new Set(['external-plugin', 'ready-for-review'])
|
||||
desiredLabels: new Set([
|
||||
'external-plugin',
|
||||
'ready-for-review',
|
||||
...(labelNames.has('external-plugin-canvas') ? ['external-plugin-canvas'] : [])
|
||||
])
|
||||
});
|
||||
|
||||
const marker = '<!-- external-plugin-mark-ready-override -->';
|
||||
|
||||
@@ -44,6 +44,10 @@ jobs:
|
||||
color: 'FEF2C0',
|
||||
description: 'Public external plugin submission'
|
||||
},
|
||||
'external-plugin-canvas': {
|
||||
color: '1D76DB',
|
||||
description: 'External plugin submission includes a canvas extension'
|
||||
},
|
||||
'hooks': {
|
||||
color: 'C2E0C6',
|
||||
description: 'PR touches hooks'
|
||||
|
||||
@@ -3,6 +3,10 @@ export const EXTERNAL_PLUGIN_INTAKE_LABELS = Object.freeze({
|
||||
color: "FEF2C0",
|
||||
description: "Public external plugin submission",
|
||||
},
|
||||
"external-plugin-canvas": {
|
||||
color: "1D76DB",
|
||||
description: "External plugin submission includes a canvas extension",
|
||||
},
|
||||
"awaiting-review": {
|
||||
color: "FBCA04",
|
||||
description: "Submission is waiting for automated intake validation",
|
||||
@@ -27,6 +31,7 @@ export const EXTERNAL_PLUGIN_INTAKE_LABELS = Object.freeze({
|
||||
|
||||
const EXTERNAL_PLUGIN_INTAKE_SYNC_LABELS = Object.freeze([
|
||||
"external-plugin",
|
||||
"external-plugin-canvas",
|
||||
"awaiting-review",
|
||||
"ready-for-review",
|
||||
"requires-submitter-fixes",
|
||||
@@ -129,6 +134,9 @@ export async function applyExternalPluginIntakeEvaluation({
|
||||
rejected: new Set(["external-plugin", "rejected"]),
|
||||
};
|
||||
const desiredLabels = desiredLabelsByState[state] ?? desiredLabelsByState.rejected;
|
||||
if (evaluation.isCanvasPlugin) {
|
||||
desiredLabels.add("external-plugin-canvas");
|
||||
}
|
||||
|
||||
await syncExternalPluginIntakeLabels({
|
||||
github,
|
||||
|
||||
@@ -54,6 +54,13 @@ const FIELD_TITLES = Object.freeze({
|
||||
const LEGACY_FIELD_TITLES = Object.freeze({
|
||||
immutableRef: "Immutable ref to review",
|
||||
});
|
||||
const EXTERNAL_CANVAS_KEYWORD = "canvas";
|
||||
const EXTERNAL_CANVAS_PREVIEW_PATH = "assets/preview.png";
|
||||
const EXTERNAL_PLUGIN_ROOT_MANIFEST_PATHS = Object.freeze([
|
||||
".github/plugin/plugin.json",
|
||||
".plugin/plugin.json",
|
||||
"plugin.json",
|
||||
]);
|
||||
|
||||
function normalizeMultilineText(value) {
|
||||
return String(value ?? "").replace(/\r\n/g, "\n");
|
||||
@@ -116,6 +123,29 @@ function parseKeywords(value) {
|
||||
return keywords.length > 0 ? keywords : undefined;
|
||||
}
|
||||
|
||||
function hasCanvasKeyword(plugin) {
|
||||
return (plugin?.keywords ?? []).some(
|
||||
(keyword) => String(keyword).trim().toLowerCase() === EXTERNAL_CANVAS_KEYWORD,
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeRepoRelativePath(value) {
|
||||
const normalized = stripNoResponse(value);
|
||||
if (!normalized || normalized === "/") {
|
||||
return "";
|
||||
}
|
||||
|
||||
return normalized.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
|
||||
}
|
||||
|
||||
function joinRepoPath(...segments) {
|
||||
return segments
|
||||
.map((segment) => String(segment ?? "").trim())
|
||||
.filter(Boolean)
|
||||
.join("/")
|
||||
.replace(/\/+/g, "/");
|
||||
}
|
||||
|
||||
function parseChecklist(value) {
|
||||
const checked = new Set();
|
||||
const normalized = normalizeMultilineText(value);
|
||||
@@ -231,6 +261,33 @@ async function fetchGitHubJson(apiPath, token) {
|
||||
}
|
||||
}
|
||||
|
||||
function encodeRepoContentPath(value) {
|
||||
return String(value)
|
||||
.split("/")
|
||||
.filter((segment) => segment.length > 0)
|
||||
.map((segment) => encodeURIComponent(segment))
|
||||
.join("/");
|
||||
}
|
||||
|
||||
async function fetchGitHubFile(repo, filePath, ref, token) {
|
||||
const encodedRepo = encodeRepoPath(repo);
|
||||
const encodedPath = encodeRepoContentPath(filePath);
|
||||
return fetchGitHubJson(
|
||||
`/repos/${encodedRepo}/contents/${encodedPath}?ref=${encodeURIComponent(ref)}`,
|
||||
token,
|
||||
);
|
||||
}
|
||||
|
||||
function decodeGitHubFileContent(fileResponse) {
|
||||
const encodedContent = fileResponse?.data?.content;
|
||||
if (!encodedContent || typeof encodedContent !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const normalized = encodedContent.replace(/\n/g, "");
|
||||
return Buffer.from(normalized, "base64").toString("utf8");
|
||||
}
|
||||
|
||||
function encodeRepoPath(repo) {
|
||||
const [owner, name] = String(repo).split("/");
|
||||
return `${encodeURIComponent(owner ?? "")}/${encodeURIComponent(name ?? "")}`;
|
||||
@@ -273,6 +330,7 @@ async function validateRemoteRepository(repo, { ref, sha }, errors, warnings, to
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (!ref) {
|
||||
@@ -322,6 +380,102 @@ async function validateRemoteRepository(repo, { ref, sha }, errors, warnings, to
|
||||
}
|
||||
}
|
||||
|
||||
async function validateCanvasPluginMetadata(plugin, errors, warnings, token) {
|
||||
const repo = plugin?.source?.repo;
|
||||
const sha = plugin?.source?.sha;
|
||||
const ref = plugin?.source?.ref;
|
||||
const releaseLocator = sha || ref;
|
||||
const releaseLocatorDescription = sha ? `commit "${sha}"` : `ref "${ref}"`;
|
||||
const pluginRoot = normalizeRepoRelativePath(plugin?.source?.path);
|
||||
|
||||
if (!releaseLocator) {
|
||||
errors.push('submission: plugins tagged with "canvas" must provide "Ref to review" and/or "Commit SHA to review"');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!repo) {
|
||||
return;
|
||||
}
|
||||
|
||||
let manifest = null;
|
||||
let manifestPath = null;
|
||||
let sawManifestApiError = false;
|
||||
|
||||
const manifestCandidates = EXTERNAL_PLUGIN_ROOT_MANIFEST_PATHS.map((relativePath) =>
|
||||
joinRepoPath(pluginRoot, relativePath),
|
||||
);
|
||||
|
||||
for (const candidatePath of manifestCandidates) {
|
||||
const response = await fetchGitHubFile(repo, candidatePath, releaseLocator, token);
|
||||
if (response.kind === "notFound") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (response.kind === "apiError") {
|
||||
sawManifestApiError = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (response.data?.type !== "file") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const decoded = decodeGitHubFileContent(response);
|
||||
if (!decoded) {
|
||||
errors.push(`submission: could not decode plugin manifest "${candidatePath}" at ${releaseLocatorDescription}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
manifest = JSON.parse(decoded);
|
||||
manifestPath = candidatePath;
|
||||
break;
|
||||
} catch (error) {
|
||||
errors.push(
|
||||
`submission: plugin manifest "${candidatePath}" at ${releaseLocatorDescription} is not valid JSON (${error.message})`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!manifest) {
|
||||
if (sawManifestApiError) {
|
||||
warnings.push(
|
||||
`submission: could not verify canvas plugin manifest in GitHub repository "${repo}" at ${releaseLocatorDescription}; a maintainer should re-run intake`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const expectedPaths = manifestCandidates.map((candidatePath) => `"${candidatePath}"`).join(", ");
|
||||
errors.push(
|
||||
`submission: plugins tagged with "canvas" must include a manifest at one of ${expectedPaths} in ${releaseLocatorDescription}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (manifest.logo !== EXTERNAL_CANVAS_PREVIEW_PATH) {
|
||||
errors.push(
|
||||
`submission: plugins tagged with "canvas" must set "logo" to "${EXTERNAL_CANVAS_PREVIEW_PATH}" in "${manifestPath}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const previewPath = joinRepoPath(pluginRoot, EXTERNAL_CANVAS_PREVIEW_PATH);
|
||||
const previewResponse = await fetchGitHubFile(repo, previewPath, releaseLocator, token);
|
||||
if (previewResponse.kind === "notFound") {
|
||||
errors.push(
|
||||
`submission: plugins tagged with "canvas" must include "${EXTERNAL_CANVAS_PREVIEW_PATH}" at ${releaseLocatorDescription}`,
|
||||
);
|
||||
} else if (previewResponse.kind === "apiError") {
|
||||
warnings.push(
|
||||
`submission: could not verify "${EXTERNAL_CANVAS_PREVIEW_PATH}" in GitHub repository "${repo}" at ${releaseLocatorDescription}; a maintainer should re-run intake`,
|
||||
);
|
||||
} else if (previewResponse.data?.type !== "file") {
|
||||
errors.push(
|
||||
`submission: "${EXTERNAL_CANVAS_PREVIEW_PATH}" must be a file in ${releaseLocatorDescription}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function parseExternalPluginIssueBody(body) {
|
||||
const sections = parseIssueFormSections(body);
|
||||
const errors = [];
|
||||
@@ -606,6 +760,7 @@ export async function evaluateExternalPluginIssue({ issue, token, runId, owner,
|
||||
const validationResult = validateExternalPlugin(parsed.plugin, 0, { policy: "publicSubmission" });
|
||||
errors.push(...validationResult.errors.map(toSubmissionError));
|
||||
warnings.push(...validationResult.warnings.map(toSubmissionError));
|
||||
const isCanvasPlugin = hasCanvasKeyword(parsed.plugin);
|
||||
|
||||
if (parsed.plugin?.name) {
|
||||
const matchingName = duplicateNames.find(
|
||||
@@ -620,6 +775,10 @@ export async function evaluateExternalPluginIssue({ issue, token, runId, owner,
|
||||
await validateRemoteRepository(parsed.plugin.source.repo, parsed.plugin.source, errors, warnings, token);
|
||||
}
|
||||
|
||||
if (isCanvasPlugin) {
|
||||
await validateCanvasPluginMetadata(parsed.plugin, errors, warnings, token);
|
||||
}
|
||||
|
||||
const dedupedErrors = [...new Set(errors)];
|
||||
const dedupedWarnings = [...new Set(warnings)];
|
||||
const valid = dedupedErrors.length === 0;
|
||||
@@ -687,6 +846,7 @@ export async function evaluateExternalPluginIssue({ issue, token, runId, owner,
|
||||
errors: dedupedErrors,
|
||||
warnings: dedupedWarnings,
|
||||
plugin: parsed.plugin,
|
||||
isCanvasPlugin,
|
||||
commentBody,
|
||||
commentMarker: marker,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user