919 lines
30 KiB
JavaScript
919 lines
30 KiB
JavaScript
const REQUIRED_RELEASE_GATES = [
|
|
{
|
|
id: "diff-check",
|
|
label: "Git diff check",
|
|
command: "git diff --check",
|
|
},
|
|
{
|
|
id: "vendor-sync",
|
|
label: "Vendor sync guard",
|
|
command: "wasm-port/tools/verify_vendor_sync.sh",
|
|
expectedOutput: "vendor sync up to date",
|
|
},
|
|
{
|
|
id: "standalone-cnc-semantics",
|
|
label: "Standalone CNC semantics guard",
|
|
command: "wasm-port/tools/verify_no_standalone_cnc_semantics.sh",
|
|
expectedOutput: "standalone CNC semantics guard complete",
|
|
},
|
|
{
|
|
id: "interp-wasm",
|
|
label: "Interpreter WASM Node smoke",
|
|
command: "SKIP_INTERP_BUILD=1 wasm-port/tests/wasm/node/verify_interp_wasm.sh",
|
|
expectedOutput: "interp_wasm_node_smoke=ok",
|
|
},
|
|
{
|
|
id: "sim-config-inventory",
|
|
label: "Sim config inventory WASM Node smoke",
|
|
command: "SKIP_INTERP_BUILD=1 wasm-port/tests/wasm/node/verify_sim_configs_inventory_wasm.sh",
|
|
expectedOutput: "sim_configs_wasm_node_inventory_unexpected_fail=0",
|
|
},
|
|
{
|
|
id: "ini-panel-browser",
|
|
label: "INI panel browser smoke",
|
|
command: "SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_ini_panel_browser.sh",
|
|
expectedOutput: "browser_ini_shell_integration_workflow_smoke=ok",
|
|
},
|
|
{
|
|
id: "release-artifact-url-browser",
|
|
label: "Release artifact URL browser workflow smoke",
|
|
command: "SKIP_INI_BUILD=1 SKIP_INTERP_BUILD=1 wasm-port/tests/browser/verify_release_artifact_url_workflow_browser.sh",
|
|
expectedOutput: "browser_release_artifact_url_workflow_smoke=ok",
|
|
},
|
|
{
|
|
id: "ui-node-smokes",
|
|
label: "UI Node smokes",
|
|
command: "wasm-port/tests/ui/node/verify_ui_node_smokes.sh",
|
|
expectedOutput: "ui_node_smokes=ok",
|
|
},
|
|
{
|
|
id: "host-smokes",
|
|
label: "Host aggregate smoke",
|
|
command: "wasm-port/tests/host/verify_host_smokes.sh",
|
|
expectedOutput: "host_wasm_opfs_browser_smokes=ok",
|
|
},
|
|
{
|
|
id: "project-release-gate",
|
|
label: "Project release gate",
|
|
command: "wasm-port/tests/host/verify_project_release_gate.sh",
|
|
expectedOutput: "project_release_gate=ok",
|
|
},
|
|
];
|
|
|
|
const DEFAULT_SIM_CONFIG_INVENTORY_BASELINE = {
|
|
executed: 28,
|
|
passed: 28,
|
|
skipped: 131,
|
|
unexpectedFail: 0,
|
|
};
|
|
|
|
const BLOCKED_RUNTIME_FAMILIES = [
|
|
"L4-USER-M-PROCESS",
|
|
"L4-TOOL-DB",
|
|
"L4-PYTHON-REMAP",
|
|
];
|
|
|
|
const PROJECT_RELEASE_READINESS_ARTIFACT_API = "project-release-readiness-report";
|
|
const PROJECT_RELEASE_READINESS_ARTIFACT_VERSION = 1;
|
|
|
|
function outputContains(observedOutputs, expectedOutput) {
|
|
return observedOutputs.some((output) => String(output).includes(expectedOutput));
|
|
}
|
|
|
|
function readGatePassed({ gate, gateResults, observedOutputs }) {
|
|
if (gateResults?.[gate.id] === true) {
|
|
return true;
|
|
}
|
|
if (gate.expectedOutput && outputContains(observedOutputs, gate.expectedOutput)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function objectOrEmpty(value) {
|
|
return value && typeof value === "object" ? value : {};
|
|
}
|
|
|
|
function arrayOrEmpty(value) {
|
|
return Array.isArray(value) ? value : [];
|
|
}
|
|
|
|
export function createProjectReleaseGateManifest() {
|
|
return {
|
|
apiName: "project-release-gate-manifest",
|
|
manifestVersion: 1,
|
|
gateCount: REQUIRED_RELEASE_GATES.length,
|
|
gateIds: REQUIRED_RELEASE_GATES.map(({ id }) => id),
|
|
gates: REQUIRED_RELEASE_GATES.map((gate) => ({
|
|
id: gate.id,
|
|
label: gate.label,
|
|
command: gate.command,
|
|
expectedOutput: gate.expectedOutput ?? null,
|
|
})),
|
|
};
|
|
}
|
|
|
|
export function createProjectReleaseGateResultMatrix({
|
|
gateResults = {},
|
|
observedOutputs = [],
|
|
manifest = createProjectReleaseGateManifest(),
|
|
} = {}) {
|
|
const outputs = Array.isArray(observedOutputs) ? observedOutputs : [];
|
|
const gates = Array.isArray(manifest?.gates) ? manifest.gates : [];
|
|
const rows = gates.map((gate) => {
|
|
const passed = readGatePassed({ gate, gateResults, observedOutputs: outputs });
|
|
return {
|
|
id: gate.id,
|
|
label: gate.label,
|
|
command: gate.command,
|
|
expectedOutput: gate.expectedOutput ?? null,
|
|
status: passed ? "passed" : "unknown",
|
|
passed,
|
|
};
|
|
});
|
|
return {
|
|
apiName: "project-release-gate-result-matrix",
|
|
matrixVersion: 1,
|
|
phase: rows.every(({ passed }) => passed) ? "ready" : "waiting",
|
|
ready: rows.every(({ passed }) => passed),
|
|
gateCount: rows.length,
|
|
passedCount: rows.filter(({ passed }) => passed).length,
|
|
unknownCount: rows.filter(({ passed }) => !passed).length,
|
|
missingGateIds: rows.filter(({ passed }) => !passed).map(({ id }) => id),
|
|
manifest,
|
|
rows,
|
|
};
|
|
}
|
|
|
|
export function createProjectReleaseGateActionPlan({
|
|
gateResultMatrix = null,
|
|
gateResults = {},
|
|
observedOutputs = [],
|
|
manifest = createProjectReleaseGateManifest(),
|
|
} = {}) {
|
|
const matrix = gateResultMatrix && typeof gateResultMatrix === "object"
|
|
? gateResultMatrix
|
|
: createProjectReleaseGateResultMatrix({ gateResults, observedOutputs, manifest });
|
|
const rows = Array.isArray(matrix.rows) ? matrix.rows : [];
|
|
const pendingRows = rows.filter(({ passed }) => passed !== true);
|
|
const completedRows = rows.filter(({ passed }) => passed === true);
|
|
const commands = pendingRows.map((gate, index) => ({
|
|
step: index + 1,
|
|
gateId: gate.id,
|
|
label: gate.label,
|
|
command: gate.command,
|
|
expectedOutput: gate.expectedOutput ?? null,
|
|
}));
|
|
const nextGate = commands[0] ?? null;
|
|
const shellScript = commands.length > 0
|
|
? [
|
|
"#!/usr/bin/env bash",
|
|
"set -euo pipefail",
|
|
"",
|
|
...commands.map(({ command }) => command),
|
|
"",
|
|
].join("\n")
|
|
: "# Project release gates are already marked passed.\n";
|
|
|
|
return {
|
|
apiName: "project-release-gate-action-plan",
|
|
planVersion: 1,
|
|
phase: commands.length === 0 ? "ready" : "waiting",
|
|
ready: commands.length === 0,
|
|
gateCount: rows.length,
|
|
completedCount: completedRows.length,
|
|
pendingCount: pendingRows.length,
|
|
completedGateIds: completedRows.map(({ id }) => id),
|
|
pendingGateIds: pendingRows.map(({ id }) => id),
|
|
nextGateId: nextGate?.gateId ?? null,
|
|
nextCommand: nextGate?.command ?? null,
|
|
commands,
|
|
shellScript,
|
|
rows: commands.map(({ gateId, label, command, expectedOutput }) => ({
|
|
id: gateId,
|
|
label,
|
|
value: command,
|
|
expectedOutput,
|
|
})),
|
|
};
|
|
}
|
|
|
|
export function createProjectReleaseReadinessReport({
|
|
gateResults = {},
|
|
observedOutputs = [],
|
|
simConfigInventory = DEFAULT_SIM_CONFIG_INVENTORY_BASELINE,
|
|
blockedRuntimeFamilies = BLOCKED_RUNTIME_FAMILIES,
|
|
promotedRuntimeFamilies = [],
|
|
} = {}) {
|
|
const gateManifest = createProjectReleaseGateManifest();
|
|
const gateResultMatrix = createProjectReleaseGateResultMatrix({
|
|
gateResults,
|
|
observedOutputs,
|
|
manifest: gateManifest,
|
|
});
|
|
const gateActionPlan = createProjectReleaseGateActionPlan({
|
|
gateResultMatrix,
|
|
manifest: gateManifest,
|
|
});
|
|
const gateRows = gateResultMatrix.rows;
|
|
const releaseGatePassed = gateRows.find(({ id }) => id === "project-release-gate")?.passed === true;
|
|
const inventory = {
|
|
...DEFAULT_SIM_CONFIG_INVENTORY_BASELINE,
|
|
...(simConfigInventory && typeof simConfigInventory === "object" ? simConfigInventory : {}),
|
|
};
|
|
const blockedFamilies = [...blockedRuntimeFamilies];
|
|
const promotedFamilies = [...promotedRuntimeFamilies];
|
|
const promotedBlockedFamilies = promotedFamilies.filter((family) => blockedFamilies.includes(family));
|
|
const inventoryReady = inventory.unexpectedFail === 0;
|
|
const blockedRuntimeReady = promotedBlockedFamilies.length === 0;
|
|
const ready = releaseGatePassed && inventoryReady && blockedRuntimeReady;
|
|
const missing = [
|
|
...(releaseGatePassed ? [] : ["project-release-gate"]),
|
|
...(inventoryReady ? [] : ["sim-config-inventory"]),
|
|
...(blockedRuntimeReady ? [] : ["blocked-runtime-families"]),
|
|
];
|
|
|
|
return {
|
|
apiName: PROJECT_RELEASE_READINESS_ARTIFACT_API,
|
|
reportVersion: PROJECT_RELEASE_READINESS_ARTIFACT_VERSION,
|
|
phase: ready ? "ready" : "waiting",
|
|
ready,
|
|
missing,
|
|
releaseGate: {
|
|
command: "wasm-port/tests/host/verify_project_release_gate.sh",
|
|
passed: releaseGatePassed,
|
|
expectedOutput: "project_release_gate=ok",
|
|
},
|
|
simConfigInventory: inventory,
|
|
blockedRuntimeFamilies: blockedFamilies,
|
|
promotedRuntimeFamilies: promotedFamilies,
|
|
promotedBlockedFamilies,
|
|
gateManifest,
|
|
gateResultMatrix,
|
|
gateActionPlan,
|
|
gates: gateRows,
|
|
rows: [
|
|
{
|
|
id: "project-release-gate",
|
|
label: "Project release gate",
|
|
value: releaseGatePassed ? "passed" : "unknown",
|
|
},
|
|
{
|
|
id: "sim-config-inventory",
|
|
label: "Sim config inventory",
|
|
value: `unexpected_fail=${inventory.unexpectedFail}`,
|
|
},
|
|
{
|
|
id: "blocked-runtime-families",
|
|
label: "Blocked runtime families",
|
|
value: blockedRuntimeReady ? blockedFamilies.join(", ") : `promoted: ${promotedBlockedFamilies.join(", ")}`,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
export function createProjectReleaseReadinessSummaryViewModel(
|
|
report = createProjectReleaseReadinessReport(),
|
|
) {
|
|
const missing = Array.isArray(report?.missing) ? report.missing : [];
|
|
const gateResultMatrix = objectOrEmpty(report?.gateResultMatrix);
|
|
const simConfigInventory = objectOrEmpty(report?.simConfigInventory);
|
|
const blockedRuntimeFamilies = arrayOrEmpty(report?.blockedRuntimeFamilies);
|
|
const promotedBlockedFamilies = arrayOrEmpty(report?.promotedBlockedFamilies);
|
|
const ready = report?.ready === true;
|
|
const statusText = ready ? "Ready" : "Waiting";
|
|
const detailText = ready ? "No blocking reasons" : (missing.length > 0 ? missing.join(", ") : "release evidence missing");
|
|
return {
|
|
apiName: "project-release-readiness-summary-view-model",
|
|
viewModelVersion: 1,
|
|
phase: ready ? "ready" : "waiting",
|
|
ready,
|
|
title: "Project release readiness",
|
|
statusText,
|
|
detailText,
|
|
statusLine: `${statusText}: ${detailText}`,
|
|
rows: [
|
|
{
|
|
id: "gate-results",
|
|
label: "Release gates",
|
|
value: `${gateResultMatrix.passedCount ?? 0}/${gateResultMatrix.gateCount ?? 0} passed`,
|
|
},
|
|
{
|
|
id: "unknown-gates",
|
|
label: "Unknown gates",
|
|
value: `${gateResultMatrix.unknownCount ?? 0}`,
|
|
},
|
|
{
|
|
id: "sim-config-inventory",
|
|
label: "Sim config inventory",
|
|
value: `unexpected_fail=${simConfigInventory.unexpectedFail ?? "unknown"}`,
|
|
},
|
|
{
|
|
id: "blocked-runtime-families",
|
|
label: "Blocked runtime families",
|
|
value: promotedBlockedFamilies.length > 0
|
|
? `promoted: ${promotedBlockedFamilies.join(", ")}`
|
|
: (blockedRuntimeFamilies.join(", ") || "none"),
|
|
},
|
|
{
|
|
id: "missing",
|
|
label: "Missing readiness",
|
|
value: missing.length > 0 ? missing.join(", ") : "none",
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
export function createProjectReleaseReadinessArtifactValidationSummaryViewModel(
|
|
validation = createProjectReleaseReadinessArtifactValidation(),
|
|
) {
|
|
const missing = arrayOrEmpty(validation?.missing);
|
|
const ready = validation?.ready === true;
|
|
const statusText = ready ? "Ready" : "Blocked";
|
|
const detailText = ready
|
|
? "Artifact evidence complete"
|
|
: (missing.length > 0 ? missing.join(", ") : "artifact evidence missing");
|
|
return {
|
|
apiName: "project-release-readiness-artifact-validation-summary-view-model",
|
|
viewModelVersion: 1,
|
|
phase: ready ? "ready" : "blocked",
|
|
ready,
|
|
title: "Project release artifact validation",
|
|
statusText,
|
|
detailText,
|
|
statusLine: `${statusText}: ${detailText}`,
|
|
rows: [
|
|
{
|
|
id: "artifact",
|
|
label: "Readiness artifact",
|
|
value: validation?.artifactApiName ?? "missing",
|
|
},
|
|
{
|
|
id: "gate-count",
|
|
label: "Gate count",
|
|
value: `${validation?.gateCount ?? 0}/${validation?.expectedGateCount ?? 0}`,
|
|
},
|
|
{
|
|
id: "gate-manifest",
|
|
label: "Gate manifest",
|
|
value: validation?.gateManifestReady === true ? "ready" : "missing",
|
|
},
|
|
{
|
|
id: "gate-result-matrix",
|
|
label: "Gate result matrix",
|
|
value: validation?.gateResultMatrixReady === true ? "ready" : "missing",
|
|
},
|
|
{
|
|
id: "gate-action-plan",
|
|
label: "Gate action plan",
|
|
value: validation?.gateActionPlanReady === true ? "ready" : "missing",
|
|
},
|
|
{
|
|
id: "missing",
|
|
label: "Missing artifact evidence",
|
|
value: missing.length > 0 ? missing.join(", ") : "none",
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
export function createProjectReleaseReadinessArtifactValidationActionPlan(
|
|
validation = createProjectReleaseReadinessArtifactValidation(),
|
|
) {
|
|
const missing = arrayOrEmpty(validation?.missing);
|
|
const ready = validation?.ready === true;
|
|
const commands = ready ? [] : [
|
|
{
|
|
step: 1,
|
|
id: "project-release-gate",
|
|
label: "Run project release gate",
|
|
command: "wasm-port/tests/host/verify_project_release_gate.sh",
|
|
expectedOutput: "project_release_gate=ok",
|
|
},
|
|
{
|
|
step: 2,
|
|
id: "artifact-validation",
|
|
label: "Validate project release readiness artifact",
|
|
command: "wasm-port/tests/host/verify_project_release_readiness_artifact.sh build/project-release-readiness.json",
|
|
expectedOutput: "project_release_readiness_artifact_node_smoke=ok",
|
|
},
|
|
];
|
|
const shellScript = commands.length > 0
|
|
? [
|
|
"#!/usr/bin/env bash",
|
|
"set -euo pipefail",
|
|
"",
|
|
...commands.map(({ command }) => command),
|
|
"",
|
|
].join("\n")
|
|
: "# Project release readiness artifact validation is already ready.\n";
|
|
|
|
return {
|
|
apiName: "project-release-readiness-artifact-validation-action-plan",
|
|
planVersion: 1,
|
|
phase: ready ? "ready" : "blocked",
|
|
ready,
|
|
missing,
|
|
missingCount: missing.length,
|
|
commandCount: commands.length,
|
|
nextActionId: commands[0]?.id ?? null,
|
|
nextCommand: commands[0]?.command ?? null,
|
|
commands,
|
|
shellScript,
|
|
rows: [
|
|
{
|
|
id: "artifact-validation",
|
|
label: "Artifact validation",
|
|
value: ready ? "ready" : "blocked",
|
|
},
|
|
{
|
|
id: "missing",
|
|
label: "Missing artifact evidence",
|
|
value: missing.length > 0 ? missing.join(", ") : "none",
|
|
},
|
|
{
|
|
id: "next-command",
|
|
label: "Next command",
|
|
value: commands[0]?.command ?? "none",
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
export function parseProjectReleaseReadinessArtifactJson(text) {
|
|
return JSON.parse(String(text));
|
|
}
|
|
|
|
export function createProjectReleaseReadinessArtifactJsonWorkflow({ artifactJson = "" } = {}) {
|
|
try {
|
|
const artifact = parseProjectReleaseReadinessArtifactJson(artifactJson);
|
|
const validation = createProjectReleaseReadinessArtifactValidation(artifact);
|
|
const summaryViewModel = createProjectReleaseReadinessArtifactValidationSummaryViewModel(validation);
|
|
const actionPlan = createProjectReleaseReadinessArtifactValidationActionPlan(validation);
|
|
return {
|
|
apiName: "project-release-readiness-artifact-json-workflow",
|
|
workflowVersion: 1,
|
|
phase: validation.ready === true ? "ready" : "blocked",
|
|
ready: validation.ready === true,
|
|
parsed: true,
|
|
parseError: null,
|
|
artifactApiName: validation.artifactApiName,
|
|
validation,
|
|
summaryViewModel,
|
|
actionPlan,
|
|
missing: validation.missing,
|
|
rows: [
|
|
{
|
|
id: "parse",
|
|
label: "Artifact JSON parse",
|
|
value: "ready",
|
|
},
|
|
{
|
|
id: "validation",
|
|
label: "Artifact validation",
|
|
value: validation.ready === true ? "ready" : "blocked",
|
|
},
|
|
{
|
|
id: "next-command",
|
|
label: "Next command",
|
|
value: actionPlan.nextCommand ?? "none",
|
|
},
|
|
],
|
|
};
|
|
} catch (error) {
|
|
const validation = createProjectReleaseReadinessArtifactValidation({});
|
|
const summaryViewModel = createProjectReleaseReadinessArtifactValidationSummaryViewModel(validation);
|
|
const actionPlan = createProjectReleaseReadinessArtifactValidationActionPlan(validation);
|
|
const parseError = error?.message ?? "artifact JSON parse error";
|
|
return {
|
|
apiName: "project-release-readiness-artifact-json-workflow",
|
|
workflowVersion: 1,
|
|
phase: "blocked",
|
|
ready: false,
|
|
parsed: false,
|
|
parseError,
|
|
artifactApiName: null,
|
|
validation,
|
|
summaryViewModel,
|
|
actionPlan,
|
|
missing: ["artifact-json", ...validation.missing],
|
|
rows: [
|
|
{
|
|
id: "parse",
|
|
label: "Artifact JSON parse",
|
|
value: "blocked",
|
|
},
|
|
{
|
|
id: "validation",
|
|
label: "Artifact validation",
|
|
value: "blocked",
|
|
},
|
|
{
|
|
id: "next-command",
|
|
label: "Next command",
|
|
value: actionPlan.nextCommand ?? "none",
|
|
},
|
|
],
|
|
};
|
|
}
|
|
}
|
|
|
|
export async function loadProjectReleaseReadinessArtifactUrlWorkflow({
|
|
artifactUrl = "",
|
|
fetchRef = globalThis.fetch,
|
|
} = {}) {
|
|
if (!artifactUrl || typeof fetchRef !== "function") {
|
|
const jsonWorkflow = createProjectReleaseReadinessArtifactJsonWorkflow({ artifactJson: "" });
|
|
const missing = [
|
|
...(artifactUrl ? [] : ["artifact-url"]),
|
|
...(typeof fetchRef === "function" ? [] : ["fetch"]),
|
|
...jsonWorkflow.missing,
|
|
];
|
|
return {
|
|
apiName: "project-release-readiness-artifact-url-workflow",
|
|
workflowVersion: 1,
|
|
phase: "blocked",
|
|
ready: false,
|
|
artifactUrl,
|
|
fetched: false,
|
|
httpStatus: null,
|
|
fetchError: null,
|
|
jsonWorkflow,
|
|
validation: jsonWorkflow.validation,
|
|
summaryViewModel: jsonWorkflow.summaryViewModel,
|
|
actionPlan: jsonWorkflow.actionPlan,
|
|
missing,
|
|
rows: [
|
|
{
|
|
id: "fetch",
|
|
label: "Artifact URL fetch",
|
|
value: "blocked",
|
|
},
|
|
{
|
|
id: "json-workflow",
|
|
label: "Artifact JSON workflow",
|
|
value: "blocked",
|
|
},
|
|
{
|
|
id: "next-command",
|
|
label: "Next command",
|
|
value: jsonWorkflow.actionPlan.nextCommand ?? "none",
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
try {
|
|
const response = await fetchRef(artifactUrl);
|
|
const httpStatus = response?.status ?? null;
|
|
if (!response?.ok) {
|
|
const jsonWorkflow = createProjectReleaseReadinessArtifactJsonWorkflow({ artifactJson: "" });
|
|
const fetchError = `artifact URL fetch failed${httpStatus ? `: ${httpStatus}` : ""}`;
|
|
return {
|
|
apiName: "project-release-readiness-artifact-url-workflow",
|
|
workflowVersion: 1,
|
|
phase: "blocked",
|
|
ready: false,
|
|
artifactUrl,
|
|
fetched: false,
|
|
httpStatus,
|
|
fetchError,
|
|
jsonWorkflow,
|
|
validation: jsonWorkflow.validation,
|
|
summaryViewModel: jsonWorkflow.summaryViewModel,
|
|
actionPlan: jsonWorkflow.actionPlan,
|
|
missing: ["artifact-fetch", ...jsonWorkflow.missing],
|
|
rows: [
|
|
{
|
|
id: "fetch",
|
|
label: "Artifact URL fetch",
|
|
value: "blocked",
|
|
},
|
|
{
|
|
id: "json-workflow",
|
|
label: "Artifact JSON workflow",
|
|
value: "blocked",
|
|
},
|
|
{
|
|
id: "next-command",
|
|
label: "Next command",
|
|
value: jsonWorkflow.actionPlan.nextCommand ?? "none",
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
const artifactJson = await response.text();
|
|
const jsonWorkflow = createProjectReleaseReadinessArtifactJsonWorkflow({ artifactJson });
|
|
return {
|
|
apiName: "project-release-readiness-artifact-url-workflow",
|
|
workflowVersion: 1,
|
|
phase: jsonWorkflow.ready === true ? "ready" : "blocked",
|
|
ready: jsonWorkflow.ready === true,
|
|
artifactUrl,
|
|
fetched: true,
|
|
httpStatus,
|
|
fetchError: null,
|
|
jsonWorkflow,
|
|
validation: jsonWorkflow.validation,
|
|
summaryViewModel: jsonWorkflow.summaryViewModel,
|
|
actionPlan: jsonWorkflow.actionPlan,
|
|
missing: jsonWorkflow.missing,
|
|
rows: [
|
|
{
|
|
id: "fetch",
|
|
label: "Artifact URL fetch",
|
|
value: "ready",
|
|
},
|
|
{
|
|
id: "json-workflow",
|
|
label: "Artifact JSON workflow",
|
|
value: jsonWorkflow.ready === true ? "ready" : "blocked",
|
|
},
|
|
{
|
|
id: "next-command",
|
|
label: "Next command",
|
|
value: jsonWorkflow.actionPlan.nextCommand ?? "none",
|
|
},
|
|
],
|
|
};
|
|
} catch (error) {
|
|
const jsonWorkflow = createProjectReleaseReadinessArtifactJsonWorkflow({ artifactJson: "" });
|
|
return {
|
|
apiName: "project-release-readiness-artifact-url-workflow",
|
|
workflowVersion: 1,
|
|
phase: "blocked",
|
|
ready: false,
|
|
artifactUrl,
|
|
fetched: false,
|
|
httpStatus: null,
|
|
fetchError: error?.message ?? "artifact URL fetch error",
|
|
jsonWorkflow,
|
|
validation: jsonWorkflow.validation,
|
|
summaryViewModel: jsonWorkflow.summaryViewModel,
|
|
actionPlan: jsonWorkflow.actionPlan,
|
|
missing: ["artifact-fetch", ...jsonWorkflow.missing],
|
|
rows: [
|
|
{
|
|
id: "fetch",
|
|
label: "Artifact URL fetch",
|
|
value: "blocked",
|
|
},
|
|
{
|
|
id: "json-workflow",
|
|
label: "Artifact JSON workflow",
|
|
value: "blocked",
|
|
},
|
|
{
|
|
id: "next-command",
|
|
label: "Next command",
|
|
value: jsonWorkflow.actionPlan.nextCommand ?? "none",
|
|
},
|
|
],
|
|
};
|
|
}
|
|
}
|
|
|
|
export function createProjectReleaseReadinessArtifactUrlWorkflowSummaryViewModel(
|
|
workflow = {},
|
|
) {
|
|
const missing = arrayOrEmpty(workflow?.missing);
|
|
const ready = workflow?.ready === true;
|
|
const statusText = ready ? "Ready" : "Blocked";
|
|
const detailText = ready
|
|
? "Artifact URL workflow complete"
|
|
: (missing.length > 0 ? missing.join(", ") : workflow?.fetchError ?? "artifact URL workflow blocked");
|
|
return {
|
|
apiName: "project-release-readiness-artifact-url-workflow-summary-view-model",
|
|
viewModelVersion: 1,
|
|
phase: ready ? "ready" : "blocked",
|
|
ready,
|
|
title: "Project release artifact URL workflow",
|
|
statusText,
|
|
detailText,
|
|
statusLine: `${statusText}: ${detailText}`,
|
|
rows: [
|
|
{
|
|
id: "artifact-url",
|
|
label: "Artifact URL",
|
|
value: workflow?.artifactUrl || "missing",
|
|
},
|
|
{
|
|
id: "fetch",
|
|
label: "Fetch",
|
|
value: workflow?.fetched === true ? `ready${workflow?.httpStatus ? ` (${workflow.httpStatus})` : ""}` : "blocked",
|
|
},
|
|
{
|
|
id: "json-workflow",
|
|
label: "JSON workflow",
|
|
value: workflow?.jsonWorkflow?.ready === true ? "ready" : "blocked",
|
|
},
|
|
{
|
|
id: "artifact-validation",
|
|
label: "Artifact validation",
|
|
value: workflow?.validation?.ready === true ? "ready" : "blocked",
|
|
},
|
|
{
|
|
id: "next-command",
|
|
label: "Next command",
|
|
value: workflow?.actionPlan?.nextCommand ?? "none",
|
|
},
|
|
{
|
|
id: "missing",
|
|
label: "Missing workflow evidence",
|
|
value: missing.length > 0 ? missing.join(", ") : "none",
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
export function createProjectReleaseReadinessArtifactUrlWorkflowActionPlan(
|
|
workflow = {},
|
|
) {
|
|
const missing = arrayOrEmpty(workflow?.missing);
|
|
const ready = workflow?.ready === true;
|
|
const validationCommands = arrayOrEmpty(workflow?.actionPlan?.commands);
|
|
const inputActions = [
|
|
...(missing.includes("artifact-url")
|
|
? [{
|
|
id: "provide-artifact-url",
|
|
label: "Provide release readiness artifact URL",
|
|
kind: "input",
|
|
command: null,
|
|
expectedOutput: null,
|
|
}]
|
|
: []),
|
|
...(missing.includes("fetch")
|
|
? [{
|
|
id: "provide-fetch",
|
|
label: "Provide fetch implementation",
|
|
kind: "input",
|
|
command: null,
|
|
expectedOutput: null,
|
|
}]
|
|
: []),
|
|
...(missing.includes("artifact-fetch")
|
|
? [{
|
|
id: "verify-artifact-url-fetch",
|
|
label: "Verify release readiness artifact URL fetch",
|
|
kind: "fetch",
|
|
command: null,
|
|
expectedOutput: null,
|
|
}]
|
|
: []),
|
|
];
|
|
const commandActions = validationCommands.map((command) => ({
|
|
id: command.id ?? command.gateId ?? `command-${command.step}`,
|
|
label: command.label,
|
|
kind: "command",
|
|
command: command.command,
|
|
expectedOutput: command.expectedOutput ?? null,
|
|
}));
|
|
const actions = ready ? [] : [...inputActions, ...commandActions].map((action, index) => ({
|
|
step: index + 1,
|
|
...action,
|
|
}));
|
|
const commands = actions.filter(({ command }) => typeof command === "string" && command.length > 0);
|
|
const shellScript = commands.length > 0
|
|
? [
|
|
"#!/usr/bin/env bash",
|
|
"set -euo pipefail",
|
|
"",
|
|
...commands.map(({ command }) => command),
|
|
"",
|
|
].join("\n")
|
|
: (ready
|
|
? "# Project release readiness artifact URL workflow is already ready.\n"
|
|
: "# Project release readiness artifact URL workflow requires non-shell input.\n");
|
|
|
|
return {
|
|
apiName: "project-release-readiness-artifact-url-workflow-action-plan",
|
|
planVersion: 1,
|
|
phase: ready ? "ready" : "blocked",
|
|
ready,
|
|
missing,
|
|
actionCount: actions.length,
|
|
commandCount: commands.length,
|
|
nextActionId: actions[0]?.id ?? null,
|
|
nextCommand: commands[0]?.command ?? null,
|
|
actions,
|
|
commands,
|
|
shellScript,
|
|
rows: actions.map(({ id, label, kind, command, expectedOutput }) => ({
|
|
id,
|
|
label,
|
|
kind,
|
|
value: command ?? kind,
|
|
expectedOutput,
|
|
})),
|
|
};
|
|
}
|
|
|
|
export function createProjectReleaseReadinessArtifactValidation(artifact = {}) {
|
|
const artifactObject = objectOrEmpty(artifact);
|
|
const releaseGate = objectOrEmpty(artifactObject.releaseGate);
|
|
const simConfigInventory = objectOrEmpty(artifactObject.simConfigInventory);
|
|
const gateManifest = objectOrEmpty(artifactObject.gateManifest);
|
|
const gateResultMatrix = objectOrEmpty(artifactObject.gateResultMatrix);
|
|
const gateActionPlan = objectOrEmpty(artifactObject.gateActionPlan);
|
|
const gates = arrayOrEmpty(artifactObject.gates);
|
|
const manifestGateIds = arrayOrEmpty(gateManifest.gateIds);
|
|
const matrixRows = arrayOrEmpty(gateResultMatrix.rows);
|
|
const actionPlanCommands = arrayOrEmpty(gateActionPlan.commands);
|
|
const rows = arrayOrEmpty(artifactObject.rows);
|
|
const expectedGateIds = createProjectReleaseGateManifest().gateIds;
|
|
const gateCountReady = gates.length === REQUIRED_RELEASE_GATES.length;
|
|
const gatesPassed = gateCountReady && gates.every(({ passed }) => passed === true);
|
|
const manifestReady = gateManifest.apiName === "project-release-gate-manifest"
|
|
&& gateManifest.manifestVersion === 1
|
|
&& gateManifest.gateCount === REQUIRED_RELEASE_GATES.length
|
|
&& manifestGateIds.join(",") === expectedGateIds.join(",");
|
|
const matrixReady = gateResultMatrix.apiName === "project-release-gate-result-matrix"
|
|
&& gateResultMatrix.matrixVersion === 1
|
|
&& gateResultMatrix.ready === true
|
|
&& gateResultMatrix.passedCount === REQUIRED_RELEASE_GATES.length
|
|
&& gateResultMatrix.unknownCount === 0
|
|
&& matrixRows.length === REQUIRED_RELEASE_GATES.length;
|
|
const actionPlanReady = gateActionPlan.apiName === "project-release-gate-action-plan"
|
|
&& gateActionPlan.planVersion === 1
|
|
&& gateActionPlan.ready === true
|
|
&& gateActionPlan.pendingCount === 0
|
|
&& gateActionPlan.nextCommand === null
|
|
&& actionPlanCommands.length === 0;
|
|
const missing = [
|
|
...(artifactObject.apiName === PROJECT_RELEASE_READINESS_ARTIFACT_API ? [] : ["apiName"]),
|
|
...(artifactObject.reportVersion === PROJECT_RELEASE_READINESS_ARTIFACT_VERSION ? [] : ["reportVersion"]),
|
|
...(artifactObject.phase === "ready" ? [] : ["phase"]),
|
|
...(artifactObject.ready === true ? [] : ["ready"]),
|
|
...(Array.isArray(artifactObject.missing) && artifactObject.missing.length === 0 ? [] : ["missing"]),
|
|
...(releaseGate.command === "wasm-port/tests/host/verify_project_release_gate.sh" ? [] : ["releaseGate.command"]),
|
|
...(releaseGate.passed === true ? [] : ["releaseGate.passed"]),
|
|
...(releaseGate.expectedOutput === "project_release_gate=ok" ? [] : ["releaseGate.expectedOutput"]),
|
|
...(simConfigInventory.executed === 28 ? [] : ["simConfigInventory.executed"]),
|
|
...(simConfigInventory.passed === 28 ? [] : ["simConfigInventory.passed"]),
|
|
...(simConfigInventory.skipped === 131 ? [] : ["simConfigInventory.skipped"]),
|
|
...(simConfigInventory.unexpectedFail === 0 ? [] : ["simConfigInventory.unexpectedFail"]),
|
|
...(arrayOrEmpty(artifactObject.blockedRuntimeFamilies).join(",") === BLOCKED_RUNTIME_FAMILIES.join(",")
|
|
? []
|
|
: ["blockedRuntimeFamilies"]),
|
|
...(arrayOrEmpty(artifactObject.promotedBlockedFamilies).length === 0 ? [] : ["promotedBlockedFamilies"]),
|
|
...(manifestReady ? [] : ["gateManifest"]),
|
|
...(matrixReady ? [] : ["gateResultMatrix"]),
|
|
...(actionPlanReady ? [] : ["gateActionPlan"]),
|
|
...(gateCountReady ? [] : ["gates.length"]),
|
|
...(gatesPassed ? [] : ["gates.passed"]),
|
|
...(rows.find(({ id, value }) => id === "project-release-gate" && value === "passed")
|
|
? []
|
|
: ["rows.project-release-gate"]),
|
|
];
|
|
|
|
return {
|
|
apiName: "project-release-readiness-artifact-validation",
|
|
validationVersion: 1,
|
|
phase: missing.length === 0 ? "ready" : "blocked",
|
|
ready: missing.length === 0,
|
|
missing,
|
|
artifactApiName: artifactObject.apiName ?? null,
|
|
artifactVersion: artifactObject.reportVersion ?? null,
|
|
gateCount: gates.length,
|
|
expectedGateCount: REQUIRED_RELEASE_GATES.length,
|
|
expectedGateIds,
|
|
gateManifestReady: manifestReady,
|
|
gateResultMatrixReady: matrixReady,
|
|
gateActionPlanReady: actionPlanReady,
|
|
blockedRuntimeFamilies: arrayOrEmpty(artifactObject.blockedRuntimeFamilies),
|
|
rows: [
|
|
{
|
|
id: "artifact",
|
|
label: "Readiness artifact",
|
|
value: artifactObject.apiName ?? "missing",
|
|
},
|
|
{
|
|
id: "project-release-gate",
|
|
label: "Project release gate",
|
|
value: releaseGate.passed === true ? "passed" : "missing",
|
|
},
|
|
{
|
|
id: "gate-manifest",
|
|
label: "Gate manifest",
|
|
value: manifestReady ? "ready" : "missing",
|
|
},
|
|
{
|
|
id: "gate-result-matrix",
|
|
label: "Gate result matrix",
|
|
value: matrixReady ? "ready" : "missing",
|
|
},
|
|
{
|
|
id: "gate-action-plan",
|
|
label: "Gate action plan",
|
|
value: actionPlanReady ? "ready" : "missing",
|
|
},
|
|
{
|
|
id: "validation",
|
|
label: "Artifact validation",
|
|
value: missing.length === 0 ? "ready" : missing.join(", "),
|
|
},
|
|
],
|
|
};
|
|
}
|