Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 41 additions & 6 deletions .github/workflows/external-plugin-pr-quality-gates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,34 @@ jobs:
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
const escapeMarkdownTableCell = (value) =>
String(value ?? '')
.replace(/\r\n?|\n/g, '\n')
.split('\n')
.map((line) =>
Array.from(line, (character) =>
/^[A-Za-z0-9 .-]$/.test(character)
? character
: `&#${character.codePointAt(0)};`
).join('')
)
.join('<br>');
const normalizeGitHubUrl = (value) => {
const raw = String(value || '').trim();
if (!raw) {
return '';
}

try {
const parsed = new URL(raw);
if (parsed.protocol !== 'https:' || parsed.hostname !== 'github.com') {
return '';
}
return parsed.toString();
} catch {
return '';
}
};
const TRUNCATED_OUTPUT_MARKER = '\n...output truncated...';
const truncateGateOutput = (rawOutput) => {
const normalized = escapeHtml(String(rawOutput || '').trim());
Expand Down Expand Up @@ -243,12 +271,17 @@ jobs:

const rows = checkedPlugins.length > 0
? checkedPlugins.map((entry) => {
const name = String(entry?.name || 'unknown');
const name = escapeMarkdownTableCell(entry?.name || 'unknown');
const quality = entry?.quality || {};
const sourceUrl = String(entry?.source_tree_url || '');
const locator = String(entry?.source?.sha || entry?.source?.ref || 'repository');
const sourceCell = sourceUrl ? `[${locator}](${sourceUrl})` : locator;
return `| ${name} | ${quality.vally_lint_status || 'not_run'} | ${quality.smoke_status || 'not_run'} | ${quality.version_match_status || 'not_run'} | ${quality.canvas_structure_status || 'not_run'} | ${quality.overall_status || 'not_run'} | ${sourceCell} |`;
const sourceUrl = normalizeGitHubUrl(entry?.source_tree_url || '');
const locator = escapeMarkdownTableCell(entry?.source?.sha || entry?.source?.ref || 'repository');
const sourceCell = sourceUrl ? `[${locator}](<${sourceUrl}>)` : locator;
const vallyLintStatus = escapeMarkdownTableCell(quality.vally_lint_status || 'not_run');
const smokeStatus = escapeMarkdownTableCell(quality.smoke_status || 'not_run');
const versionMatchStatus = escapeMarkdownTableCell(quality.version_match_status || 'not_run');
const canvasStructureStatus = escapeMarkdownTableCell(quality.canvas_structure_status || 'not_run');
const overallStatus = escapeMarkdownTableCell(quality.overall_status || 'not_run');
return `| ${name} | ${vallyLintStatus} | ${smokeStatus} | ${versionMatchStatus} | ${canvasStructureStatus} | ${overallStatus} | ${sourceCell} |`;
})
: ['| _none_ | not_run | not_run | not_run | not_run | not_run | _n/a_ |'];
const failureDetails = checkedPlugins.flatMap((entry) => {
Expand Down Expand Up @@ -296,7 +329,9 @@ jobs:
'',
]
: []),
String(qualityResult.summary || '').trim() || '_No summary provided._',
String(qualityResult.summary || '').trim()
? `<pre><code>${escapeHtml(String(qualityResult.summary).trim())}</code></pre>`
: '_No summary provided._',
].join('\n');

await intakeState.upsertExternalPluginIntakeComment({
Expand Down
23 changes: 22 additions & 1 deletion eng/external-plugin-pr-quality-gates.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env node

import { runExternalPluginQualityGates } from "./external-plugin-quality-gates.mjs";
import { validateExternalPlugin } from "./external-plugin-validation.mjs";

function normalizePluginPath(pluginPath) {
if (!pluginPath || pluginPath === "/") {
Expand Down Expand Up @@ -66,13 +67,33 @@ function aggregateResultStatus(pluginResults) {
};
}

function createValidationFailureQuality(errors) {
const output = errors.map((error) => `- ${error}`).join("\n");
return {
overall_status: "fail",
vally_lint_status: "fail",
smoke_status: "not_run",
version_match_status: "not_run",
canvas_structure_status: "not_run",
failure_class: "submitter_fixes",
summary: "Plugin entry failed external.json validation. Fix the listed errors and re-run quality checks.",
vally_lint_output: output,
smoke_output: "Install smoke test skipped due to external.json validation errors.",
version_match_output: "Version match skipped due to external.json validation errors.",
canvas_structure_output: "Canvas structure check skipped due to external.json validation errors.",
};
}

export async function runExternalPluginPrQualityGates(plugins) {
if (!Array.isArray(plugins)) {
throw new Error("plugins must be an array");
}

const checkedPlugins = await Promise.all(plugins.map(async (plugin) => {
const quality = await runExternalPluginQualityGates(plugin);
const validation = validateExternalPlugin(plugin, "changed-plugin", { policy: "marketplace" });
const quality = validation.errors.length > 0
? createValidationFailureQuality(validation.errors)
: await runExternalPluginQualityGates(plugin);
return {
name: plugin?.name ?? "unknown",
source: plugin?.source ?? {},
Expand Down
16 changes: 12 additions & 4 deletions eng/external-plugin-validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,20 @@ function validatePluginName(name, prefix, errors) {
return;
}

if (name.length > 50) {
errors.push(`${prefix}: "name" must be 50 characters or fewer`);
if (name.length > 64) {
errors.push(`${prefix}: "name" must be 64 characters or fewer`);
}

if (!/^[a-z0-9-]+$/.test(name)) {
errors.push(`${prefix}: "name" must contain only lowercase letters, numbers, and hyphens`);
if (!/^[a-z0-9.-]+$/.test(name)) {
errors.push(`${prefix}: "name" must contain only lowercase letters, numbers, hyphens, and periods`);
}

if (!/^[a-z0-9].*[a-z0-9]$/.test(name) && !/^[a-z0-9]$/.test(name)) {
errors.push(`${prefix}: "name" must start and end with a lowercase letter or number`);
}

if (name.includes("--") || name.includes("..")) {
errors.push(`${prefix}: "name" must not contain consecutive hyphens or periods`);
}
}

Expand Down
Loading