chore: publish from main

This commit is contained in:
github-actions[bot]
2026-09-23 01:35:18 +00:00
parent 7f3e8ea133
commit e78cc11cb0
10 changed files with 237 additions and 20 deletions
@@ -767,6 +767,7 @@ jobs:
BASE_RESULT_JSON: ${{ needs.rerun-intake-parse.outputs.base-result }} BASE_RESULT_JSON: ${{ needs.rerun-intake-parse.outputs.base-result }}
BASE_VALID: ${{ needs.rerun-intake-parse.outputs.valid }} BASE_VALID: ${{ needs.rerun-intake-parse.outputs.valid }}
QUALITY_RESULT_JSON: ${{ needs.rerun-intake-quality-gates.outputs.quality-result }} QUALITY_RESULT_JSON: ${{ needs.rerun-intake-quality-gates.outputs.quality-result }}
QUALITY_LOG_URL: ${{ needs.rerun-intake-quality-gates.outputs.quality-log-url }}
QUALITY_JOB_RESULT: ${{ needs.rerun-intake-quality-gates.result }} QUALITY_JOB_RESULT: ${{ needs.rerun-intake-quality-gates.result }}
ISSUE_STATE: ${{ needs.rerun-intake-parse.outputs.issue-state }} ISSUE_STATE: ${{ needs.rerun-intake-parse.outputs.issue-state }}
ISSUE_LABELS: ${{ needs.rerun-intake-parse.outputs.issue-labels }} ISSUE_LABELS: ${{ needs.rerun-intake-parse.outputs.issue-labels }}
@@ -809,7 +810,14 @@ jobs:
}; };
} }
finalResult = intake.applyQualityGateResult(baseResult, qualityResult, context.runId, context.repo.owner, context.repo.repo); finalResult = intake.applyQualityGateResult(
baseResult,
qualityResult,
context.runId,
context.repo.owner,
context.repo.repo,
process.env.QUALITY_LOG_URL
);
} }
await intakeState.applyExternalPluginIntakeEvaluation({ await intakeState.applyExternalPluginIntakeEvaluation({
+9 -1
View File
@@ -94,6 +94,7 @@ jobs:
BASE_RESULT_JSON: ${{ needs.evaluate-submission.outputs.evaluation }} BASE_RESULT_JSON: ${{ needs.evaluate-submission.outputs.evaluation }}
BASE_VALID: ${{ needs.evaluate-submission.outputs.valid }} BASE_VALID: ${{ needs.evaluate-submission.outputs.valid }}
QUALITY_RESULT_JSON: ${{ needs.quality-gates.outputs.quality-result }} QUALITY_RESULT_JSON: ${{ needs.quality-gates.outputs.quality-result }}
QUALITY_LOG_URL: ${{ needs.quality-gates.outputs.quality-log-url }}
QUALITY_JOB_RESULT: ${{ needs.quality-gates.result }} QUALITY_JOB_RESULT: ${{ needs.quality-gates.result }}
ISSUE_STATE: ${{ needs.evaluate-submission.outputs.issue-state }} ISSUE_STATE: ${{ needs.evaluate-submission.outputs.issue-state }}
ISSUE_LABELS: ${{ needs.evaluate-submission.outputs.issue-labels }} ISSUE_LABELS: ${{ needs.evaluate-submission.outputs.issue-labels }}
@@ -136,7 +137,14 @@ jobs:
}; };
} }
finalResult = intake.applyQualityGateResult(baseResult, qualityResult, context.runId, context.repo.owner, context.repo.repo); finalResult = intake.applyQualityGateResult(
baseResult,
qualityResult,
context.runId,
context.repo.owner,
context.repo.repo,
process.env.QUALITY_LOG_URL
);
} }
await intakeState.applyExternalPluginIntakeEvaluation({ await intakeState.applyExternalPluginIntakeEvaluation({
@@ -143,6 +143,18 @@ jobs:
const intakeState = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake-state.mjs')).href); const intakeState = await import(pathToFileURL(path.join(process.env.GITHUB_WORKSPACE, 'eng', 'external-plugin-intake-state.mjs')).href);
const marker = '<!-- external-plugin-pr-quality -->'; const marker = '<!-- external-plugin-pr-quality -->';
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
owner: context.repo.owner,
repo: context.repo.repo,
run_id: workflowRun.id,
per_page: 100,
});
const qualityLogArtifact = artifacts.find((artifact) =>
artifact.name === 'external-plugin-pr-quality-logs' && !artifact.expired
);
const qualityLogUrl = qualityLogArtifact
? `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${workflowRun.id}/artifacts/${qualityLogArtifact.id}`
: '';
const detectJobResult = payload.detect_job_result; const detectJobResult = payload.detect_job_result;
const shouldRun = payload.should_run; const shouldRun = payload.should_run;
const changedCount = payload.changed_count; const changedCount = payload.changed_count;
@@ -345,6 +357,7 @@ jobs:
'', '',
`- **Changed entries detected:** ${changedCount}`, `- **Changed entries detected:** ${changedCount}`,
`- **Workflow state label:** \`${stateLabel}\``, `- **Workflow state label:** \`${stateLabel}\``,
qualityLogUrl ? `- **Full logs:** [Download quality gate artifact](${qualityLogUrl})` : '',
'- **Status legend:** ✅ pass · ⚠️ warning · 🛑 fail', '- **Status legend:** ✅ pass · ⚠️ warning · 🛑 fail',
'', '',
'### Per-plugin quality summary', '### Per-plugin quality summary',
@@ -117,13 +117,24 @@ jobs:
env: env:
CHANGED_PLUGINS_JSON: ${{ needs.detect-changed-plugins.outputs.changed-plugins }} CHANGED_PLUGINS_JSON: ${{ needs.detect-changed-plugins.outputs.changed-plugins }}
run: | run: |
result=$(node ./eng/external-plugin-pr-quality-gates.mjs --plugins-json "$CHANGED_PLUGINS_JSON") result=$(node ./eng/external-plugin-pr-quality-gates.mjs \
--plugins-json "$CHANGED_PLUGINS_JSON" \
--logs-directory "${RUNNER_TEMP}/external-plugin-pr-quality-logs")
{ {
echo 'quality-result<<EOF' echo 'quality-result<<EOF'
echo "$result" echo "$result"
echo 'EOF' echo 'EOF'
} >> "$GITHUB_OUTPUT" } >> "$GITHUB_OUTPUT"
- name: Upload full quality gate logs
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: external-plugin-pr-quality-logs
path: ${{ runner.temp }}/external-plugin-pr-quality-logs
if-no-files-found: warn
retention-days: 14
publish-quality-result: publish-quality-result:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [detect-changed-plugins, run-quality-gates] needs: [detect-changed-plugins, run-quality-gates]
@@ -11,6 +11,9 @@ on:
quality-result: quality-result:
description: JSON result for quality checks description: JSON result for quality checks
value: ${{ jobs.quality.outputs.quality-result }} value: ${{ jobs.quality.outputs.quality-result }}
quality-log-url:
description: URL for the full quality gate log artifact
value: ${{ jobs.quality.outputs.quality-log-url }}
permissions: permissions:
contents: read contents: read
@@ -20,6 +23,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
quality-result: ${{ steps.quality.outputs.quality-result }} quality-result: ${{ steps.quality.outputs.quality-result }}
quality-log-url: ${{ steps.upload-logs.outputs.artifact-url }}
steps: steps:
- name: Checkout main branch - name: Checkout main branch
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
@@ -44,9 +48,21 @@ jobs:
env: env:
PLUGIN_JSON: ${{ inputs.plugin-json }} PLUGIN_JSON: ${{ inputs.plugin-json }}
run: | run: |
result=$(node ./eng/external-plugin-quality-gates.mjs --plugin-json "$PLUGIN_JSON") result=$(node ./eng/external-plugin-quality-gates.mjs \
--plugin-json "$PLUGIN_JSON" \
--log-file "${RUNNER_TEMP}/external-plugin-quality-logs/quality-gates.log")
{ {
echo 'quality-result<<EOF' echo 'quality-result<<EOF'
echo "$result" echo "$result"
echo 'EOF' echo 'EOF'
} >> "$GITHUB_OUTPUT" } >> "$GITHUB_OUTPUT"
- name: Upload full quality gate logs
id: upload-logs
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: external-plugin-quality-logs
path: ${{ runner.temp }}/external-plugin-quality-logs
if-no-files-found: warn
retention-days: 14
+8 -4
View File
@@ -1242,7 +1242,7 @@ function getIntakeStateFromQualityResult(baseResult, qualityResult) {
return "ready-for-review"; return "ready-for-review";
} }
function buildMergedIntakeComment(baseResult, qualityResult, runId, owner, repo) { function buildMergedIntakeComment(baseResult, qualityResult, runId, owner, repo, qualityLogUrl) {
if (!baseResult.valid) { if (!baseResult.valid) {
return baseResult.commentBody; return baseResult.commentBody;
} }
@@ -1250,6 +1250,9 @@ function buildMergedIntakeComment(baseResult, qualityResult, runId, owner, repo)
const marker = baseResult.commentMarker ?? EXTERNAL_PLUGIN_INTAKE_COMMENT_MARKER; const marker = baseResult.commentMarker ?? EXTERNAL_PLUGIN_INTAKE_COMMENT_MARKER;
const qualitySection = buildQualityGatesCommentSection(qualityResult); const qualitySection = buildQualityGatesCommentSection(qualityResult);
const runLink = runId && owner && repo ? `_[View workflow run](https://github.com/${owner}/${repo}/actions/runs/${runId})_` : ""; const runLink = runId && owner && repo ? `_[View workflow run](https://github.com/${owner}/${repo}/actions/runs/${runId})_` : "";
const logLink = /^https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/actions\/runs\/\d+\/artifacts\/\d+$/.test(String(qualityLogUrl || ""))
? `_[Download full quality gate logs](${qualityLogUrl})_`
: "";
const hasSpecWarnings = String(qualityResult.spec_compliance_status || "") === "warning"; const hasSpecWarnings = String(qualityResult.spec_compliance_status || "") === "warning";
const intro = const intro =
@@ -1276,6 +1279,8 @@ function buildMergedIntakeComment(baseResult, qualityResult, runId, owner, repo)
"", "",
statusLine, statusLine,
"", "",
[runLink, logLink].filter(Boolean).join(" · "),
"",
`- **Plugin:** ${baseResult.plugin?.name ?? "unknown"}`, `- **Plugin:** ${baseResult.plugin?.name ?? "unknown"}`,
`- **Repository:** ${baseResult.plugin?.repository ?? "unknown"}`, `- **Repository:** ${baseResult.plugin?.repository ?? "unknown"}`,
baseResult.plugin?.source?.ref ? `- **Ref:** [\`${baseResult.plugin.source.ref.replaceAll('\`', '\\\`')}\`](https://github.com/${encodeRepoPath(baseResult.plugin.source.repo)}/tree/${encodeURIComponent(baseResult.plugin.source.ref).replaceAll("%2F", "/")})` : undefined, baseResult.plugin?.source?.ref ? `- **Ref:** [\`${baseResult.plugin.source.ref.replaceAll('\`', '\\\`')}\`](https://github.com/${encodeRepoPath(baseResult.plugin.source.repo)}/tree/${encodeURIComponent(baseResult.plugin.source.ref).replaceAll("%2F", "/")})` : undefined,
@@ -1295,11 +1300,10 @@ function buildMergedIntakeComment(baseResult, qualityResult, runId, owner, repo)
baseResult.warnings?.length baseResult.warnings?.length
? ["", "### Warnings", "", ...baseResult.warnings.map((warning) => `- ${warning}`)].join("\n") ? ["", "### Warnings", "", ...baseResult.warnings.map((warning) => `- ${warning}`)].join("\n")
: "", : "",
runLink ? `\n${runLink}` : "",
].join("\n"); ].join("\n");
} }
export function applyQualityGateResult(baseEvaluation, qualityGateResult, runId, owner, repo) { export function applyQualityGateResult(baseEvaluation, qualityGateResult, runId, owner, repo, qualityLogUrl) {
const baseResult = typeof baseEvaluation === "string" ? JSON.parse(baseEvaluation) : baseEvaluation; const baseResult = typeof baseEvaluation === "string" ? JSON.parse(baseEvaluation) : baseEvaluation;
const qualityResult = normalizeQualityGateResult( const qualityResult = normalizeQualityGateResult(
typeof qualityGateResult === "string" ? JSON.parse(qualityGateResult) : qualityGateResult, typeof qualityGateResult === "string" ? JSON.parse(qualityGateResult) : qualityGateResult,
@@ -1310,7 +1314,7 @@ export function applyQualityGateResult(baseEvaluation, qualityGateResult, runId,
...baseResult, ...baseResult,
qualityGates: qualityResult, qualityGates: qualityResult,
intakeState, intakeState,
commentBody: buildMergedIntakeComment(baseResult, qualityResult, runId, owner, repo), commentBody: buildMergedIntakeComment(baseResult, qualityResult, runId, owner, repo, qualityLogUrl),
}; };
} }
+29
View File
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
import http from "node:http"; import http from "node:http";
import { afterEach, test } from "node:test"; import { afterEach, test } from "node:test";
import { import {
applyQualityGateResult,
evaluateExternalPluginIssue, evaluateExternalPluginIssue,
PinnedAddressDispatcher, PinnedAddressDispatcher,
validateCanvasPluginMetadata, validateCanvasPluginMetadata,
@@ -15,6 +16,34 @@ const TREE_PLUGINS = "tree-plugins";
const TREE_UPGRADE_AGENT = "tree-upgrade-agent"; const TREE_UPGRADE_AGENT = "tree-upgrade-agent";
const TREE_EXTENSIONS = "tree-extensions"; const TREE_EXTENSIONS = "tree-extensions";
test("applyQualityGateResult links the full quality log artifact", () => {
const artifactUrl = "https://github.com/github/awesome-copilot/actions/runs/123/artifacts/456";
const result = applyQualityGateResult(
{
valid: true,
plugin: {
name: "example-plugin",
repository: "https://github.com/example/plugin",
source: { repo: "example/plugin", ref: "v1.0.0" },
},
warnings: [],
},
{
overall_status: "fail",
failure_class: "submitter_fixes",
vally_lint_status: "fail",
vally_lint_output: "...output truncated...",
},
123,
"github",
"awesome-copilot",
artifactUrl,
);
assert.match(result.commentBody, /Download full quality gate logs/);
assert.match(result.commentBody, new RegExp(artifactUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
});
function fileNode(content) { function fileNode(content) {
return { type: "file", content: Buffer.from(content, "utf8").toString("base64") }; return { type: "file", content: Buffer.from(content, "utf8").toString("base64") };
} }
+46 -6
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env node #!/usr/bin/env node
import fs from "fs";
import path from "path";
import { runExternalPluginQualityGates } from "./external-plugin-quality-gates.mjs"; import { runExternalPluginQualityGates } from "./external-plugin-quality-gates.mjs";
import { validateExternalPlugin } from "./external-plugin-validation.mjs"; import { validateExternalPlugin } from "./external-plugin-validation.mjs";
@@ -86,16 +88,52 @@ function createValidationFailureQuality(errors) {
}; };
} }
export async function runExternalPluginPrQualityGates(plugins) { function createLogFilePath(logsDirectory, pluginName, index) {
if (!logsDirectory) {
return undefined;
}
const safeName = String(pluginName || "unknown")
.toLowerCase()
.replace(/[^a-z0-9.-]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 64) || "unknown";
return path.join(logsDirectory, `${String(index + 1).padStart(2, "0")}-${safeName}.log`);
}
function writeValidationFailureLog(logFile, plugin, quality) {
if (!logFile) {
return;
}
fs.mkdirSync(path.dirname(logFile), { recursive: true });
fs.writeFileSync(
logFile,
[
`External plugin quality gate log: ${plugin?.name || "unknown"}`,
"",
"External plugin entry validation",
quality.vally_lint_output || "No output captured.",
"",
].join("\n"),
);
}
export async function runExternalPluginPrQualityGates(plugins, { logsDirectory } = {}) {
if (!Array.isArray(plugins)) { if (!Array.isArray(plugins)) {
throw new Error("plugins must be an array"); throw new Error("plugins must be an array");
} }
const checkedPlugins = await Promise.all(plugins.map(async (plugin) => { const checkedPlugins = await Promise.all(plugins.map(async (plugin, index) => {
const validation = validateExternalPlugin(plugin, "changed-plugin", { policy: "marketplace" }); const validation = validateExternalPlugin(plugin, "changed-plugin", { policy: "marketplace" });
const quality = validation.errors.length > 0 const logFile = createLogFilePath(logsDirectory, plugin?.name, index);
? createValidationFailureQuality(validation.errors) let quality;
: await runExternalPluginQualityGates(plugin); if (validation.errors.length > 0) {
quality = createValidationFailureQuality(validation.errors);
writeValidationFailureLog(logFile, plugin, quality);
} else {
quality = await runExternalPluginQualityGates(plugin, { logFile });
}
return { return {
name: plugin?.name ?? "unknown", name: plugin?.name ?? "unknown",
source: plugin?.source ?? {}, source: plugin?.source ?? {},
@@ -143,6 +181,8 @@ if (import.meta.url === `file://${process.argv[1]}`) {
} }
const plugins = JSON.parse(args["plugins-json"]); const plugins = JSON.parse(args["plugins-json"]);
const result = await runExternalPluginPrQualityGates(plugins); const result = await runExternalPluginPrQualityGates(plugins, {
logsDirectory: args["logs-directory"],
});
process.stdout.write(`${JSON.stringify(result)}\n`); process.stdout.write(`${JSON.stringify(result)}\n`);
} }
+47 -5
View File
@@ -47,8 +47,12 @@ const INFRA_ERROR_PATTERNS = [
/etimedout/, /etimedout/,
]; ];
function normalizeOutput(value) {
return String(value ?? "").replace(/\x1b\[[0-9;]*m/g, "").trim();
}
function truncateOutput(value) { function truncateOutput(value) {
const normalized = String(value ?? "").replace(/\x1b\[[0-9;]*m/g, "").trim(); const normalized = normalizeOutput(value);
if (normalized.length <= MAX_OUTPUT_LENGTH) { if (normalized.length <= MAX_OUTPUT_LENGTH) {
return normalized; return normalized;
} }
@@ -366,18 +370,46 @@ async function runVallyLintGate(pluginRoot) {
} }
} }
const logOutput = normalizeOutput(combinedOutput);
return { return {
status: anyFailure ? "fail" : "pass", status: anyFailure ? "fail" : "pass",
output: truncateOutput(combinedOutput), output: truncateOutput(logOutput),
logOutput,
}; };
} catch (error) { } catch (error) {
const logOutput = normalizeOutput(error.stack || error.message);
return { return {
status: "infra_error", status: "infra_error",
output: truncateOutput(error.message), output: truncateOutput(logOutput),
logOutput,
}; };
} }
} }
export function formatQualityGateLog(plugin, result, { vallyLintOutput } = {}) {
const pluginName = String(plugin?.name || "unknown");
const sections = [
`External plugin quality gate log: ${pluginName}`,
"",
"Summary",
String(result.summary || "No summary provided."),
];
const gateOutputs = [
["Spec compliance", result.spec_compliance_output],
["Vally lint", vallyLintOutput ?? result.vally_lint_output],
["Install smoke test", result.smoke_output],
["Version match", result.version_match_output],
["Ref/SHA consistency", result.ref_sha_consistency_output],
["Canvas structure", result.canvas_structure_output],
];
for (const [heading, output] of gateOutputs) {
sections.push("", heading, normalizeOutput(output) || "No output captured.");
}
return `${sections.join("\n")}\n`;
}
function buildEphemeralMarketplace(workDir, plugin) { function buildEphemeralMarketplace(workDir, plugin) {
const marketplaceDir = path.join(workDir, "marketplace"); const marketplaceDir = path.join(workDir, "marketplace");
ensureDirectory(marketplaceDir); ensureDirectory(marketplaceDir);
@@ -952,8 +984,9 @@ function toFailureClass(overallStatus) {
return "none"; return "none";
} }
export async function runExternalPluginQualityGates(plugin) { export async function runExternalPluginQualityGates(plugin, { logFile } = {}) {
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "external-plugin-quality-")); const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "external-plugin-quality-"));
let vallyLintLogOutput = "";
const result = { const result = {
overall_status: "not_run", overall_status: "not_run",
vally_lint_status: "not_run", vally_lint_status: "not_run",
@@ -1014,6 +1047,7 @@ export async function runExternalPluginQualityGates(plugin) {
const vallyResult = await runVallyLintGate(pluginRoot); const vallyResult = await runVallyLintGate(pluginRoot);
result.vally_lint_status = vallyResult.status; result.vally_lint_status = vallyResult.status;
result.vally_lint_output = vallyResult.output; result.vally_lint_output = vallyResult.output;
vallyLintLogOutput = vallyResult.logOutput;
const smokeResult = runInstallSmokeGate(workDir, plugin); const smokeResult = runInstallSmokeGate(workDir, plugin);
result.smoke_status = smokeResult.status; result.smoke_status = smokeResult.status;
@@ -1043,8 +1077,16 @@ export async function runExternalPluginQualityGates(plugin) {
result.failure_class = "infra"; result.failure_class = "infra";
result.summary = truncateOutput(error.message); result.summary = truncateOutput(error.message);
result.vally_lint_output = truncateOutput(error.stack || error.message); result.vally_lint_output = truncateOutput(error.stack || error.message);
vallyLintLogOutput = normalizeOutput(error.stack || error.message);
return result; return result;
} finally { } finally {
if (logFile) {
fs.mkdirSync(path.dirname(logFile), { recursive: true });
fs.writeFileSync(
logFile,
formatQualityGateLog(plugin, result, { vallyLintOutput: vallyLintLogOutput }),
);
}
fs.rmSync(workDir, { recursive: true, force: true }); fs.rmSync(workDir, { recursive: true, force: true });
} }
} }
@@ -1071,6 +1113,6 @@ if (import.meta.url === `file://${process.argv[1]}`) {
} }
const plugin = JSON.parse(args["plugin-json"]); const plugin = JSON.parse(args["plugin-json"]);
const result = await runExternalPluginQualityGates(plugin); const result = await runExternalPluginQualityGates(plugin, { logFile: args["log-file"] });
process.stdout.write(`${JSON.stringify(result)}\n`); process.stdout.write(`${JSON.stringify(result)}\n`);
} }
+47 -1
View File
@@ -4,7 +4,13 @@ import os from "os";
import path from "path"; import path from "path";
import { spawnSync } from "child_process"; import { spawnSync } from "child_process";
import { after, test } from "node:test"; import { after, test } from "node:test";
import { runCanvasStructureGate, runRefShaConsistencyGate, runVersionMatchGate } from "./external-plugin-quality-gates.mjs"; import {
formatQualityGateLog,
runCanvasStructureGate,
runRefShaConsistencyGate,
runVersionMatchGate,
} from "./external-plugin-quality-gates.mjs";
import { runExternalPluginPrQualityGates } from "./external-plugin-pr-quality-gates.mjs";
const tempDirs = []; const tempDirs = [];
@@ -38,6 +44,46 @@ function commitAll(repoDir, message) {
return runGit(repoDir, "rev-parse", "HEAD"); return runGit(repoDir, "rev-parse", "HEAD");
} }
test("formatQualityGateLog preserves full vally output for artifacts", () => {
const fullVallyOutput = `lint failure\n${"x".repeat(13000)}`;
const log = formatQualityGateLog(
{ name: "example-plugin" },
{
summary: "- vally lint: fail",
spec_compliance_output: "spec output",
vally_lint_output: "truncated output",
smoke_output: "smoke output",
version_match_output: "version output",
ref_sha_consistency_output: "ref output",
canvas_structure_output: "canvas output",
},
{ vallyLintOutput: fullVallyOutput },
);
assert.match(log, /External plugin quality gate log: example-plugin/);
assert.match(log, /lint failure/);
assert.equal(log.includes("x".repeat(13000)), true);
assert.equal(log.includes("truncated output"), false);
});
test("runExternalPluginPrQualityGates writes logs for validation failures", async () => {
const logsDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "external-plugin-pr-logs-"));
tempDirs.push(logsDirectory);
const result = await runExternalPluginPrQualityGates(
[{ name: "Invalid Plugin", source: {} }],
{ logsDirectory },
);
assert.equal(result.overall_status, "fail");
const logFiles = fs.readdirSync(logsDirectory);
assert.deepEqual(logFiles, ["01-invalid-plugin.log"]);
assert.match(
fs.readFileSync(path.join(logsDirectory, logFiles[0]), "utf8"),
/External plugin entry validation/,
);
});
test("runCanvasStructureGate passes when a named extension exists", () => { test("runCanvasStructureGate passes when a named extension exists", () => {
const repoDir = createTempRepo(); const repoDir = createTempRepo();
fs.mkdirSync(path.join(repoDir, "com.github.copilot", "extensions", "canvas-plugin"), { recursive: true }); fs.mkdirSync(path.join(repoDir, "com.github.copilot", "extensions", "canvas-plugin"), { recursive: true });