docs: clarify hwlab v02 cli rollout closeout
This commit is contained in:
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user