From 674b59db76d94696b34ad54fd9ab86bdf43941ab Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 21 Jun 2026 18:09:40 +0000 Subject: [PATCH] fix: summarize job status output --- scripts/cli.ts | 9 ++++-- scripts/src/jobs.ts | 67 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/scripts/cli.ts b/scripts/cli.ts index 46a6c479..1db70e7c 100644 --- a/scripts/cli.ts +++ b/scripts/cli.ts @@ -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 { } 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") { diff --git a/scripts/src/jobs.ts b/scripts/src/jobs.ts index d5b90459..cf38327b 100644 --- a/scripts/src/jobs.ts +++ b/scripts/src/jobs.ts @@ -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): 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;