fix: harden dev cd runtime postflight
Summary: - shared runtime durable readiness classifier for live-status and DEV postflight - structured postflight blockers propagated through dev-cd apply - contract coverage for ready queryResult, auth/schema/migration blockers, evidence persistence blockers, and bounded stdout Verification: - git diff --check - node --check changed scripts/tests/live-status modules - node --test scripts/src/dev-runtime-postflight.test.mjs scripts/src/dev-cd-apply.test.mjs web/hwlab-cloud-web/scripts/live-status-contract.test.mjs - node scripts/dev-runtime-postflight.mjs --check - node web/hwlab-cloud-web/scripts/check.mjs - npm run check No DEV apply/rollout/live health verification was run.
This commit is contained in:
@@ -4,6 +4,7 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { DEV_ENDPOINT, ENVIRONMENT_DEV } from "../../internal/protocol/index.mjs";
|
||||
import { classifyRuntimeDurableReadiness } from "../../web/hwlab-cloud-web/live-status.mjs";
|
||||
import {
|
||||
buildM3IoControlSourceReport,
|
||||
runM3IoControlLiveReport
|
||||
@@ -78,8 +79,16 @@ export async function buildDevRuntimePostflightReport(args = {}, options = {}) {
|
||||
blockerCount: sourceReport.summary?.status === "pass" ? 0 : 1
|
||||
};
|
||||
if (sourceReport.summary?.status !== "pass") {
|
||||
addBlocker(report, "contract_blocker", "m3-source-contract", "M3 postflight source contract is blocked.", {
|
||||
classification: sourceReport.summary?.classification ?? "unknown"
|
||||
addBlocker(report, {
|
||||
type: "contract_blocker",
|
||||
scope: "m3-source-contract",
|
||||
reason: "M3 postflight source contract is blocked.",
|
||||
impact: "DEV CD cannot rely on the M3 durable postflight harness contract.",
|
||||
safeNextAction: "Fix the source M3 IO control contract tests before running live DEV postflight.",
|
||||
retryable: true,
|
||||
evidence: {
|
||||
classification: sourceReport.summary?.classification ?? "unknown"
|
||||
}
|
||||
});
|
||||
}
|
||||
return report;
|
||||
@@ -227,6 +236,7 @@ async function defaultHttpGetJson(url, timeoutMs) {
|
||||
function summarizeApiPayload(check, response) {
|
||||
const json = response.json ?? {};
|
||||
const runtime = summarizeRuntime(json.runtime);
|
||||
const runtimeDurableReadiness = classifyRuntimeDurableReadiness(json, { requirePostgresAdapter: true });
|
||||
return {
|
||||
check,
|
||||
httpStatus: response.status,
|
||||
@@ -236,6 +246,7 @@ function summarizeApiPayload(check, response) {
|
||||
status: json.status ?? null,
|
||||
ready: json.ready === true,
|
||||
runtime,
|
||||
runtimeDurableReadiness,
|
||||
db: summarizeDb(json.db),
|
||||
readiness: {
|
||||
ready: json.readiness?.ready === true,
|
||||
@@ -372,23 +383,48 @@ function summarizeM3PersistenceContract(operations = []) {
|
||||
function addApiReadinessBlockers(report) {
|
||||
for (const [scope, payload] of [["api-health-live", report.apiHealth], ["api-v1", report.apiV1]]) {
|
||||
if (!payload.reachable) {
|
||||
addBlocker(report, "runtime_blocker", scope, `${payload.check} was not reachable.`, {
|
||||
httpStatus: payload.httpStatus,
|
||||
error: payload.error
|
||||
addBlocker(report, {
|
||||
type: "runtime_blocker",
|
||||
scope,
|
||||
reason: `${payload.check} was not reachable.`,
|
||||
impact: "DEV CD cannot prove cloud-api durable readiness, so M3 evidence persistence postflight cannot safely run.",
|
||||
safeNextAction: "Restore DEV cloud-api reachability on the repo-owned public endpoint, then rerun postflight.",
|
||||
retryable: true,
|
||||
evidence: {
|
||||
httpStatus: payload.httpStatus,
|
||||
error: payload.error
|
||||
}
|
||||
});
|
||||
continue;
|
||||
}
|
||||
if (!runtimeDurableReady(payload.runtime)) {
|
||||
addBlocker(report, "runtime_blocker", scope, `${payload.check} did not prove durable runtime readiness.`, {
|
||||
runtime: payload.runtime,
|
||||
blockerCodes: payload.blockerCodes
|
||||
if (payload.runtimeDurableReadiness?.ready !== true) {
|
||||
addBlocker(report, {
|
||||
type: "runtime_blocker",
|
||||
scope,
|
||||
reason: `${payload.check} did not prove durable runtime readiness: ${payload.runtimeDurableReadiness?.reason ?? "not ready"}.`,
|
||||
impact: payload.runtimeDurableReadiness?.impact ?? "M3 durable postflight cannot prove evidence persistence.",
|
||||
safeNextAction: payload.runtimeDurableReadiness?.safeNextAction ?? "Repair runtime durability through repo-owned DEV CD steps, then rerun postflight.",
|
||||
retryable: payload.runtimeDurableReadiness?.retryable !== false,
|
||||
evidence: {
|
||||
runtime: payload.runtime,
|
||||
runtimeDurableReadiness: payload.runtimeDurableReadiness,
|
||||
blockerCodes: payload.blockerCodes
|
||||
}
|
||||
});
|
||||
}
|
||||
if (scope === "api-v1" && !apiV1M3ContractReady(payload)) {
|
||||
addBlocker(report, "runtime_blocker", scope, "GET /v1 did not expose the controlled M3 IO route required by durable postflight.", {
|
||||
m3IoControl: payload.m3IoControl,
|
||||
requiredRoute: "/v1/m3/io",
|
||||
requiredContractVersion: "m3-io-control-v1"
|
||||
addBlocker(report, {
|
||||
type: "runtime_blocker",
|
||||
scope,
|
||||
reason: "GET /v1 did not expose the controlled M3 IO route required by durable postflight.",
|
||||
impact: "DEV CD cannot run the M3 true/false IO persistence smoke through the cloud-api controlled boundary.",
|
||||
safeNextAction: "Restore /v1 m3IoControl route metadata for /v1/m3/io without exposing direct frontend simulator access.",
|
||||
retryable: true,
|
||||
evidence: {
|
||||
m3IoControl: payload.m3IoControl,
|
||||
requiredRoute: "/v1/m3/io",
|
||||
requiredContractVersion: "m3-io-control-v1"
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -406,58 +442,96 @@ function apiV1M3ContractReady(payload) {
|
||||
|
||||
function addM3Blockers(report) {
|
||||
if (report.m3?.persistenceContract?.ready !== true) {
|
||||
addBlocker(report, "runtime_blocker", "m3-durable-postflight", "M3 DO1 true/false durable postflight did not prove persisted operation/audit/evidence for every step.", {
|
||||
persistenceContract: report.m3?.persistenceContract ?? null,
|
||||
trustedGreen: report.m3?.trustedGreen === true,
|
||||
operationCount: report.m3?.operationCount ?? 0
|
||||
addBlocker(report, {
|
||||
type: "runtime_blocker",
|
||||
scope: "m3-durable-postflight",
|
||||
reason: "M3 DO1 true/false durable postflight did not prove persisted operation/audit/evidence for every step.",
|
||||
impact: "A later rollout could appear ready while losing operation, audit, or evidence persistence for M3 hardware IO.",
|
||||
safeNextAction: "Inspect the failed M3 operation sequence, repair durable audit/evidence persistence, then rerun DEV runtime postflight.",
|
||||
retryable: true,
|
||||
evidence: {
|
||||
persistenceContract: report.m3?.persistenceContract ?? null,
|
||||
trustedGreen: report.m3?.trustedGreen === true,
|
||||
operationCount: report.m3?.operationCount ?? 0
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (report.m3?.trustedGreen !== true || report.m3?.status !== "pass") {
|
||||
addBlocker(report, "runtime_blocker", "m3-durable-postflight", "M3 true/false durable postflight did not produce trusted green persisted evidence.", {
|
||||
classification: report.m3?.classification ?? "unknown",
|
||||
trustedGreen: report.m3?.trustedGreen === true,
|
||||
operationCount: report.m3?.operationCount ?? 0
|
||||
addBlocker(report, {
|
||||
type: "runtime_blocker",
|
||||
scope: "m3-durable-postflight",
|
||||
reason: "M3 true/false durable postflight did not produce trusted green persisted evidence.",
|
||||
impact: "M3 durable readiness cannot be accepted because trusted evidence is not green.",
|
||||
safeNextAction: "Fix the M3 control path or trusted-record persistence and rerun DEV runtime postflight.",
|
||||
retryable: true,
|
||||
evidence: {
|
||||
classification: report.m3?.classification ?? "unknown",
|
||||
trustedGreen: report.m3?.trustedGreen === true,
|
||||
operationCount: report.m3?.operationCount ?? 0
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (const operation of report.m3.operations ?? []) {
|
||||
if (!operation.operationId || !operation.auditId || !operation.evidenceId || operation.evidenceState?.status !== "green" || operation.evidenceState?.durable !== true) {
|
||||
addBlocker(report, "runtime_blocker", "m3-durable-evidence", "M3 operation/audit/evidence durable fields were incomplete.", {
|
||||
operationIdPresent: Boolean(operation.operationId),
|
||||
auditIdPresent: Boolean(operation.auditId),
|
||||
evidenceIdPresent: Boolean(operation.evidenceId),
|
||||
evidenceState: operation.evidenceState
|
||||
addBlocker(report, {
|
||||
type: "runtime_blocker",
|
||||
scope: "m3-durable-evidence",
|
||||
reason: "M3 operation/audit/evidence durable fields were incomplete.",
|
||||
impact: "The M3 postflight cannot prove every IO operation is bound to persisted audit and evidence records.",
|
||||
safeNextAction: "Repair operation, audit, and evidence ID propagation before accepting DEV readiness.",
|
||||
retryable: true,
|
||||
evidence: {
|
||||
operationIdPresent: Boolean(operation.operationId),
|
||||
traceIdPresent: Boolean(operation.traceId),
|
||||
auditIdPresent: Boolean(operation.auditId),
|
||||
evidenceIdPresent: Boolean(operation.evidenceId),
|
||||
evidenceState: operation.evidenceState
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function runtimeDurableReady(runtime) {
|
||||
return runtime.adapter === "postgres" &&
|
||||
runtime.durable === true &&
|
||||
runtime.ready === true &&
|
||||
runtime.liveRuntimeEvidence === true;
|
||||
}
|
||||
|
||||
function addSafetyRefusal(report, summary) {
|
||||
report.safetyRefusal = true;
|
||||
addBlocker(report, "safety_refusal", "runtime-postflight-live-boundary", summary, {
|
||||
devOnly: true,
|
||||
prodAllowed: false,
|
||||
secretValuesPrinted: false
|
||||
addBlocker(report, {
|
||||
type: "safety_refusal",
|
||||
scope: "runtime-postflight-live-boundary",
|
||||
reason: summary,
|
||||
impact: "Live DEV reads and M3 writes are not allowed without explicit non-production confirmation.",
|
||||
safeNextAction: "Rerun with --live --confirm-dev --confirmed-non-production for DEV only, or use --check for source-only validation.",
|
||||
retryable: true,
|
||||
evidence: {
|
||||
devOnly: true,
|
||||
prodAllowed: false,
|
||||
secretValuesPrinted: false
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function addBlocker(report, type, scope, summary, evidence = {}) {
|
||||
function addBlocker(report, {
|
||||
type,
|
||||
scope,
|
||||
reason,
|
||||
impact,
|
||||
safeNextAction,
|
||||
retryable,
|
||||
evidence = {}
|
||||
}) {
|
||||
report.blockers.push({
|
||||
type,
|
||||
scope,
|
||||
status: "open",
|
||||
summary,
|
||||
reason: oneLine(reason),
|
||||
impact: oneLine(impact),
|
||||
safeNextAction: oneLine(safeNextAction),
|
||||
retryable: Boolean(retryable),
|
||||
summary: oneLine(`${reason} Impact: ${impact} Safe next action: ${safeNextAction}`),
|
||||
sourceIssue: issue,
|
||||
evidence
|
||||
});
|
||||
@@ -475,6 +549,10 @@ function finalizeReport(report) {
|
||||
return report;
|
||||
}
|
||||
|
||||
function oneLine(value) {
|
||||
return String(value ?? "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function buildSafety(args) {
|
||||
return {
|
||||
devOnly: true,
|
||||
|
||||
Reference in New Issue
Block a user