Skip to content
Draft
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
57 changes: 47 additions & 10 deletions .github/scripts/js/e2e/report/cluster-report.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,27 @@ const {
*/

const workflowStages = [
{ name: "bootstrap", displayName: "Bootstrap cluster", needsJobId: "bootstrap" },
{ name: "configure-sdn", displayName: "Configure SDN", needsJobId: "configure-sdn" },
{ name: "storage-setup", displayName: "Configure storage", needsJobId: "configure-storage" },
{ name: "virtualization-setup", displayName: "Configure Virtualization", needsJobId: "configure-virtualization" },
{ name: "e2e-test", displayName: "E2E test", needsJobId: "e2e-test" },
{
name: "bootstrap",
displayName: "Bootstrap cluster",
needsJobId: "bootstrap",
},
{
name: "configure-sdn",
displayName: "Configure SDN",
needsJobId: "configure-sdn",
},
{
name: "storage-setup",
displayName: "Configure storage",
needsJobId: "configure-storage",
},
{
name: "virtualization-setup",
displayName: "Configure Virtualization",
needsJobId: "configure-virtualization",
},
{ name: "e2e-test", displayName: "E2E test", needsJobId: "e2e-test" },
];

function readClusterReportConfigFromEnv(env = process.env) {
Expand All @@ -87,7 +103,11 @@ function readClusterReportConfigFromEnv(env = process.env) {
};
}

const requiredClusterReportConfigKeys = ["storageType", "reportsDir", "reportFile"];
const requiredClusterReportConfigKeys = [
"storageType",
"reportsDir",
"reportFile",
];

function requireClusterReportConfig(config) {
for (const key of requiredClusterReportConfigKeys) {
Expand Down Expand Up @@ -128,7 +148,9 @@ async function listWorkflowRunJobs(github, context) {
}

function findWorkflowJob(jobs, pipelineJobName, jobName) {
const nestedJobName = pipelineJobName ? `${pipelineJobName} / ${jobName}` : "";
const nestedJobName = pipelineJobName
? `${pipelineJobName} / ${jobName}`
: "";

return (
jobs.find((job) => job.name === nestedJobName) ||
Expand All @@ -147,7 +169,8 @@ function readStageResultsFromEnv(env = process.env) {

const stageResults = {};
for (const { name, needsJobId } of workflowStages) {
stageResults[name] = String((needs[needsJobId] || {}).result || "").trim() || "skipped";
stageResults[name] =
String((needs[needsJobId] || {}).result || "").trim() || "skipped";
}
return stageResults;
}
Expand All @@ -161,7 +184,9 @@ async function readStageJobUrlsFromApi(github, context, config, core) {
if (job) {
stageJobUrls[name] = job.html_url || "";
} else {
core.warning(`Unable to find workflow job "${displayName}" for E2E report`);
core.warning(
`Unable to find workflow job "${displayName}" for E2E report`
);
}
}

Expand All @@ -178,6 +203,8 @@ async function readStageJobUrlsFromApi(github, context, config, core) {
* metrics: ReturnType<typeof zeroMetrics>,
* failedTests: string[],
* failedTestDetails: Array<{name: string, reason: string}>,
* specTimings: Array<Record<string, any>>,
* suiteTotalMs: number,
* startedAt: null,
* source: string,
* }} Empty parsed-report payload.
Expand All @@ -187,6 +214,8 @@ function emptyParsedReport(source) {
metrics: zeroMetrics(),
failedTests: [],
failedTestDetails: [],
specTimings: [],
suiteTotalMs: 0,
startedAt: null,
source,
};
Expand Down Expand Up @@ -217,6 +246,8 @@ const ginkgoOutputSource = {
* metrics: ReturnType<typeof zeroMetrics>,
* failedTests: string[],
* failedTestDetails: Array<{name: string, reason: string}>,
* specTimings: Array<Record<string, any>>,
* suiteTotalMs: number,
* startedAt: string|null,
* }} parse Parser function for the source content.
* @property {function(string): RegExp} pattern Builds the file-name regex for the source.
Expand Down Expand Up @@ -252,6 +283,8 @@ function findGinkgoSource(config, source) {
* metrics: ReturnType<typeof zeroMetrics>,
* failedTests: string[],
* failedTestDetails: Array<{name: string, reason: string}>,
* specTimings: Array<Record<string, any>>,
* suiteTotalMs: number,
* startedAt: string|null,
* source: string,
* }} Parsed report payload with a source tag.
Expand Down Expand Up @@ -320,6 +353,8 @@ function buildReportPayload({
metrics: parsedReport.metrics,
failedTests: parsedReport.failedTests,
failedTestDetails: parsedReport.failedTestDetails,
specTimings: parsedReport.specTimings || [],
suiteTotalMs: parsedReport.suiteTotalMs || 0,
sourceReport: sourcePath,
reportSource: parsedReport.source,
};
Expand Down Expand Up @@ -391,7 +426,9 @@ async function buildClusterReport({ core, context, github, config } = {}) {
? null
: findGinkgoSource(resolvedConfig, ginkgoOutputSource);
const sourcePath = rawReportPath || outputPath;
const sourceDescriptor = rawReportPath ? ginkgoJsonSource : ginkgoOutputSource;
const sourceDescriptor = rawReportPath
? ginkgoJsonSource
: ginkgoOutputSource;

if (!rawReportPath) {
core.warning(
Expand Down
92 changes: 63 additions & 29 deletions .github/scripts/js/e2e/report/cluster-report.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,12 @@ function createContext() {
* @returns {Record<string, any>} Mocked GitHub client.
*/
function createGithub(jobNames) {
const jobs = jobNames.map(
(name, index) => ({
name,
html_url: `https://github.com/test/repo/actions/runs/12345/job/${
index + 1
}`,
})
);
const jobs = jobNames.map((name, index) => ({
name,
html_url: `https://github.com/test/repo/actions/runs/12345/job/${
index + 1
}`,
}));

return {
rest: {
Expand Down Expand Up @@ -238,11 +236,11 @@ describe("cluster-report", () => {
process.env.REPORTS_DIR = tempDir;
process.env.REPORT_FILE = reportFile;
process.env.NEEDS_CONTEXT = JSON.stringify({
"bootstrap": { result: "success" },
"configure-sdn": { result: "success" },
"configure-storage": { result: "success" },
bootstrap: { result: "success" },
"configure-sdn": { result: "success" },
"configure-storage": { result: "success" },
"configure-virtualization": { result: "success" },
"e2e-test": { result: "success" },
"e2e-test": { result: "success" },
});

expect(readClusterReportConfigFromEnv()).toMatchObject({
Expand Down Expand Up @@ -275,11 +273,11 @@ describe("cluster-report", () => {
process.env.REPORTS_DIR = tempDir;
process.env.REPORT_FILE = reportFile;
process.env.NEEDS_CONTEXT = JSON.stringify({
"bootstrap": { result: "success" },
"configure-sdn": { result: "failure" },
"configure-storage": { result: "skipped" },
bootstrap: { result: "success" },
"configure-sdn": { result: "failure" },
"configure-storage": { result: "skipped" },
"configure-virtualization": { result: "skipped" },
"e2e-test": { result: "skipped" },
"e2e-test": { result: "skipped" },
});

const report = await buildClusterReport({
Expand All @@ -305,11 +303,11 @@ describe("cluster-report", () => {
process.env.REPORTS_DIR = tempDir;
process.env.REPORT_FILE = reportFile;
process.env.NEEDS_CONTEXT = JSON.stringify({
"bootstrap": { result: "success" },
"configure-sdn": { result: "failure" },
"configure-storage": { result: "skipped" },
bootstrap: { result: "success" },
"configure-sdn": { result: "failure" },
"configure-storage": { result: "skipped" },
"configure-virtualization": { result: "skipped" },
"e2e-test": { result: "skipped" },
"e2e-test": { result: "skipped" },
});

const report = await buildClusterReport({
Expand Down Expand Up @@ -341,11 +339,11 @@ describe("cluster-report", () => {
process.env.REPORTS_DIR = tempDir;
process.env.REPORT_FILE = reportFile;
process.env.NEEDS_CONTEXT = JSON.stringify({
"bootstrap": { result: "success" },
"configure-sdn": { result: "success" },
"configure-storage": { result: "success" },
bootstrap: { result: "success" },
"configure-sdn": { result: "success" },
"configure-storage": { result: "success" },
"configure-virtualization": { result: "success" },
"e2e-test": { result: "success" },
"e2e-test": { result: "success" },
});

const report = await buildClusterReport({
Expand Down Expand Up @@ -454,11 +452,43 @@ describe("cluster-report", () => {
reason: "timed out waiting for VM to become ready",
},
]);
expect(report.suiteTotalMs).toBe(1800000);
expect(report.specTimings).toEqual([
{
name: "passes",
group: "Suite",
state: "passed",
runtimeMs: 60000,
labels: [],
},
{
name: "fails & burns",
group: "Suite",
state: "failed",
runtimeMs: 60000,
labels: ["Slow"],
},
{
name: "errors <loudly>",
group: "Other",
state: "errors",
runtimeMs: 60000,
labels: [],
},
{
name: "skipped",
group: "skipped",
state: "skipped",
runtimeMs: 60000,
labels: [],
},
]);
expect(report.reportSource).toBe("ginkgo-json");
expect(report.sourceReport).toBe(rawReportPath);
expect(JSON.parse(fs.readFileSync(reportFile, "utf8")).reportKind).toBe(
"tests"
);
const persistedReport = JSON.parse(fs.readFileSync(reportFile, "utf8"));
expect(persistedReport.reportKind).toBe("tests");
expect(persistedReport.specTimings).toEqual(report.specTimings);
expect(persistedReport.suiteTotalMs).toBe(1800000);
expect(core.setOutput).toHaveBeenCalledWith("report_file", reportFile);
expect(core.setOutput).toHaveBeenCalledWith("report_kind", "tests");
expect(core.setOutput).toHaveBeenCalledWith("status", "failure");
Expand Down Expand Up @@ -590,7 +620,7 @@ describe("cluster-report", () => {
"/home/runner/work/virtualization/virtualization/test/e2e/e2e_test.go:44",
"PASS: all 94 specs have precheck labels",
" STEP: Ensuring 12 precreated CVIs are available @ 05/14/26 13:57:36.142",
" CVI \"v12n-e2e-testdata-iso\" exists but not ready (phase: Pending), waiting...",
' CVI "v12n-e2e-testdata-iso" exists but not ready (phase: Pending), waiting...',
" [FAILED] in [SynchronizedBeforeSuite] - /home/runner/work/.../until.go:207 @ 05/14/26 14:02:37.61",
"[SynchronizedBeforeSuite] [FAILED] [307.964 seconds]",
"[SynchronizedBeforeSuite]",
Expand Down Expand Up @@ -636,7 +666,9 @@ describe("cluster-report", () => {
);
// The plain "[SynchronizedBeforeSuite]" header that follows the
// "[FAILED] [307.964 seconds]" line must not leak into the reason.
expect(detail.reason.split("\n")[0]).not.toBe("[SynchronizedBeforeSuite]");
expect(detail.reason.split("\n")[0]).not.toBe(
"[SynchronizedBeforeSuite]"
);
}));

test("fails when multiple matching Ginkgo JSON reports exist", async () =>
Expand Down Expand Up @@ -828,6 +860,8 @@ describe("cluster-report", () => {
successRate: 92.78,
});
expect(parsed.startedAt).toBe("2026-04-28T03:11:27.708387575Z");
expect(parsed.specTimings).toHaveLength(131);
expect(parsed.suiteTotalMs).toBe(1800000);
expect(parsed.failedTests).toHaveLength(7);
expect(parsed.failedTests).toContain(
"[It] VirtualMachineOperationRestore restores a virtual machine from a snapshot BestEffort restore mode; automatic restart approval mode; manual run policy [Slow]"
Expand Down
31 changes: 24 additions & 7 deletions .github/scripts/js/e2e/report/messenger-report.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const fs = require("fs");

const { listMatchingFiles } = require("./shared/fs-utils");
const { REPORT_FILE_PATTERN } = require("./shared/report-model");
const { renderClusterCharts } = require("./messenger/charts/chart-renderer");
const { makeThreadedReportInLoop } = require("./messenger/loop-client");
const { readMessengerConfigFromEnv } = require("./messenger/config");
const {
Expand Down Expand Up @@ -103,12 +104,19 @@ function readReports(reportsDir, configuredClusters, core) {
* @param {MessengerMessagesParams} params Message rendering inputs.
* @returns {{
* message: string,
* threadMessages: string[]
* threadMessages: Array<{message: string, files: Array<Record<string, any>>}>
* }} Rendered markdown payloads.
*/
function buildMessengerMessages({ reportsDir, configuredClusters, core }) {
async function buildMessengerMessages({
reportsDir,
configuredClusters,
core,
}) {
const orderedReports = readReports(reportsDir, configuredClusters, core);
const threadMessages = buildThreadMessages(orderedReports);
const threadMessages = await buildThreadMessages(orderedReports, {
renderClusterCharts,
core,
});
return {
message: buildMainMessage(orderedReports),
threadMessages,
Expand All @@ -122,26 +130,35 @@ function buildMessengerMessages({ reportsDir, configuredClusters, core }) {
* @param {RenderMessengerReportParams} params GitHub script dependencies.
* @returns {Promise<{
* message: string,
* threadMessages: string[]
* threadMessages: Array<{message: string, files: Array<Record<string, any>>}>
* }>} Rendered messages.
*/
async function renderMessengerReport({ core, reportsDir }) {
const config = readMessengerConfigFromEnv();
const { message, threadMessages } = buildMessengerMessages({
const { message, threadMessages } = await buildMessengerMessages({
reportsDir: reportsDir || config.reportsDir,
configuredClusters: config.configuredClusters,
core,
});

core.info(message);
core.setOutput("message", message);
core.setOutput("thread_messages", JSON.stringify(threadMessages));
core.setOutput(
"thread_messages",
JSON.stringify(threadMessages.map((threadMessage) => threadMessage.message))
);

if (config.loop) {
try {
await makeThreadedReportInLoop({ message, threadMessages, loop: config.loop }, core);
await makeThreadedReportInLoop(
{ message, threadMessages, loop: config.loop },
core
);
} catch (error) {
core.warning(`Unable to deliver report to Loop API: ${error.message}`);
if (config.loop.strictDelivery) {
throw error;
}
}
}

Expand Down
Loading
Loading