docs: clarify hwlab v02 cli rollout closeout

This commit is contained in:
Codex
2026-05-31 14:08:42 +00:00
parent 1af11b1f0c
commit 1761f319e4
5 changed files with 151 additions and 16 deletions
+60 -2
View File
@@ -744,6 +744,10 @@ function v02WebAssetsProbeScript(): string {
"css=$(fetch_url \"$base/styles.css\" 2>/dev/null)",
"css_code=$?",
"printf 'cssOk\\t%s\\n' \"$css_code\"",
"app_js=$(fetch_url \"$base/app.js\" 2>/dev/null)",
"app_js_code=$?",
"printf 'appJsOk\\t%s\\n' \"$app_js_code\"",
"printf 'appJsBytes\\t%s\\n' \"$(printf '%s' \"$app_js\" | wc -c | tr -d ' ')\"",
"printf 'sidebarFitCss\\t%s\\n' \"$(printf '%s' \"$css\" | grep -Eq 'grid-template-rows:[[:space:]]*auto[[:space:]]+auto[[:space:]]+auto[[:space:]]+auto[[:space:]]+minmax\\(0,[[:space:]]*1fr\\)'; printf '%s' \"$?\")\"",
"printf 'workspaceFitCss\\t%s\\n' \"$(printf '%s' \"$css\" | grep -Eq 'grid-template-rows:[[:space:]]*auto[[:space:]]+minmax\\(0,[[:space:]]*1fr\\)'; printf '%s' \"$?\")\"",
"printf 'eventPanelFitCss\\t%s\\n' \"$(printf '%s' \"$css\" | grep -Eq 'grid-template-rows:[[:space:]]*auto[[:space:]]+minmax\\(132px,[[:space:]]*1fr\\)'; printf '%s' \"$?\")\"",
@@ -812,16 +816,18 @@ function v02WebAssetsFromText(
}
const htmlOk = fields.htmlOk === "0";
const cssOk = fields.cssOk === "0";
const appJsOk = fields.appJsOk === "0";
const apiHealthOk = fields.apiHealthOk === "0";
const readonlyNoteAbsent = fields.readonlyNote === "1";
const sidebarFitCss = fields.sidebarFitCss === "0";
const workspaceFitCss = fields.workspaceFitCss === "0";
const eventPanelFitCss = fields.eventPanelFitCss === "0";
const apiRevision = fields.apiRevision || null;
const webChecksPass = htmlOk && cssOk && readonlyNoteAbsent && sidebarFitCss && workspaceFitCss && eventPanelFitCss && apiHealthOk;
const webChecksPass = htmlOk && cssOk && appJsOk && readonlyNoteAbsent && sidebarFitCss && workspaceFitCss && eventPanelFitCss && apiHealthOk;
const failedChecks = Object.entries({
htmlOk,
cssOk,
appJsOk,
readonlyNoteAbsent,
sidebarFitCss,
workspaceFitCss,
@@ -838,6 +844,7 @@ function v02WebAssetsFromText(
checks: {
htmlOk,
cssOk,
appJsOk,
readonlyNoteAbsent,
sidebarFitCss,
workspaceFitCss,
@@ -847,12 +854,16 @@ function v02WebAssetsFromText(
probeExitCodes: {
html: numericField(fields.htmlOk),
css: numericField(fields.cssOk),
appJs: numericField(fields.appJsOk),
readonlyNoteGrep: numericField(fields.readonlyNote),
sidebarFitCssGrep: numericField(fields.sidebarFitCss),
workspaceFitCssGrep: numericField(fields.workspaceFitCss),
eventPanelFitCssGrep: numericField(fields.eventPanelFitCss),
apiHealth: numericField(fields.apiHealthOk),
},
assetBytes: {
appJs: numericField(fields.appJsBytes),
},
apiRevision,
note: webAssetsRevisionNote(apiRevision, sourceCommit, activePipelineRuns),
exitCode,
@@ -870,7 +881,7 @@ function webAssetsRevisionNote(apiRevision: string | null, sourceCommit: string
if (activeItems.length > 0) {
return `runtime apiRevision=${apiRevision} differs from sourceCommit=${sourceCommit}; active v02 PipelineRun observed, wait for rollout before judging runtime revision`;
}
return `runtime revision lag: cloud-api apiRevision=${apiRevision} differs from sourceCommit=${sourceCommit}; inspect recentPipelineRuns, argo, and runtime-ready state`;
return `cloud-api apiRevision=${apiRevision} is service-scoped and differs from sourceCommit=${sourceCommit}; when web asset probes pass, this is not proof that Cloud Web is stale`;
}
function numericField(value: string | undefined): number | null {
@@ -1716,6 +1727,52 @@ export function parseGitMirrorStatusRefs(raw: string): { refs: Record<string, st
}
}
function parseLabeledJsonLine(raw: string, label: string): Record<string, unknown> {
const line = raw.split(/\r?\n/u).find((item) => item.startsWith(`${label}=`));
if (line === undefined) return {};
const value = line.slice(label.length + 1).trim();
if (value.length === 0) return {};
try {
return record(JSON.parse(value) as unknown);
} catch {
return {};
}
}
export function gitMirrorStatusSummary(raw: string): Record<string, unknown> {
const refs = parseGitMirrorStatusRefs(raw);
const lastSync = parseLabeledJsonLine(raw, "lastSync");
const lastWrite = parseLabeledJsonLine(raw, "lastWrite");
const lastFlush = parseLabeledJsonLine(raw, "lastFlush");
const localGitops = refs.refs.localGitops ?? null;
const githubGitops = refs.refs.githubGitops ?? null;
return {
localV02: refs.refs.localV02 ?? null,
localG14: refs.refs.localG14 ?? null,
localGitops,
githubGitops,
pendingFlush: refs.pendingFlush,
lastSync: {
status: stringOrNull(lastSync.status) ?? null,
at: stringOrNull(lastSync.publishedAt) ?? stringOrNull(lastSync.syncedAt) ?? null,
pendingFlush: typeof lastSync.pendingFlush === "boolean" ? lastSync.pendingFlush : null,
},
lastWrite: {
status: stringOrNull(lastWrite.status) ?? null,
at: stringOrNull(lastWrite.writtenAt) ?? null,
pendingFlush: typeof lastWrite.pendingFlush === "boolean" ? lastWrite.pendingFlush : null,
},
lastFlush: {
status: stringOrNull(lastFlush.status) ?? null,
at: stringOrNull(lastFlush.flushedAt) ?? null,
pendingFlush: typeof lastFlush.pendingFlush === "boolean" ? lastFlush.pendingFlush : null,
},
flushNeeded: refs.pendingFlush === true,
flushCommand: refs.pendingFlush === true ? "bun scripts/cli.ts hwlab g14 git-mirror flush --confirm" : null,
githubInSync: Boolean(localGitops && githubGitops && localGitops === githubGitops),
};
}
export function gitMirrorV02SyncRequirement(sourceCommit: string, rawStatus: string): Record<string, unknown> {
const parsed = parseGitMirrorStatusRefs(rawStatus);
const localV02 = parsed.refs.localV02 ?? null;
@@ -1937,6 +1994,7 @@ function runGitMirrorStatus(): Record<string, unknown> {
},
cache: {
ok: shellSectionOk(cache),
summary: gitMirrorStatusSummary(String(cache?.stdout ?? "").trim()),
raw: String(cache?.stdout ?? "").trim(),
stderr: shellSectionOk(cache) ? "" : bundleStderr,
},
+73 -8
View File
@@ -25,7 +25,7 @@ export interface JobRecord {
}
export interface JobProgressSummary {
kind: "hwlab-v02-trigger" | "generic";
kind: "hwlab-v02-trigger" | "hwlab-git-mirror" | "generic";
stage: string | null;
stageStatus: string | null;
sourceCommit: string | null;
@@ -196,11 +196,13 @@ export function jobWithTail(job: JobRecord, maxBytes = 12000): JobRecord & {
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 gitMirrorWorkflow = job.name === "hwlab_g14_git_mirror_sync" || job.name === "hwlab_g14_git_mirror_flush";
if (!knownWorkflow && !gitMirrorWorkflow && tails === undefined) return genericJobProgress(job);
const nowMs = Date.now();
const progressTailBytes = Math.max(4096, Math.floor(maxBytes));
const stderrTail = tails?.stderrTail ?? tailFile(job.stderrFile, progressTailBytes);
const stdoutTail = tails?.stdoutTail ?? tailFile(job.stdoutFile, progressTailBytes);
if (gitMirrorWorkflow) return summarizeGitMirrorJobProgress(job, stdoutTail, stderrTail, nowMs);
const events = parseJsonLineEvents(stderrTail, "hwlab.v02.trigger.progress");
const lastEvent = events.at(-1) ?? {};
const stage = stringField(lastEvent.stage);
@@ -266,6 +268,57 @@ function summarizeJobProgress(job: JobRecord, maxBytes = 96_000, tails?: { stdou
};
}
function summarizeGitMirrorJobProgress(job: JobRecord, stdoutTail: string, stderrTail: string, nowMs = Date.now()): JobProgressSummary {
const action = job.name.endsWith("_flush") ? "flush" : job.name.endsWith("_sync") ? "sync" : "unknown";
const elapsedSeconds = jobElapsedSeconds(job, nowMs);
const timings = extractGitMirrorTimings(`${stderrTail}\n${stdoutTail}`);
const status =
firstMatch(stdoutTail, new RegExp(`"event"\\s*:\\s*"git-mirror-${action}"[\\s\\S]{0,240}?"status"\\s*:\\s*"([^"]+)"`, "u")) ??
firstMatch(stdoutTail.replaceAll('\\"', '"').replaceAll("\\n", "\n"), new RegExp(`"event"\\s*:\\s*"git-mirror-${action}"[\\s\\S]{0,240}?"status"\\s*:\\s*"([^"]+)"`, "u"));
const pendingFlush = firstMatch(stdoutTail.replaceAll('\\"', '"'), /"pendingFlush"\s*:\s*(true|false)/u);
const jobName = firstMatch(stdoutTail, /"jobName"\s*:\s*"([^"]+)"/u);
const warnings = jobProgressWarnings({
job,
eventsObserved: 0,
elapsedSeconds,
stage: action,
stageStatus: job.status === "running" ? "running" : status,
stageElapsedSeconds: job.status === "running" ? elapsedSeconds : null,
lastEventAgeSeconds: null,
});
const timingSummary = Object.entries(timings)
.filter(([key]) => key.startsWith(`gitMirror.${action}.`) || key === "sshRuntimeMaxMs")
.map(([key, value]) => `${key}=${value}ms`);
return {
kind: "hwlab-git-mirror",
stage: action,
stageStatus: status ?? (job.status === "running" ? "running" : null),
sourceCommit: null,
pipelineRun: null,
pipelineCreated: null,
elapsedSeconds,
stageElapsedSeconds: job.status === "running" ? elapsedSeconds : null,
lastEventAt: null,
lastEventAgeSeconds: null,
eventsObserved: 0,
slow: warnings.length > 0,
warnings,
timings,
summary: [
job.status,
`git-mirror-${action}${status ? `:${status}` : ""}`,
jobName ? `job=${jobName}` : null,
pendingFlush !== null ? `pendingFlush=${pendingFlush}` : null,
elapsedSeconds !== null ? `elapsed=${elapsedSeconds}s` : null,
...timingSummary,
warnings.length > 0 ? "visibility-warning" : null,
].filter(Boolean).join(" "),
nextCommand: job.status === "running"
? `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`
: "bun scripts/cli.ts hwlab g14 git-mirror status",
};
}
function genericJobProgress(job: JobRecord): JobProgressSummary {
const nowMs = Date.now();
const elapsedSeconds = jobElapsedSeconds(job, nowMs);
@@ -401,10 +454,18 @@ function extractGitMirrorTimingJsonLines(text: string, timings: Record<string, n
const parsed = JSON.parse(trimmed) as unknown;
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) continue;
const event = parsed as Record<string, unknown>;
if (event.event !== "git-mirror-sync-timing") continue;
const operation = event.event === "git-mirror-sync-timing"
? "sync"
: event.event === "git-mirror-flush-timing"
? "flush"
: null;
if (operation === null) continue;
const phase = stringField(event.phase);
const durationMs = typeof event.durationMs === "number" && Number.isFinite(event.durationMs) ? event.durationMs : null;
if (phase !== null && durationMs !== null) timings[`gitMirror.${phase}Ms`] = Math.round(durationMs);
if (phase !== null && durationMs !== null) {
timings[`gitMirror.${operation}.${phase}Ms`] = Math.round(durationMs);
if (operation === "sync") timings[`gitMirror.${phase}Ms`] = Math.round(durationMs);
}
} catch {
// Ignore non-JSON lines; raw logs remain available through job status tails.
}
@@ -412,11 +473,15 @@ function extractGitMirrorTimingJsonLines(text: string, timings: Record<string, n
}
function extractGitMirrorTimingRegex(text: string, timings: Record<string, number>): void {
const pattern = /"event"\s*:\s*"git-mirror-sync-timing"[\s\S]{0,240}?"phase"\s*:\s*"([^"]+)"[\s\S]{0,240}?"durationMs"\s*:\s*(\d+)/gu;
const pattern = /"event"\s*:\s*"git-mirror-(sync|flush)-timing"[\s\S]{0,240}?"phase"\s*:\s*"([^"]+)"[\s\S]{0,240}?"durationMs"\s*:\s*(\d+)/gu;
for (const match of text.matchAll(pattern)) {
const phase = match[1];
const durationMs = Number(match[2]);
if (phase !== undefined && Number.isFinite(durationMs)) timings[`gitMirror.${phase}Ms`] = Math.round(durationMs);
const operation = match[1];
const phase = match[2];
const durationMs = Number(match[3]);
if (operation !== undefined && phase !== undefined && Number.isFinite(durationMs)) {
timings[`gitMirror.${operation}.${phase}Ms`] = Math.round(durationMs);
if (operation === "sync") timings[`gitMirror.${phase}Ms`] = Math.round(durationMs);
}
}
}