新增项目发布 gate action plan API

This commit is contained in:
2026-06-16 10:51:18 +08:00
parent 1d383b475c
commit 9abca15d60
9 changed files with 185 additions and 9 deletions

View File

@@ -37,10 +37,12 @@ continuation records are in `../text14.txt`.
- CI dashboards and external SDK callers can use
`createProjectReleaseGateManifest()` to discover required gate commands and
expected smoke outputs, `createProjectReleaseGateResultMatrix()` to map
observed outputs onto passed/unknown gate rows, and
observed outputs onto passed/unknown gate rows,
`createProjectReleaseGateActionPlan()` to derive pending gate commands, the
next command, display rows, and a shell script from that matrix, and
`createProjectReleaseReadinessReport()` for a machine-readable release
readiness report covering the same manifest, result matrix, sim-config
inventory baseline, and blocked runtime families.
readiness report covering the same manifest, result matrix, action plan,
sim-config inventory baseline, and blocked runtime families.
`createProjectReleaseReadinessSummaryViewModel()` turns that report into
stable status text and rows for dashboards. The report helper is evidence
driven and only reports `ready: true` when the caller supplies release gate

View File

@@ -51,6 +51,7 @@ import {
createMachineSessionPersistenceRenderState,
createMachineSessionPersistenceSummary,
createMachineSessionSnapshotPayload,
createProjectReleaseGateActionPlan,
createProjectReleaseGateManifest,
createProjectReleaseGateResultMatrix,
createProjectReleaseReadinessArtifactValidation,
@@ -196,11 +197,15 @@ It does not implement or reinterpret LinuxCNC error semantics.
commands, expected smoke outputs, and gate count for CI dashboards and external
SDK callers. `createProjectReleaseGateResultMatrix()` maps observed gate output
and explicit gate results onto that manifest as stable `passed`/`unknown` rows.
`createProjectReleaseGateActionPlan()` turns that matrix into pending gate
commands, the next gate command, stable display rows, and a shell script that
external CI can execute or show without duplicating the manifest order.
`createProjectReleaseReadinessReport()` returns a machine-readable release
readiness report that embeds the same manifest and result matrix beside the
sim-config inventory baseline and blocked runtime families. It is evidence
driven: the default report is `ready: false` until the caller supplies observed
gate output such as `project_release_gate=ok` or explicit gate results.
readiness report that embeds the same manifest, result matrix, and action plan
beside the sim-config inventory baseline and blocked runtime families. It is
evidence driven: the default report is `ready: false` until the caller supplies
observed gate output such as `project_release_gate=ok` or explicit gate
results.
`createProjectReleaseReadinessSummaryViewModel()` turns the report into stable
status text, a status line, and display rows for dashboards without executing
any gate.

View File

@@ -1,6 +1,7 @@
export { createLinuxCncIniSdk } from "./linuxcnc-ini.js";
export { createLinuxCncInterpSdk } from "./linuxcnc-interp.js";
export {
createProjectReleaseGateActionPlan,
createProjectReleaseGateManifest,
createProjectReleaseGateResultMatrix,
createProjectReleaseReadinessArtifactValidation,

View File

@@ -145,6 +145,59 @@ export function createProjectReleaseGateResultMatrix({
};
}
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 = [],
@@ -158,6 +211,10 @@ export function createProjectReleaseReadinessReport({
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 = {
@@ -193,6 +250,7 @@ export function createProjectReleaseReadinessReport({
promotedBlockedFamilies,
gateManifest,
gateResultMatrix,
gateActionPlan,
gates: gateRows,
rows: [
{

View File

@@ -33,6 +33,7 @@ for (const phrase of [
"runtime/sdk/src/index.js",
"createProjectReleaseGateManifest()",
"createProjectReleaseGateResultMatrix()",
"createProjectReleaseGateActionPlan()",
"createProjectReleaseReadinessReport()",
"createProjectReleaseReadinessSummaryViewModel()",
"parseProjectReleaseReadinessArtifactJson()",
@@ -101,6 +102,7 @@ for (const phrase of [
"Project release readiness export",
"createProjectReleaseGateManifest()",
"createProjectReleaseGateResultMatrix()",
"createProjectReleaseGateActionPlan()",
"createProjectReleaseReadinessReport()",
"createProjectReleaseReadinessSummaryViewModel()",
"parseProjectReleaseReadinessArtifactJson()",

View File

@@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import {
createProjectReleaseGateActionPlan,
createProjectReleaseGateManifest,
createProjectReleaseGateResultMatrix,
createProjectReleaseReadinessReport,
@@ -78,12 +79,47 @@ assert.deepEqual(
],
);
const partialActionPlan = createProjectReleaseGateActionPlan({ gateResultMatrix: partialMatrix });
assert.equal(partialActionPlan.apiName, "project-release-gate-action-plan");
assert.equal(partialActionPlan.planVersion, 1);
assert.equal(partialActionPlan.phase, "waiting");
assert.equal(partialActionPlan.ready, false);
assert.equal(partialActionPlan.gateCount, 10);
assert.equal(partialActionPlan.completedCount, 3);
assert.equal(partialActionPlan.pendingCount, 7);
assert.deepEqual(partialActionPlan.completedGateIds, [
"diff-check",
"vendor-sync",
"release-artifact-url-browser",
]);
assert.deepEqual(partialActionPlan.pendingGateIds, [
"standalone-cnc-semantics",
"interp-wasm",
"sim-config-inventory",
"ini-panel-browser",
"ui-node-smokes",
"host-smokes",
"project-release-gate",
]);
assert.equal(partialActionPlan.nextGateId, "standalone-cnc-semantics");
assert.equal(partialActionPlan.nextCommand, "wasm-port/tools/verify_no_standalone_cnc_semantics.sh");
assert.equal(partialActionPlan.commands.length, 7);
assert.equal(partialActionPlan.commands[0].step, 1);
assert.equal(partialActionPlan.commands[0].gateId, "standalone-cnc-semantics");
assert.equal(partialActionPlan.rows[0].id, "standalone-cnc-semantics");
assert.match(partialActionPlan.shellScript, /^#!\/usr\/bin\/env bash\nset -euo pipefail\n/);
assert.match(partialActionPlan.shellScript, /wasm-port\/tests\/host\/verify_project_release_gate\.sh/);
const report = createProjectReleaseReadinessReport({
gateResults: Object.fromEntries(manifest.gateIds.map((id) => [id, true])),
});
assert.deepEqual(report.gateManifest, manifest);
assert.equal(report.gateResultMatrix.ready, true);
assert.equal(report.gateResultMatrix.passedCount, manifest.gateCount);
assert.equal(report.gateActionPlan.ready, true);
assert.equal(report.gateActionPlan.pendingCount, 0);
assert.equal(report.gateActionPlan.nextCommand, null);
assert.equal(report.gateActionPlan.shellScript, "# Project release gates are already marked passed.\n");
assert.deepEqual(report.gates.map(({ id }) => id), manifest.gateIds);
assert.equal(report.gates.length, manifest.gateCount);
assert.deepEqual(createProjectReleaseReadinessSummaryViewModel(report), {

View File

@@ -32,6 +32,7 @@ import {
createMachineSessionPersistenceDisplayViewModel,
createMachineSessionPersistenceRenderState,
createMachineSessionPersistenceSummary,
createProjectReleaseGateActionPlan,
createProjectReleaseGateManifest,
createProjectReleaseGateResultMatrix,
createProjectReleaseReadinessArtifactValidation,
@@ -86,6 +87,7 @@ const trackerText = readFileSync(resolve(root, "../PROJECT_COMPLETION_TRACKER.md
const requiredExports = [
["createLinuxCncIniSdk", createLinuxCncIniSdk],
["createLinuxCncInterpSdk", createLinuxCncInterpSdk],
["createProjectReleaseGateActionPlan", createProjectReleaseGateActionPlan],
["createProjectReleaseGateManifest", createProjectReleaseGateManifest],
["createProjectReleaseGateResultMatrix", createProjectReleaseGateResultMatrix],
["createProjectReleaseReadinessArtifactValidation", createProjectReleaseReadinessArtifactValidation],
@@ -211,6 +213,9 @@ assert.equal(releaseReadinessWaiting.ready, false);
assert.deepEqual(releaseReadinessWaiting.missing, ["project-release-gate"]);
assert.equal(releaseReadinessWaiting.simConfigInventory.unexpectedFail, 0);
assert.deepEqual(releaseReadinessWaiting.promotedBlockedFamilies, []);
assert.equal(releaseReadinessWaiting.gateActionPlan.apiName, "project-release-gate-action-plan");
assert.equal(releaseReadinessWaiting.gateActionPlan.pendingCount, 10);
assert.equal(releaseReadinessWaiting.gateActionPlan.nextGateId, "diff-check");
assert.equal(
releaseReadinessWaiting.rows.find(({ id }) => id === "blocked-runtime-families")?.value,
"L4-USER-M-PROCESS, L4-TOOL-DB, L4-PYTHON-REMAP",
@@ -222,6 +227,12 @@ assert.equal(releaseReadinessReady.ready, true);
assert.equal(releaseReadinessReady.releaseGate.passed, true);
assert.deepEqual(releaseReadinessReady.gateManifest, createProjectReleaseGateManifest());
assert.equal(releaseReadinessReady.gateResultMatrix.rows.length, 10);
assert.equal(createProjectReleaseGateActionPlan({
gateResultMatrix: releaseReadinessReady.gateResultMatrix,
}).ready, false);
assert.equal(createProjectReleaseGateActionPlan({
gateResults: Object.fromEntries(createProjectReleaseGateManifest().gateIds.map((id) => [id, true])),
}).shellScript, "# Project release gates are already marked passed.\n");
assert.equal(createProjectReleaseReadinessSummaryViewModel(releaseReadinessReady).statusLine, "Ready: No blocking reasons");
assert.equal(
createProjectReleaseGateResultMatrix({