fix(sentinel): harden post-deploy report and ssh summaries
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-27-p11-monitor-web-observability-dashboard.
|
||||
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-07-01-p15-cadence-otel.
|
||||
// Responsibility: P5 web-probe sentinel service validation, maintenance, report and dashboard commands.
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import type { CommandResult } from "./command";
|
||||
import { runCommand } from "./command";
|
||||
import { repoRoot } from "./config";
|
||||
@@ -14,6 +15,7 @@ import {
|
||||
compactSentinelServiceBodyJson,
|
||||
mergeWarnings,
|
||||
numberAt,
|
||||
numberAtNullable,
|
||||
parseJsonObject,
|
||||
pickFields,
|
||||
record,
|
||||
@@ -229,13 +231,18 @@ export function runSentinelReport(state: SentinelCicdState, options: Extract<Web
|
||||
if (options.sampleSeq !== null) query.set("sampleSeq", String(options.sampleSeq));
|
||||
const report = callSentinelService(state, "GET", `/api/report?${query.toString()}`, null, options.timeoutSeconds);
|
||||
const body = record(report.bodyJson);
|
||||
const renderedText = typeof body.renderedText === "string" ? body.renderedText : renderReportResult({ command, node: state.spec.nodeId, lane: state.spec.lane, report, valuesRedacted: true });
|
||||
const rawPayload = Object.keys(body).length > 0 ? body : report;
|
||||
if (options.full) return rendered(report.ok && body.ok !== false, command, JSON.stringify(rawPayload, null, 2));
|
||||
if (options.raw) {
|
||||
const artifactSummary = readSentinelReportArtifactSummary(state, body, Math.min(options.timeoutSeconds, 55));
|
||||
return rendered(report.ok && body.ok !== false, command, JSON.stringify(compactSentinelReportRawPayload(state, body, report, artifactSummary), null, 2));
|
||||
}
|
||||
if (options.view === "summary") {
|
||||
const artifactSummary = readSentinelReportArtifactSummary(state, body, Math.min(options.timeoutSeconds, 55));
|
||||
const payload = compactSentinelReportRawPayload(state, body, report, artifactSummary);
|
||||
return rendered(report.ok && body.ok !== false, command, renderSentinelReportSummary(payload, state));
|
||||
}
|
||||
const renderedText = typeof body.renderedText === "string" ? body.renderedText : renderReportResult({ command, node: state.spec.nodeId, lane: state.spec.lane, report, valuesRedacted: true });
|
||||
return rendered(report.ok && body.ok !== false, command, renderedText);
|
||||
}
|
||||
|
||||
@@ -374,6 +381,40 @@ function isSafeSentinelReportStateDir(value: string): boolean {
|
||||
return value.startsWith(".state/web-observe/") && !value.includes("\0") && !value.includes("..") && !value.startsWith("/");
|
||||
}
|
||||
|
||||
function renderSentinelReportSummary(payload: Record<string, unknown>, state: SentinelCicdState): string {
|
||||
const run = record(payload.run);
|
||||
const summary = record(payload.summary);
|
||||
const artifactSummary = record(payload.artifactSummary);
|
||||
const findings = Array.isArray(payload.findings) ? payload.findings.map(record) : [];
|
||||
const reportSha = stringAtNullable(run, "reportJsonSha256") ?? stringAtNullable(artifactSummary, "reportJsonSha256");
|
||||
const findingCount = run.findingCount ?? findings.length;
|
||||
const analyzerArtifactCount = numberAtNullable(artifactSummary, "artifactCount") ?? numberAtNullable(record(artifactSummary.counts), "artifacts");
|
||||
const runArtifactCount = numberAtNullable(run, "artifactCount");
|
||||
const artifactCount = runArtifactCount === null || runArtifactCount === 0 && analyzerArtifactCount !== null
|
||||
? analyzerArtifactCount ?? "-"
|
||||
: runArtifactCount;
|
||||
const publicOrigin = stringAtNullable(state.publicExposure, "publicBaseUrl") ?? "-";
|
||||
const findingRows = findings.length === 0
|
||||
? "-"
|
||||
: table(["ID", "SEVERITY", "COUNT", "SUMMARY"], findings.slice(0, 12).map((item) => [
|
||||
stringAtNullable(item, "id") ?? "-",
|
||||
stringAtNullable(item, "severity") ?? "-",
|
||||
item.count ?? "-",
|
||||
reportText(item.summary, 120) ?? "-",
|
||||
]));
|
||||
return [
|
||||
"Web Probe Sentinel Quick Verify",
|
||||
"=======================================================",
|
||||
`run=${run.id ?? "-"} scenario=${run.scenarioId ?? "-"} observer=${run.observerId ?? "-"}`,
|
||||
`status=${run.status ?? summary.status ?? "-"} reason=${summary.reason ?? "-"} failure=${summary.failure ?? "-"}`,
|
||||
`report=${reportSha ?? "-"} artifacts=${artifactCount} findings=${findingCount}`,
|
||||
`publicOrigin=${publicOrigin}`,
|
||||
"",
|
||||
"Findings",
|
||||
findingRows,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function compactRootCauseSignals(value: unknown): Record<string, unknown> | null {
|
||||
const item = record(value);
|
||||
const keys = [
|
||||
@@ -960,7 +1001,9 @@ function callSentinelService(state: SentinelCicdState, method: "GET" | "POST", p
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||||
result = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "sh", "--", script], repoRoot, { timeoutMs: attemptTimeoutSeconds * 1000 });
|
||||
parsed = parseJsonObject(result.stdout);
|
||||
attempts.push({ attempt, ...compactCommand(result), parsedOk: parsed !== null, valuesRedacted: true });
|
||||
const recovered = parsed === null ? recoverTruncatedSshStdoutJson(result) : null;
|
||||
if (recovered !== null) parsed = recovered.parsed;
|
||||
attempts.push({ attempt, ...compactCommand(result), parsedOk: parsed !== null, parsedFromDump: recovered !== null, dumpPath: recovered?.dumpPath ?? null, valuesRedacted: true });
|
||||
if (result.exitCode === 0) break;
|
||||
}
|
||||
const compactBodyJson = compactSentinelServiceBodyJson(parsed);
|
||||
@@ -981,6 +1024,29 @@ function callSentinelService(state: SentinelCicdState, method: "GET" | "POST", p
|
||||
};
|
||||
}
|
||||
|
||||
function recoverTruncatedSshStdoutJson(result: CommandResult): { parsed: Record<string, unknown>; dumpPath: string } | null {
|
||||
const dumpPath = sshStdoutDumpPathFromStderr(result.stderr);
|
||||
if (dumpPath === null || !existsSync(dumpPath)) return null;
|
||||
const parsed = parseJsonObject(readFileSync(dumpPath, "utf8"));
|
||||
return parsed === null ? null : { parsed, dumpPath };
|
||||
}
|
||||
|
||||
function sshStdoutDumpPathFromStderr(value: string): string | null {
|
||||
for (const rawLine of value.split(/\r?\n/u)) {
|
||||
const line = rawLine.trim();
|
||||
const prefix = "UNIDESK_SSH_STDOUT_TRUNCATED ";
|
||||
if (!line.startsWith(prefix)) continue;
|
||||
try {
|
||||
const payload = JSON.parse(line.slice(prefix.length)) as unknown;
|
||||
const dumpPath = stringAtNullable(record(payload), "dumpPath");
|
||||
if (dumpPath !== null) return dumpPath;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function callSentinelServiceDirect(method: "GET" | "POST", pathWithQuery: string, body: Record<string, unknown> | null, timeoutSeconds: number, url: string): Record<string, unknown> {
|
||||
const bodyB64 = Buffer.from(body === null ? "" : JSON.stringify(body), "utf8").toString("base64");
|
||||
const fetchScript = [
|
||||
|
||||
Reference in New Issue
Block a user