fix: summarize job status output

This commit is contained in:
Codex
2026-06-21 18:09:40 +00:00
parent 9e8499824f
commit 674b59db76
2 changed files with 74 additions and 2 deletions
+7 -2
View File
@@ -2,7 +2,7 @@ import { readConfig } from "./src/config";
import { debugDispatch, debugHealth, debugSshPool, debugTask, isDebugDispatchCommand, type DebugDispatchCommand } from "./src/debug";
import { isRebuildableService, rebuildService, restartService, stackLogs, stackStatus, startStack, stopStack, unsupportedRebuildService, unsupportedRestartService } from "./src/docker";
import { emitError, emitJson, emitText, isRenderedCliResult } from "./src/output";
import { cancelJob, jobWithTail, listJobs, listJobsSummary, readJob, runJob } from "./src/jobs";
import { cancelJob, jobWithTail, listJobs, listJobsSummary, readJob, renderJobStatusSummary, runJob } from "./src/jobs";
import { checkHelp, parseCheckOptions, runChecks, runRecoveryGuardrailsCheck } from "./src/check";
import { runSsh } from "./src/ssh";
import { autoRemoteCiPublishUserServiceDryRunPlan, extractRemoteCliOptions, runRemoteCli } from "./src/remote";
@@ -551,7 +551,12 @@ async function main(): Promise<void> {
}
if (sub === "status") {
const id = third === "latest" || third === undefined ? latestJobId() : third;
emitJson(commandName, { job: jobWithTail(readJob(id), boundedNumberOption("--tail-bytes", 12000, 500_000)) });
const job = jobWithTail(readJob(id), boundedNumberOption("--tail-bytes", 12000, 500_000));
if (args.includes("--full") || args.includes("--raw")) {
emitJson(commandName, { job });
return;
}
emitText(renderJobStatusSummary(job).renderedText, commandName);
return;
}
if (sub === "cancel") {
+67
View File
@@ -4,6 +4,7 @@ import { join } from "node:path";
import { repoRoot, rootPath } from "./config";
import { runCommandToFiles, tailFile } from "./command";
import { hwlabDefaultRuntimeTarget } from "./hwlab-node-lanes";
import type { RenderedCliResult } from "./output";
export type JobStatus = "queued" | "running" | "succeeded" | "failed" | "canceled";
@@ -228,6 +229,47 @@ export function jobWithTail(job: JobRecord, maxBytes = 12000): JobRecord & {
};
}
export function renderJobStatusSummary(job: ReturnType<typeof jobWithTail>): RenderedCliResult {
const progress = job.progress;
const warnings = Array.isArray(progress.warnings) ? progress.warnings : [];
const lines = [
jobStatusTable(
["JOB", "STATUS", "EXIT", "KIND", "STAGE", "STAGE_STATUS", "ELAPSED", "LAST_EVENT_AGE"],
[[job.id, job.status, job.exitCode ?? "-", progress.kind, progress.stage ?? "-", progress.stageStatus ?? "-", secondsText(progress.elapsedSeconds), secondsText(progress.lastEventAgeSeconds)]],
),
"",
jobStatusTable(
["SOURCE", "PIPELINE", "CREATED", "EVENTS", "SLOW", "SUMMARY"],
[[progress.sourceCommit ? String(progress.sourceCommit).slice(0, 12) : "-", progress.pipelineRun ?? "-", progress.pipelineCreated ?? "-", progress.eventsObserved, progress.slow, progress.summary]],
),
"",
warnings.length === 0 ? "WARNINGS\n-" : ["WARNINGS", ...warnings.slice(0, 6).map((item) => `- ${jobStatusCell(item, 220)}`)].join("\n"),
"",
jobStatusTable(
["TAIL", "BYTES", "TRUNCATED", "PATH"],
[
["stdout", job.tailPolicy.stdoutBytes, job.tailPolicy.stdoutTruncated, job.tailPolicy.fullLogPaths.stdoutFile],
["stderr", job.tailPolicy.stderrBytes, job.tailPolicy.stderrTruncated, job.tailPolicy.fullLogPaths.stderrFile],
],
),
"",
jobTailSection("STDOUT_TAIL", job.stdoutTail),
"",
jobTailSection("STDERR_TAIL", job.stderrTail),
"",
"NEXT",
` status: bun scripts/cli.ts job status ${job.id} --tail-bytes ${job.tailPolicy.requestedTailBytes}`,
progress.nextCommand ? ` progress: ${progress.nextCommand}` : null,
` full: bun scripts/cli.ts job status ${job.id} --tail-bytes ${job.tailPolicy.requestedTailBytes} --full`,
].filter((line): line is string => line !== null);
return {
ok: job.status !== "failed",
command: "job status",
contentType: "text/plain",
renderedText: `${lines.join("\n")}\n`,
};
}
function summarizeJobProgress(job: JobRecord, maxBytes = 96_000, tails?: { stdoutTail: string; stderrTail: string }): JobProgressSummary {
const nowMs = Date.now();
const knownWorkflow = job.name === "hwlab_g14_v02_trigger_current";
@@ -898,6 +940,31 @@ function tailTextByBytes(text: string, maxBytes: number): string {
return buffer.subarray(buffer.length - safeMaxBytes).toString("utf8");
}
function secondsText(value: number | null): string {
return value === null ? "-" : `${value}s`;
}
function jobStatusCell(value: unknown, maxLength = 96): string {
if (value === null || value === undefined) return "-";
const text = typeof value === "string" ? value : String(value);
const compact = text.replace(/\s+/gu, " ").trim();
if (compact.length === 0) return "-";
if (compact.length <= maxLength) return compact;
return `${compact.slice(0, Math.max(1, maxLength - 1))}...`;
}
function jobStatusTable(headers: string[], rows: unknown[][]): string {
const stringRows = rows.map((row) => row.map((value) => jobStatusCell(value)));
const widths = headers.map((header, index) => Math.max(header.length, ...stringRows.map((row) => row[index]?.length ?? 0)));
const renderRow = (row: string[]) => row.map((cell, index) => cell.padEnd(widths[index] ?? cell.length)).join(" ").trimEnd();
return [renderRow(headers), ...stringRows.map(renderRow)].join("\n");
}
function jobTailSection(title: string, text: string): string {
const lines = text.trim().split(/\r?\n/u).filter((line) => line.trim().length > 0).slice(-6).map((line) => ` ${jobStatusCell(line, 220)}`);
return lines.length === 0 ? `${title}\n-` : [title, ...lines].join("\n");
}
function compactJobStdoutTail(job: JobRecord, progress: JobProgressSummary, rawTail: string, requestedTailBytes: number): string {
if (progress.kind !== "hwlab-runtime-lane-trigger") return rawTail;
if (Buffer.byteLength(rawTail, "utf8") <= 4096 && !rawTail.includes("\"expected\"") && !rawTail.includes("stdoutTail")) return rawTail;