新增 release artifact URL workflow SDK API

This commit is contained in:
2026-06-16 13:54:53 +08:00
parent d789920d7f
commit 9015de1961
9 changed files with 305 additions and 2 deletions

View File

@@ -62,6 +62,9 @@ continuation records are in `../text14.txt`.
`createProjectReleaseReadinessArtifactJsonWorkflow()` is the one-call
non-throwing JSON text workflow for dashboards that need parse, validation,
summary, and action-plan output from the readiness artifact.
`loadProjectReleaseReadinessArtifactUrlWorkflow()` provides the same
SDK-side workflow from an artifact URL and caller-provided fetch
implementation, including fetch status and next-command output.
Browser shells can also pass the JSON to
`linuxCncIniPanelWorkflowOverviewApi.validateWorkflowOverviewReleaseReadinessArtifactJson()`
through the workflow overview iframe and receive stable validation rows. They

View File

@@ -74,6 +74,7 @@ import {
isSupportedIniPanelLaunchApiManifest,
isSupportedIniPanelShellIntegrationManifest,
loadIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrl,
loadProjectReleaseReadinessArtifactUrlWorkflow,
loadMachineSessionFromOpfs,
loadMachineSessionSnapshot,
loadMachineTextFiles,
@@ -243,6 +244,10 @@ one-call wrapper for dashboards that receive artifact JSON text. It parses,
validates, builds the validation summary, and builds the action plan. Invalid
JSON returns a blocked workflow with `parsed: false` and the same next-command
action plan instead of throwing.
`loadProjectReleaseReadinessArtifactUrlWorkflow()` is the SDK-side URL workflow
for callers that supply a `fetch` implementation. It fetches the artifact URL,
runs the JSON workflow, and returns fetch status, validation, summary, and
action-plan output without executing release gates.
## OPFS/session persistence exports

View File

@@ -10,6 +10,7 @@ export {
createProjectReleaseReadinessArtifactValidationSummaryViewModel,
createProjectReleaseReadinessReport,
createProjectReleaseReadinessSummaryViewModel,
loadProjectReleaseReadinessArtifactUrlWorkflow,
parseProjectReleaseReadinessArtifactJson,
} from "./project-release-readiness.js";
export {

View File

@@ -518,6 +518,162 @@ export function createProjectReleaseReadinessArtifactJsonWorkflow({ artifactJson
}
}
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 createProjectReleaseReadinessArtifactValidation(artifact = {}) {
const artifactObject = objectOrEmpty(artifact);
const releaseGate = objectOrEmpty(artifactObject.releaseGate);

View File

@@ -41,6 +41,7 @@ for (const phrase of [
"createProjectReleaseReadinessArtifactValidation()",
"createProjectReleaseReadinessArtifactValidationActionPlan()",
"createProjectReleaseReadinessArtifactValidationSummaryViewModel()",
"loadProjectReleaseReadinessArtifactUrlWorkflow()",
"embedded gate manifest, result matrix, and action plan",
"validateWorkflowOverviewReleaseReadinessArtifactJson()",
"loadWorkflowOverviewReleaseReadinessArtifactUrl()",
@@ -119,6 +120,7 @@ for (const phrase of [
"gateResultMatrixReady",
"gateActionPlanReady",
"loadIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrl()",
"loadProjectReleaseReadinessArtifactUrlWorkflow()",
"build/project-release-readiness.json",
"INI panel shell handoff exports",
"createIniPanelShellApiSurfaceInventory()",

View File

@@ -10,6 +10,7 @@ import {
createProjectReleaseReadinessArtifactValidationSummaryViewModel,
createProjectReleaseReadinessReport,
createProjectReleaseReadinessSummaryViewModel,
loadProjectReleaseReadinessArtifactUrlWorkflow,
} from "../../../runtime/sdk/src/index.js";
const manifest = createProjectReleaseGateManifest();
@@ -184,6 +185,14 @@ const artifactValidationActionPlan = createProjectReleaseReadinessArtifactValida
const artifactJsonWorkflow = createProjectReleaseReadinessArtifactJsonWorkflow({
artifactJson: JSON.stringify(report),
});
const artifactUrlWorkflow = await loadProjectReleaseReadinessArtifactUrlWorkflow({
artifactUrl: "/project-release-readiness.json",
fetchRef: async () => ({
ok: true,
status: 200,
text: async () => JSON.stringify(report),
}),
});
assert.equal(
artifactValidationSummary.apiName,
"project-release-readiness-artifact-validation-summary-view-model",
@@ -249,6 +258,26 @@ assert.deepEqual(
["next-command", "none"],
],
);
assert.equal(artifactUrlWorkflow.apiName, "project-release-readiness-artifact-url-workflow");
assert.equal(artifactUrlWorkflow.workflowVersion, 1);
assert.equal(artifactUrlWorkflow.phase, "ready");
assert.equal(artifactUrlWorkflow.ready, true);
assert.equal(artifactUrlWorkflow.artifactUrl, "/project-release-readiness.json");
assert.equal(artifactUrlWorkflow.fetched, true);
assert.equal(artifactUrlWorkflow.httpStatus, 200);
assert.equal(artifactUrlWorkflow.fetchError, null);
assert.equal(artifactUrlWorkflow.jsonWorkflow.ready, true);
assert.equal(artifactUrlWorkflow.validation.ready, true);
assert.equal(artifactUrlWorkflow.summaryViewModel.statusLine, "Ready: Artifact evidence complete");
assert.equal(artifactUrlWorkflow.actionPlan.commandCount, 0);
assert.deepEqual(
artifactUrlWorkflow.rows.map(({ id, value }) => [id, value]),
[
["fetch", "ready"],
["json-workflow", "ready"],
["next-command", "none"],
],
);
const staleArtifactValidationSummary = createProjectReleaseReadinessArtifactValidationSummaryViewModel(
createProjectReleaseReadinessArtifactValidation({}),
);
@@ -260,6 +289,18 @@ const staleArtifactJsonWorkflow = createProjectReleaseReadinessArtifactJsonWorkf
const invalidArtifactJsonWorkflow = createProjectReleaseReadinessArtifactJsonWorkflow({
artifactJson: "{not-json",
});
const missingArtifactUrlWorkflow = await loadProjectReleaseReadinessArtifactUrlWorkflow({
artifactUrl: "",
fetchRef: null,
});
const failedArtifactUrlWorkflow = await loadProjectReleaseReadinessArtifactUrlWorkflow({
artifactUrl: "/missing.json",
fetchRef: async () => ({
ok: false,
status: 404,
text: async () => "",
}),
});
assert.equal(staleArtifactValidationSummary.phase, "blocked");
assert.match(staleArtifactValidationSummary.statusLine, /^Blocked: apiName, reportVersion/);
assert.equal(staleArtifactValidationSummary.rows.find(({ id }) => id === "gate-action-plan")?.value, "missing");
@@ -304,5 +345,17 @@ assert.deepEqual(
["next-command", "wasm-port/tests/host/verify_project_release_gate.sh"],
],
);
assert.equal(missingArtifactUrlWorkflow.ready, false);
assert.equal(missingArtifactUrlWorkflow.fetched, false);
assert.deepEqual(missingArtifactUrlWorkflow.missing.slice(0, 2), ["artifact-url", "fetch"]);
assert.equal(failedArtifactUrlWorkflow.ready, false);
assert.equal(failedArtifactUrlWorkflow.fetched, false);
assert.equal(failedArtifactUrlWorkflow.httpStatus, 404);
assert.match(failedArtifactUrlWorkflow.fetchError, /404/);
assert.equal(failedArtifactUrlWorkflow.missing[0], "artifact-fetch");
assert.equal(
failedArtifactUrlWorkflow.actionPlan.nextCommand,
"wasm-port/tests/host/verify_project_release_gate.sh",
);
console.log("project_release_gate_manifest_node_smoke=ok");

View File

@@ -56,6 +56,7 @@ import {
isSupportedIniPanelLaunchApiManifest,
isSupportedIniPanelShellIntegrationManifest,
loadIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrl,
loadProjectReleaseReadinessArtifactUrlWorkflow,
loadMachineSessionFromOpfs,
loadMachineSessionSnapshot,
loadMachineTextFiles,
@@ -116,6 +117,7 @@ const requiredExports = [
"loadIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrl",
loadIniPanelShellWorkflowOverviewReleaseReadinessArtifactUrl,
],
["loadProjectReleaseReadinessArtifactUrlWorkflow", loadProjectReleaseReadinessArtifactUrlWorkflow],
["planIniFileContextStaging", planIniFileContextStaging],
["planSimConfigStaging", planSimConfigStaging],
["analyzeIniRuntimeBoundaries", analyzeIniRuntimeBoundaries],
@@ -355,6 +357,24 @@ assert.equal(
createProjectReleaseReadinessArtifactJsonWorkflow({ artifactJson: "not-json" }).parsed,
false,
);
assert.equal(
(await loadProjectReleaseReadinessArtifactUrlWorkflow({
artifactUrl: "/project-release-readiness.json",
fetchRef: async () => ({
ok: true,
status: 200,
text: async () => JSON.stringify(releaseReadinessArtifact),
}),
})).summaryViewModel.statusLine,
"Ready: Artifact evidence complete",
);
assert.equal(
(await loadProjectReleaseReadinessArtifactUrlWorkflow({
artifactUrl: "/missing.json",
fetchRef: async () => ({ ok: false, status: 404, text: async () => "" }),
})).missing[0],
"artifact-fetch",
);
assert.deepEqual(
createProjectReleaseReadinessArtifactValidation({}).missing,
[