mirror of
https://github.com/github/awesome-copilot.git
synced 2026-09-23 14:50:26 +00:00
chore: publish from main
This commit is contained in:
@@ -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({
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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") };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
Reference in New Issue
Block a user