强化发布 readiness artifact gate evidence 校验

This commit is contained in:
2026-06-16 11:01:37 +08:00
parent 9abca15d60
commit ce2c145374
10 changed files with 442 additions and 40 deletions

View File

@@ -51,7 +51,9 @@ continuation records are in `../text14.txt`.
with `verify_project_release_readiness_artifact.sh`.
External tools can load that artifact with
`parseProjectReleaseReadinessArtifactJson()` and validate it with
`createProjectReleaseReadinessArtifactValidation()`.
`createProjectReleaseReadinessArtifactValidation()`. Artifact validation
requires the embedded gate manifest, result matrix, and action plan to be
ready, so stale release JSON cannot pass the executable artifact gate.
Browser shells can also pass the JSON to
`linuxCncIniPanelWorkflowOverviewApi.validateWorkflowOverviewReleaseReadinessArtifactJson()`
through the workflow overview iframe and receive stable validation rows. They

View File

@@ -223,7 +223,9 @@ load that JSON with `parseProjectReleaseReadinessArtifactJson()` and validate
it with `createProjectReleaseReadinessArtifactValidation()` without copying the
host test assertions. The validation result includes `expectedGateCount` and
`expectedGateIds` so external callers can compare an artifact against the
current manifest.
current manifest, plus `gateManifestReady`, `gateResultMatrixReady`, and
`gateActionPlanReady` so stale artifacts that omit the embedded gate evidence
chain fail the same executable gate.
## OPFS/session persistence exports

View File

@@ -332,10 +332,33 @@ 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"]),
@@ -353,6 +376,9 @@ export function createProjectReleaseReadinessArtifactValidation(artifact = {}) {
? []
: ["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")
@@ -370,7 +396,10 @@ export function createProjectReleaseReadinessArtifactValidation(artifact = {}) {
artifactVersion: artifactObject.reportVersion ?? null,
gateCount: gates.length,
expectedGateCount: REQUIRED_RELEASE_GATES.length,
expectedGateIds: createProjectReleaseGateManifest().gateIds,
expectedGateIds,
gateManifestReady: manifestReady,
gateResultMatrixReady: matrixReady,
gateActionPlanReady: actionPlanReady,
blockedRuntimeFamilies: arrayOrEmpty(artifactObject.blockedRuntimeFamilies),
rows: [
{
@@ -383,6 +412,21 @@ export function createProjectReleaseReadinessArtifactValidation(artifact = {}) {
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",

View File

@@ -38,6 +38,7 @@ for (const phrase of [
"createProjectReleaseReadinessSummaryViewModel()",
"parseProjectReleaseReadinessArtifactJson()",
"createProjectReleaseReadinessArtifactValidation()",
"embedded gate manifest, result matrix, and action plan",
"validateWorkflowOverviewReleaseReadinessArtifactJson()",
"loadWorkflowOverviewReleaseReadinessArtifactUrl()",
"render-state",
@@ -107,6 +108,9 @@ for (const phrase of [
"createProjectReleaseReadinessSummaryViewModel()",
"parseProjectReleaseReadinessArtifactJson()",
"createProjectReleaseReadinessArtifactValidation()",
"gateManifestReady",
"gateResultMatrixReady",
"gateActionPlanReady",
"loadIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrl()",
"build/project-release-readiness.json",
"INI panel shell handoff exports",

View File

@@ -22,6 +22,283 @@
],
"promotedRuntimeFamilies": [],
"promotedBlockedFamilies": [],
"gateManifest": {
"apiName": "project-release-gate-manifest",
"manifestVersion": 1,
"gateCount": 10,
"gateIds": [
"diff-check",
"vendor-sync",
"standalone-cnc-semantics",
"interp-wasm",
"sim-config-inventory",
"ini-panel-browser",
"release-artifact-url-browser",
"ui-node-smokes",
"host-smokes",
"project-release-gate"
],
"gates": [
{
"id": "diff-check",
"label": "Git diff check",
"command": "git diff --check",
"expectedOutput": null
},
{
"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"
}
]
},
"gateResultMatrix": {
"apiName": "project-release-gate-result-matrix",
"matrixVersion": 1,
"phase": "ready",
"ready": true,
"gateCount": 10,
"passedCount": 10,
"unknownCount": 0,
"missingGateIds": [],
"manifest": {
"apiName": "project-release-gate-manifest",
"manifestVersion": 1,
"gateCount": 10,
"gateIds": [
"diff-check",
"vendor-sync",
"standalone-cnc-semantics",
"interp-wasm",
"sim-config-inventory",
"ini-panel-browser",
"release-artifact-url-browser",
"ui-node-smokes",
"host-smokes",
"project-release-gate"
],
"gates": [
{
"id": "diff-check",
"label": "Git diff check",
"command": "git diff --check",
"expectedOutput": null
},
{
"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"
}
]
},
"rows": [
{
"id": "diff-check",
"label": "Git diff check",
"command": "git diff --check",
"expectedOutput": null,
"status": "passed",
"passed": true
},
{
"id": "vendor-sync",
"label": "Vendor sync guard",
"command": "wasm-port/tools/verify_vendor_sync.sh",
"expectedOutput": "vendor sync up to date",
"status": "passed",
"passed": true
},
{
"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",
"status": "passed",
"passed": true
},
{
"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",
"status": "passed",
"passed": true
},
{
"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",
"status": "passed",
"passed": true
},
{
"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",
"status": "passed",
"passed": true
},
{
"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",
"status": "passed",
"passed": true
},
{
"id": "ui-node-smokes",
"label": "UI Node smokes",
"command": "wasm-port/tests/ui/node/verify_ui_node_smokes.sh",
"expectedOutput": "ui_node_smokes=ok",
"status": "passed",
"passed": true
},
{
"id": "host-smokes",
"label": "Host aggregate smoke",
"command": "wasm-port/tests/host/verify_host_smokes.sh",
"expectedOutput": "host_wasm_opfs_browser_smokes=ok",
"status": "passed",
"passed": true
},
{
"id": "project-release-gate",
"label": "Project release gate",
"command": "wasm-port/tests/host/verify_project_release_gate.sh",
"expectedOutput": "project_release_gate=ok",
"status": "passed",
"passed": true
}
]
},
"gateActionPlan": {
"apiName": "project-release-gate-action-plan",
"planVersion": 1,
"phase": "ready",
"ready": true,
"gateCount": 10,
"completedCount": 10,
"pendingCount": 0,
"completedGateIds": [
"diff-check",
"vendor-sync",
"standalone-cnc-semantics",
"interp-wasm",
"sim-config-inventory",
"ini-panel-browser",
"release-artifact-url-browser",
"ui-node-smokes",
"host-smokes",
"project-release-gate"
],
"pendingGateIds": [],
"nextGateId": null,
"nextCommand": null,
"commands": [],
"shellScript": "# Project release gates are already marked passed.\n",
"rows": []
},
"gates": [
{
"id": "diff-check",

View File

@@ -22,6 +22,9 @@ assert.equal(validation.artifactApiName, "project-release-readiness-report");
assert.equal(validation.artifactVersion, 1);
assert.equal(validation.gateCount, 10);
assert.equal(validation.expectedGateCount, 10);
assert.equal(validation.gateManifestReady, true);
assert.equal(validation.gateResultMatrixReady, true);
assert.equal(validation.gateActionPlanReady, true);
assert.deepEqual(validation.expectedGateIds, [
"diff-check",
"vendor-sync",
@@ -34,5 +37,10 @@ assert.deepEqual(validation.expectedGateIds, [
"host-smokes",
"project-release-gate",
]);
assert.equal(artifact.gateManifest.apiName, "project-release-gate-manifest");
assert.equal(artifact.gateResultMatrix.apiName, "project-release-gate-result-matrix");
assert.equal(artifact.gateActionPlan.apiName, "project-release-gate-action-plan");
assert.equal(artifact.gateActionPlan.pendingCount, 0);
assert.equal(artifact.gateActionPlan.nextCommand, null);
console.log("project_release_readiness_artifact_node_smoke=ok");

View File

@@ -266,6 +266,9 @@ assert.deepEqual(createProjectReleaseReadinessArtifactValidation(releaseReadines
gateCount: 10,
expectedGateCount: 10,
expectedGateIds: createProjectReleaseGateManifest().gateIds,
gateManifestReady: true,
gateResultMatrixReady: true,
gateActionPlanReady: true,
blockedRuntimeFamilies: [
"L4-USER-M-PROCESS",
"L4-TOOL-DB",
@@ -282,6 +285,21 @@ assert.deepEqual(createProjectReleaseReadinessArtifactValidation(releaseReadines
label: "Project release gate",
value: "passed",
},
{
id: "gate-manifest",
label: "Gate manifest",
value: "ready",
},
{
id: "gate-result-matrix",
label: "Gate result matrix",
value: "ready",
},
{
id: "gate-action-plan",
label: "Gate action plan",
value: "ready",
},
{
id: "validation",
label: "Artifact validation",
@@ -305,6 +323,9 @@ assert.deepEqual(
"simConfigInventory.skipped",
"simConfigInventory.unexpectedFail",
"blockedRuntimeFamilies",
"gateManifest",
"gateResultMatrix",
"gateActionPlan",
"gates.length",
"gates.passed",
"rows.project-release-gate",

View File

@@ -1,5 +1,10 @@
import assert from "node:assert/strict";
import {
createProjectReleaseGateManifest,
createProjectReleaseReadinessReport,
} from "../../../runtime/sdk/src/project-release-readiness.js";
import {
INI_PANEL_ENTRIES,
controlPageStatusText,
@@ -1285,38 +1290,9 @@ assert.equal(workflowDomMountResult.ready, false);
assert.equal(workflowDomMountResult.renderResult.rowCount, 8);
assert.equal(workflowDomMountResult.renderResult.dataset.handoffScope, "workflow");
assert.deepEqual(createIniPanelShellWorkflowOverviewContract(), shellViewModel.shellWorkflowOverviewContract);
const releaseReadinessArtifactJson = JSON.stringify({
apiName: "project-release-readiness-report",
reportVersion: 1,
phase: "ready",
ready: true,
missing: [],
releaseGate: {
command: "wasm-port/tests/host/verify_project_release_gate.sh",
passed: true,
expectedOutput: "project_release_gate=ok",
},
simConfigInventory: {
executed: 28,
passed: 28,
skipped: 131,
unexpectedFail: 0,
},
blockedRuntimeFamilies: [
"L4-USER-M-PROCESS",
"L4-TOOL-DB",
"L4-PYTHON-REMAP",
],
promotedBlockedFamilies: [],
gates: Array.from({ length: 10 }, (_, index) => ({ id: `gate-${index}`, passed: true })),
rows: [
{
id: "project-release-gate",
label: "Project release gate",
value: "passed",
},
],
});
const releaseReadinessArtifactJson = JSON.stringify(createProjectReleaseReadinessReport({
gateResults: Object.fromEntries(createProjectReleaseGateManifest().gateIds.map((id) => [id, true])),
}));
const releaseArtifactValidation = validateIniPanelShellWorkflowOverviewReleaseReadinessArtifactJson(
releaseReadinessArtifactJson,
);
@@ -1373,10 +1349,14 @@ const releaseArtifactMountResult = mountIniPanelShellWorkflowOverviewReleaseRead
rowsNode: releaseArtifactRowsNode,
});
assert.equal(releaseArtifactRenderResult.rendered, true);
assert.equal(releaseArtifactRenderResult.rowCount, 3);
assert.equal(releaseArtifactRenderResult.rowCount, 6);
assert.equal(releaseArtifactRenderResult.dataset.handoffScope, "workflow-overview-release-readiness-artifact");
assert.equal(
releaseArtifactRenderResult.rowIds.includes("gate-action-plan"),
true,
);
assert.equal(releaseArtifactMountResult.ready, true);
assert.equal(releaseArtifactMountResult.renderResult.rowCount, 3);
assert.equal(releaseArtifactMountResult.renderResult.rowCount, 6);
assert.deepEqual(
validateIniPanelShellWorkflowOverviewReleaseReadinessArtifactJson("{").missing,
["artifact-json"],
@@ -1409,7 +1389,7 @@ assert.equal(releaseArtifactUrlWorkflow.httpStatus, 200);
assert.equal(releaseArtifactUrlWorkflow.validation.ready, true);
assert.equal(releaseArtifactUrlWorkflow.renderState.statusLine, "Ready: No blocking reasons");
assert.equal(releaseArtifactUrlWorkflow.mountResult.ready, true);
assert.equal(releaseArtifactUrlWorkflow.mountResult.renderResult.rowCount, 3);
assert.equal(releaseArtifactUrlWorkflow.mountResult.renderResult.rowCount, 6);
const releaseArtifactUrlWorkflowMissingDom = await loadIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrl({
artifactUrl: "/project-release-readiness.json",
fetchRef: async () => ({