fix: improve hwlab v02 cli observability
This commit is contained in:
+117
-2
@@ -24,6 +24,19 @@ export interface JobRecord {
|
||||
note: string;
|
||||
}
|
||||
|
||||
export interface JobProgressSummary {
|
||||
kind: "hwlab-v02-trigger" | "generic";
|
||||
stage: string | null;
|
||||
stageStatus: string | null;
|
||||
sourceCommit: string | null;
|
||||
pipelineRun: string | null;
|
||||
pipelineCreated: boolean | null;
|
||||
lastEventAt: string | null;
|
||||
eventsObserved: number;
|
||||
summary: string;
|
||||
nextCommand: string | null;
|
||||
}
|
||||
|
||||
export interface StartJobOptions {
|
||||
runner?: "local" | "docker";
|
||||
dockerImage?: string;
|
||||
@@ -142,6 +155,7 @@ export async function runJob(id: string): Promise<JobRecord> {
|
||||
}
|
||||
|
||||
export function jobWithTail(job: JobRecord, maxBytes = 12000): JobRecord & {
|
||||
progress: JobProgressSummary;
|
||||
tailPolicy: {
|
||||
requestedTailBytes: number;
|
||||
stdoutBytes: number;
|
||||
@@ -155,8 +169,12 @@ export function jobWithTail(job: JobRecord, maxBytes = 12000): JobRecord & {
|
||||
} {
|
||||
const stdoutBytes = existsSync(job.stdoutFile) ? statSync(job.stdoutFile).size : 0;
|
||||
const stderrBytes = existsSync(job.stderrFile) ? statSync(job.stderrFile).size : 0;
|
||||
const progressTailBytes = Math.max(maxBytes, 96_000);
|
||||
const stdoutProgressTail = tailFile(job.stdoutFile, progressTailBytes);
|
||||
const stderrProgressTail = tailFile(job.stderrFile, progressTailBytes);
|
||||
return {
|
||||
...job,
|
||||
progress: summarizeJobProgress(job, progressTailBytes, { stdoutTail: stdoutProgressTail, stderrTail: stderrProgressTail }),
|
||||
tailPolicy: {
|
||||
requestedTailBytes: maxBytes,
|
||||
stdoutBytes,
|
||||
@@ -165,11 +183,107 @@ export function jobWithTail(job: JobRecord, maxBytes = 12000): JobRecord & {
|
||||
stderrTruncated: stderrBytes > maxBytes,
|
||||
fullLogPaths: { stdoutFile: job.stdoutFile, stderrFile: job.stderrFile },
|
||||
},
|
||||
stdoutTail: tailFile(job.stdoutFile, maxBytes),
|
||||
stderrTail: tailFile(job.stderrFile, maxBytes),
|
||||
stdoutTail: tailTextByBytes(stdoutProgressTail, maxBytes),
|
||||
stderrTail: tailTextByBytes(stderrProgressTail, maxBytes),
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeJobProgress(job: JobRecord, maxBytes = 96_000, tails?: { stdoutTail: string; stderrTail: string }): JobProgressSummary {
|
||||
const knownWorkflow = job.name === "hwlab_g14_v02_trigger_current";
|
||||
if (!knownWorkflow && tails === undefined) return genericJobProgress(job);
|
||||
const progressTailBytes = Math.max(4096, Math.floor(maxBytes));
|
||||
const stderrTail = tails?.stderrTail ?? tailFile(job.stderrFile, progressTailBytes);
|
||||
const stdoutTail = tails?.stdoutTail ?? tailFile(job.stdoutFile, progressTailBytes);
|
||||
const events = parseJsonLineEvents(stderrTail, "hwlab.v02.trigger.progress");
|
||||
const lastEvent = events.at(-1) ?? {};
|
||||
const stage = stringField(lastEvent.stage);
|
||||
const stageStatus = stringField(lastEvent.status);
|
||||
const sourceCommit = stringField(lastEvent.sourceCommit) ?? firstMatch(stdoutTail, /"sourceCommit"\s*:\s*"([0-9a-f]{40})"/iu);
|
||||
const pipelineRun = stringField(lastEvent.pipelineRun) ?? firstMatch(stdoutTail, /"pipelineRun"\s*:\s*"([^"]+)"/u);
|
||||
const pipelineCreated = /pipelinerun\.tekton\.dev\/[^ \n]+ created/u.test(stdoutTail)
|
||||
? true
|
||||
: stage === "create-pipelinerun" && stageStatus === "failed"
|
||||
? false
|
||||
: null;
|
||||
const lastEventAt = stringField(lastEvent.at);
|
||||
const kind = events.length > 0 || knownWorkflow ? "hwlab-v02-trigger" : "generic";
|
||||
const nextCommand = pipelineRun
|
||||
? `bun scripts/cli.ts hwlab g14 control-plane status --lane v02`
|
||||
: job.status === "running"
|
||||
? `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`
|
||||
: null;
|
||||
const summary = kind === "hwlab-v02-trigger"
|
||||
? [
|
||||
job.status,
|
||||
stage ? `${stage}${stageStatus ? `:${stageStatus}` : ""}` : "stage:unknown",
|
||||
sourceCommit ? `source=${sourceCommit.slice(0, 12)}` : null,
|
||||
pipelineRun ? `pipelineRun=${pipelineRun}` : null,
|
||||
pipelineCreated === true ? "created" : pipelineCreated === false ? "create-failed" : null,
|
||||
].filter(Boolean).join(" ")
|
||||
: `${job.status}${job.exitCode === null ? "" : ` exit=${job.exitCode}`}`;
|
||||
return {
|
||||
kind,
|
||||
stage,
|
||||
stageStatus,
|
||||
sourceCommit,
|
||||
pipelineRun,
|
||||
pipelineCreated,
|
||||
lastEventAt,
|
||||
eventsObserved: events.length,
|
||||
summary,
|
||||
nextCommand,
|
||||
};
|
||||
}
|
||||
|
||||
function genericJobProgress(job: JobRecord): JobProgressSummary {
|
||||
return {
|
||||
kind: "generic",
|
||||
stage: null,
|
||||
stageStatus: null,
|
||||
sourceCommit: null,
|
||||
pipelineRun: null,
|
||||
pipelineCreated: null,
|
||||
lastEventAt: null,
|
||||
eventsObserved: 0,
|
||||
summary: `${job.status}${job.exitCode === null ? "" : ` exit=${job.exitCode}`}`,
|
||||
nextCommand: job.status === "running" ? `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000` : null,
|
||||
};
|
||||
}
|
||||
|
||||
function tailTextByBytes(text: string, maxBytes: number): string {
|
||||
const safeMaxBytes = Math.max(0, Math.floor(maxBytes));
|
||||
if (safeMaxBytes === 0) return "";
|
||||
const buffer = Buffer.from(text, "utf8");
|
||||
if (buffer.length <= safeMaxBytes) return text;
|
||||
return buffer.subarray(buffer.length - safeMaxBytes).toString("utf8");
|
||||
}
|
||||
|
||||
function parseJsonLineEvents(text: string, eventName: string): Record<string, unknown>[] {
|
||||
const events: Record<string, unknown>[] = [];
|
||||
for (const line of text.split(/\r?\n/u)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith("{")) continue;
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed) as unknown;
|
||||
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && (parsed as Record<string, unknown>).event === eventName) {
|
||||
events.push(parsed as Record<string, unknown>);
|
||||
}
|
||||
} catch {
|
||||
// Ignore non-JSON stderr lines; the raw tail remains available in stderrTail.
|
||||
}
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
function stringField(value: unknown): string | null {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
||||
}
|
||||
|
||||
function firstMatch(text: string, pattern: RegExp): string | null {
|
||||
const match = pattern.exec(text);
|
||||
return typeof match?.[1] === "string" && match[1].length > 0 ? match[1] : null;
|
||||
}
|
||||
|
||||
export interface JobListOptions {
|
||||
limit?: number;
|
||||
includeCommand?: boolean;
|
||||
@@ -182,6 +296,7 @@ export function listJobsSummary(options: JobListOptions = {}): unknown {
|
||||
id: job.id,
|
||||
name: job.name,
|
||||
status: job.status,
|
||||
progress: summarizeJobProgress(job, 32_000),
|
||||
runner: job.runner,
|
||||
runnerPid: job.runnerPid ?? null,
|
||||
runnerContainer: job.runnerContainer ?? null,
|
||||
|
||||
Reference in New Issue
Block a user