fix(sentinel): harden post-deploy report and ssh summaries

This commit is contained in:
Codex
2026-07-01 08:16:53 +00:00
parent 6906037ba4
commit 6b73dcd0c1
6 changed files with 273 additions and 14 deletions
+68 -2
View File
@@ -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 = [