From 424d99b66ea1ace367e1beaa6c49cd02ad196c10 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 4 Jun 2026 17:36:25 +0000 Subject: [PATCH] fix: make hwlab monitor status read-only --- scripts/hwlab-g14-contract-test.ts | 8 +++- scripts/src/hwlab-g14.ts | 65 +++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/scripts/hwlab-g14-contract-test.ts b/scripts/hwlab-g14-contract-test.ts index e230a9be..c4749c9d 100644 --- a/scripts/hwlab-g14-contract-test.ts +++ b/scripts/hwlab-g14-contract-test.ts @@ -40,9 +40,10 @@ assertCondition( ); assertCondition( hwlabHelpUsage.some((line) => line.includes("monitor-prs --lane v02")) + && hwlabHelpUsage.some((line) => line.includes("monitor-prs --lane v02 --status")) && JSON.stringify(hwlabG14Help()).includes("v02-pr-comment-signatures.json") && JSON.stringify(hwlabG14Help()).includes("latest-only"), - "v0.2 PR monitor help must expose the auto CI/CD lane, latest-only CD, and dedupe comment state", + "v0.2 PR monitor help must expose the auto CI/CD lane, status query, latest-only CD, and dedupe comment state", hwlabG14Help(), ); @@ -291,6 +292,11 @@ assertCondition( && !sourceText.includes("date +%s%3N"), "v0.2 web asset elapsed timing must avoid non-portable date +%s%3N in k3s probe shells", ); +assertCondition( + sourceText.includes("if (args.includes(\"--status\")) return monitorStatus(options);") + && sourceText.indexOf("if (args.includes(\"--status\")) return monitorStatus(options);") < sourceText.indexOf("const command = [\"bun\", \"scripts/cli.ts\", \"hwlab\", \"g14\", \"monitor-prs\""), + "monitor-prs --status must be a read-only query before async monitor startJob", +); const staleSuccessAlignment = v02CommitAlignment({ expectedSourceHead: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", diff --git a/scripts/src/hwlab-g14.ts b/scripts/src/hwlab-g14.ts index 5c95ec23..71a68341 100644 --- a/scripts/src/hwlab-g14.ts +++ b/scripts/src/hwlab-g14.ts @@ -3,7 +3,7 @@ import { dirname, join } from "node:path"; import { createHash } from "node:crypto"; import { repoRoot, rootPath, type Config } from "./config"; import { runCommand } from "./command"; -import { startJob } from "./jobs"; +import { readJob, startJob } from "./jobs"; const HWLAB_REPO = "pikasTech/HWLAB"; const G14_SOURCE_BRANCH = "G14"; @@ -5347,6 +5347,7 @@ export function hwlabG14Help(): Record { usage: [ "bun scripts/cli.ts hwlab g14 monitor-prs", "bun scripts/cli.ts hwlab g14 monitor-prs --lane v02", + "bun scripts/cli.ts hwlab g14 monitor-prs --lane v02 --status", "bun scripts/cli.ts hwlab g14 monitor-prs --once --dry-run", "bun scripts/cli.ts hwlab g14 monitor-prs --lane v02 --once --dry-run", "bun scripts/cli.ts hwlab g14 record-rollout --pr [--source-commit sha]", @@ -5405,6 +5406,67 @@ export function hwlabG14Help(): Record { }; } +function monitorStatus(options: G14MonitorOptions): Record { + const stateDir = rootPath(".state", "hwlab-g14"); + const stateFileName = hwlabG14MonitorStateFileName(options); + const stateFileRole = hwlabG14MonitorStateRole(options); + const latestPath = join(stateDir, stateFileName); + const exists = existsSync(latestPath); + let latest: Record | null = null; + let job: Record | null = null; + let statusCommand: string | null = null; + let degradedReason: string | null = null; + if (exists) { + try { + latest = record(JSON.parse(readFileSync(latestPath, "utf8")) as unknown); + const jobId = typeof latest.jobId === "string" ? latest.jobId : null; + if (jobId !== null) { + statusCommand = `bun scripts/cli.ts job status ${jobId} --tail-bytes 30000`; + try { + const current = readJob(jobId); + job = { + id: current.id, + name: current.name, + status: current.status, + createdAt: current.createdAt, + startedAt: current.startedAt, + finishedAt: current.finishedAt, + exitCode: current.exitCode, + runnerPid: current.runnerPid ?? null, + stdoutFile: current.stdoutFile, + stderrFile: current.stderrFile, + note: current.note, + }; + } catch (error) { + degradedReason = error instanceof Error ? error.message : String(error); + } + } + } catch (error) { + degradedReason = `failed to parse ${latestPath}: ${error instanceof Error ? error.message : String(error)}`; + } + } + return { + ok: degradedReason === null, + command: "hwlab g14 monitor-prs --status", + lane: options.lane, + baseBranch: monitorBaseBranch(options.lane), + mode: "status", + latestPath, + stateFileName, + stateFileRole, + exists, + latest, + job, + statusCommand, + degradedReason, + next: statusCommand === null ? { + start: `bun scripts/cli.ts hwlab g14 monitor-prs --lane ${options.lane}`, + } : { + status: statusCommand, + }, + }; +} + export async function runHwlabG14Command(_config: Config, args: string[]): Promise> { if (args.length === 0 || args.includes("--help") || args.includes("-h")) return { ok: true, ...hwlabG14Help() }; const [action] = args; @@ -5439,6 +5501,7 @@ export async function runHwlabG14Command(_config: Config, args: string[]): Promi } const options = parseOptions(args.slice(1)); if (options.worker) return runMonitorWorker(options); + if (args.includes("--status")) return monitorStatus(options); const command = ["bun", "scripts/cli.ts", "hwlab", "g14", "monitor-prs", "--worker", "--lane", options.lane, "--interval-seconds", String(options.intervalSeconds), "--timeout-seconds", String(options.timeoutSeconds), ...(options.once ? ["--once"] : []), ...(options.dryRun ? ["--dry-run"] : []), ...(options.maxCycles > 0 ? ["--max-cycles", String(options.maxCycles)] : [])]; const jobName = options.lane === "v02" ? "hwlab_g14_v02_pr_monitor" : "hwlab_g14_pr_monitor"; const jobNote = options.lane === "v02"