Files
pikasTech-unidesk/scripts/src/hwlab-g14/monitor.ts
T
2026-06-25 16:16:25 +00:00

277 lines
17 KiB
TypeScript

// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. monitor module for scripts/src/hwlab-g14.ts.
// Moved mechanically from scripts/src/hwlab-g14.ts:10163-10420 for #903.
import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { createHash, randomBytes } from "node:crypto";
import { repoRoot, rootPath, type Config } from "../config";
import { runCommand } from "../command";
import { cancelJob, listJobs, readJob, startJob } from "../jobs";
import { hwlabRequiredNoProxyEntries, hwlabRuntimeLaneConfigPath, hwlabRuntimeLaneIds, hwlabRuntimeLaneSpec, isHwlabRuntimeLane, type HwlabRuntimeLane, type HwlabRuntimeLaneSpec } from "../hwlab-node-lanes";
import type { CommandJsonResult, G14MonitorOptions } from "./types";
import { runRuntimeLanePrAutoCd, runV02PrAutoCd } from "./cd-trigger";
import { appendRolloutBrief, collectPipelineMetrics, commentRuntimeLanePullRequest, commentV02PullRequest, durationSeconds, getArgoStatus, getDevWorkloads, getG14Head, getLiveHealth, getPipelineStatus, listOpenG14PullRequests, mergePullRequest, preflightPullRequest, preflightSummary, refreshArgoDev, reportRuntimeLaneAutomationIssue, statusText, v02PrConflictState, workloadReadiness } from "./pr-monitor";
import { commandData, compactCommandResult, extractPullRequests, isCommandSuccess, nested, precheckWorkspace, printEvent, printRuntimeLanePrMonitorProgress, printV02PrMonitorProgress, record, shortSha, sleep } from "./remote";
import { legacyG14RetirementBlocksMonitor } from "./retirement";
import { V02_SOURCE_BRANCH } from "./types";
export async function waitForG14Dev(sourceCommit: string, timeoutSeconds: number): Promise<Record<string, unknown>> {
const started = Date.now();
const startedAt = new Date(started).toISOString();
let pipelineText = "";
let argoText = "";
let healthOk = false;
let workloadsReady: Record<string, unknown> = { ready: false };
let pipelineSucceededAt: string | null = null;
while (Date.now() - started < timeoutSeconds * 1000) {
const pipeline = getPipelineStatus(sourceCommit);
pipelineText = statusText(pipeline);
printEvent("g14.pipeline.status", { sourceCommit, pipelineRun: `hwlab-g14-ci-poll-${shortSha(sourceCommit)}`, text: pipelineText.slice(0, 500) });
if (!pipelineText.startsWith("True\nSucceeded")) {
if (pipelineText.startsWith("False\n")) return { ok: false, phase: "pipeline", sourceCommit, pipelineText };
await sleep(30_000);
continue;
}
pipelineSucceededAt ??= new Date().toISOString();
refreshArgoDev();
const argo = getArgoStatus();
argoText = statusText(argo);
printEvent("g14.argo.status", { text: argoText.slice(0, 500) });
const argoLines = argoText.split(/\r?\n/u);
const synced = argoLines[1] === "Synced";
const healthy = argoLines[2] === "Healthy";
const gitopsRevision = argoLines[0] ?? null;
const workloads = getDevWorkloads();
const workloadsText = statusText(workloads);
workloadsReady = workloadReadiness(workloadsText, isCommandSuccess(workloads));
const health = getLiveHealth();
healthOk = health.ok && /"status"\s*:\s*"ok"/u.test(health.stdout);
if (synced && healthy && record(workloadsReady).ready === true && healthOk) {
const finishedAt = new Date().toISOString();
const pipelineRun = `hwlab-g14-ci-poll-${shortSha(sourceCommit)}`;
const ciMetrics = collectPipelineMetrics(pipelineRun);
return { ok: true, sourceCommit, gitopsRevision, pipelineRun, pipelineText, argoText, startedAt, pipelineSucceededAt, finishedAt, workloadsReady, healthOk, ciMetrics };
}
await sleep(30_000);
}
return { ok: false, phase: "timeout", sourceCommit, pipelineText, argoText, startedAt, pipelineSucceededAt, workloadsReady, healthOk, timeoutSeconds };
}
export async function monitorV02Cycle(options: G14MonitorOptions, cycle: number): Promise<Record<string, unknown>> {
printEvent("v02.monitor.cycle.start", { cycle, dryRun: options.dryRun });
printV02PrMonitorProgress({ stage: "cycle", status: "started", cycle, dryRun: options.dryRun });
const listed = listOpenG14PullRequests();
if (!isCommandSuccess(listed)) return { ok: false, cycle, phase: "list-prs", listed };
const prs = extractPullRequests(listed, V02_SOURCE_BRANCH);
printEvent("v02.monitor.prs", { cycle, count: prs.length, pullRequests: prs });
if (prs.length === 0) {
printV02PrMonitorProgress({ stage: "idle", status: "waiting", cycle, pullRequests: 0, intervalSeconds: options.intervalSeconds });
return { ok: true, cycle, action: "none", lane: "v02", pullRequests: [] };
}
const observations: unknown[] = [];
for (const pr of prs) {
const startedAt = new Date().toISOString();
const preflightResult = preflightPullRequest(pr.number);
const preflight = preflightSummary(preflightResult);
printEvent("v02.pr.preflight", {
cycle,
number: pr.number,
ok: isCommandSuccess(preflightResult),
conclusion: preflight.conclusion,
readyForCommanderMerge: preflight.readyForCommanderMerge,
conflict: v02PrConflictState(preflight),
});
printV02PrMonitorProgress({
stage: "preflight",
status: preflight.readyForCommanderMerge === true ? "succeeded" : "running",
pr: pr.number,
conclusion: preflight.conclusion,
conflict: v02PrConflictState(preflight),
});
if (!isCommandSuccess(preflightResult) || preflight.readyForCommanderMerge !== true) {
const conclusion = String(preflight.conclusion ?? "unknown");
const blocked = conclusion === "blocked" || v02PrConflictState(preflight) === "conflict" || !isCommandSuccess(preflightResult);
const comment = commentV02PullRequest({
pr,
phase: "preflight",
state: blocked ? "blocked" : "waiting-ci",
startedAt,
observedAt: new Date().toISOString(),
elapsedSeconds: durationSeconds(startedAt, new Date().toISOString()),
preflight,
dryRun: options.dryRun,
message: blocked
? "v0.2 自动化已暂停在 PR preflight:存在 blocker 或冲突,需要先修复后再继续。"
: "v0.2 自动化正在等待 GitHub CI / mergeability 收敛;CI 通过且无冲突后会自动合并并触发 CD。",
});
observations.push({ pullRequest: pr, preflight, comment });
printV02PrMonitorProgress({ stage: "pr-comment", status: record(comment).ok === true ? "succeeded" : "failed", pr: pr.number, conclusion: preflight.conclusion });
if (record(comment).ok !== true) return { ok: false, cycle, lane: "v02", phase: "pr-comment", pullRequest: pr, preflight, comment, observations };
continue;
}
const merge = mergePullRequest(pr.number, options.dryRun);
printEvent("v02.pr.merge", { cycle, number: pr.number, dryRun: options.dryRun, ok: isCommandSuccess(merge) });
printV02PrMonitorProgress({ stage: "merge", status: isCommandSuccess(merge) ? "succeeded" : "running", pr: pr.number, dryRun: options.dryRun });
const result = await runV02PrAutoCd(pr, preflight, merge, options, startedAt);
observations.push(result);
if (record(result).ok !== true) return { ok: false, cycle, lane: "v02", phase: record(result).phase ?? "v02-auto-cd", pullRequest: pr, result, observations };
return { ok: true, cycle, lane: "v02", action: record(result).action ?? (options.dryRun ? "dry-run-merge" : "merged-and-rolled-v02"), result, observations };
}
return { ok: true, cycle, lane: "v02", action: "none", observations };
}
export function runtimeLanePreflightBlocked(preflightResult: CommandJsonResult, preflight: Record<string, unknown>): boolean {
const blockers = Array.isArray(preflight.blockers) ? preflight.blockers : [];
const pending = Array.isArray(preflight.pending) ? preflight.pending : [];
const conclusion = String(preflight.conclusion ?? "unknown").toLowerCase();
return !isCommandSuccess(preflightResult)
|| v02PrConflictState(preflight) === "conflict"
|| conclusion === "blocked"
|| conclusion === "failed"
|| conclusion === "failure"
|| conclusion === "error"
|| (blockers.length > 0 && pending.length === 0);
}
export async function monitorRuntimeLaneCycle(spec: HwlabRuntimeLaneSpec, options: G14MonitorOptions, cycle: number): Promise<Record<string, unknown>> {
printEvent(`${spec.lane}.monitor.cycle.start`, { cycle, dryRun: options.dryRun });
printRuntimeLanePrMonitorProgress(spec, { stage: "cycle", status: "started", cycle, dryRun: options.dryRun });
const listed = listOpenG14PullRequests();
if (!isCommandSuccess(listed)) return { ok: false, cycle, lane: spec.lane, phase: "list-prs", listed };
const prs = extractPullRequests(listed, spec.sourceBranch);
printEvent(`${spec.lane}.monitor.prs`, { cycle, count: prs.length, pullRequests: prs });
if (prs.length === 0) {
printRuntimeLanePrMonitorProgress(spec, { stage: "idle", status: "waiting", cycle, pullRequests: 0, intervalSeconds: options.intervalSeconds });
return { ok: true, cycle, action: "none", lane: spec.lane, pullRequests: [] };
}
const observations: unknown[] = [];
for (const pr of prs) {
const startedAt = new Date().toISOString();
const preflightResult = preflightPullRequest(pr.number);
const preflight = preflightSummary(preflightResult);
printEvent(`${spec.lane}.pr.preflight`, {
cycle,
number: pr.number,
ok: isCommandSuccess(preflightResult),
conclusion: preflight.conclusion,
readyForCommanderMerge: preflight.readyForCommanderMerge,
conflict: v02PrConflictState(preflight),
});
printRuntimeLanePrMonitorProgress(spec, {
stage: "preflight",
status: preflight.readyForCommanderMerge === true ? "succeeded" : "running",
pr: pr.number,
conclusion: preflight.conclusion,
conflict: v02PrConflictState(preflight),
});
if (!isCommandSuccess(preflightResult) || preflight.readyForCommanderMerge !== true) {
const blocked = runtimeLanePreflightBlocked(preflightResult, preflight);
const observedAt = new Date().toISOString();
const comment = commentRuntimeLanePullRequest(spec, {
pr,
phase: "preflight",
state: blocked ? "blocked" : "waiting-ci",
startedAt,
observedAt,
elapsedSeconds: durationSeconds(startedAt, observedAt),
preflight,
dryRun: options.dryRun,
message: blocked
? `${spec.version} 自动化已暂停在 PR preflight:存在 blocker、失败 check 或冲突,需要先修复后再继续。`
: `${spec.version} 自动化正在等待 GitHub CI / mergeability 收敛;CI 通过且无冲突后会自动合并并触发 CD。`,
});
const issue = blocked ? reportRuntimeLaneAutomationIssue({
spec,
pr,
state: "blocked",
phase: "preflight",
startedAt,
observedAt,
preflight,
details: { preflightResult: compactCommandResult(preflightResult) },
message: `${spec.version} PR preflight 未通过或存在冲突,自动合并/CD 已暂停。`,
dryRun: options.dryRun,
}) : { ok: true, skipped: true, reason: "waiting-ci-without-blocker" };
observations.push({ pullRequest: pr, preflight, comment, issue });
printRuntimeLanePrMonitorProgress(spec, { stage: "pr-comment", status: record(comment).ok === true ? "succeeded" : "failed", pr: pr.number, conclusion: preflight.conclusion });
if (record(comment).ok !== true) return { ok: false, cycle, lane: spec.lane, phase: "pr-comment", pullRequest: pr, preflight, comment, issue, observations };
if (record(issue).ok !== true) return { ok: false, cycle, lane: spec.lane, phase: "failure-issue", pullRequest: pr, preflight, comment, issue, observations };
continue;
}
const merge = mergePullRequest(pr.number, options.dryRun);
printEvent(`${spec.lane}.pr.merge`, { cycle, number: pr.number, dryRun: options.dryRun, ok: isCommandSuccess(merge) });
printRuntimeLanePrMonitorProgress(spec, { stage: "merge", status: isCommandSuccess(merge) ? "succeeded" : "running", pr: pr.number, dryRun: options.dryRun });
const result = await runRuntimeLanePrAutoCd(spec, pr, preflight, merge, options, startedAt);
observations.push(result);
if (record(result).ok !== true) return { ok: false, cycle, lane: spec.lane, phase: record(result).phase ?? `${spec.lane}-auto-cd`, pullRequest: pr, result, observations };
return { ok: true, cycle, lane: spec.lane, action: record(result).action ?? (options.dryRun ? "dry-run-merge" : `merged-and-rolled-${spec.lane}`), result, observations };
}
return { ok: true, cycle, lane: spec.lane, action: "none", observations };
}
export async function monitorCycle(options: G14MonitorOptions, cycle: number): Promise<Record<string, unknown>> {
if (options.lane === "v02") return monitorV02Cycle(options, cycle);
if (options.lane !== "g14") return monitorRuntimeLaneCycle(hwlabRuntimeLaneSpec(options.lane), options, cycle);
const retirement = legacyG14RetirementBlocksMonitor();
if (retirement !== null) {
return {
ok: false,
cycle,
phase: "retired",
degradedReason: "legacy-g14-dev-prod-retired",
retirement,
next: { status: "bun scripts/cli.ts hwlab g14 retirement status", v02Monitor: "bun scripts/cli.ts hwlab g14 monitor-prs --lane v02" },
};
}
printEvent("g14.monitor.cycle.start", { cycle, dryRun: options.dryRun });
const precheck = precheckWorkspace();
if (!isCommandSuccess(precheck)) return { ok: false, cycle, phase: "workspace-precheck", precheck };
const listed = listOpenG14PullRequests();
if (!isCommandSuccess(listed)) return { ok: false, cycle, phase: "list-prs", listed };
const prs = extractPullRequests(listed);
printEvent("g14.monitor.prs", { cycle, count: prs.length, pullRequests: prs });
if (prs.length === 0) return { ok: true, cycle, action: "none", pullRequests: [] };
const merged: unknown[] = [];
for (const pr of prs) {
const preflight = preflightPullRequest(pr.number);
printEvent("g14.pr.preflight", { cycle, number: pr.number, ok: isCommandSuccess(preflight) });
if (!isCommandSuccess(preflight) || nested(preflight.parsed, ["data", "mergeability", "readyForCommanderMerge"]) !== true) {
return { ok: false, cycle, phase: "pr-preflight", pullRequest: pr, preflight };
}
const merge = mergePullRequest(pr.number, options.dryRun);
printEvent("g14.pr.merge", { cycle, number: pr.number, dryRun: options.dryRun, ok: isCommandSuccess(merge) });
if (!isCommandSuccess(merge)) return { ok: false, cycle, phase: "pr-merge", pullRequest: pr, merge };
const sourceCommit = getG14Head();
const rollout = options.dryRun || sourceCommit === null ? { skipped: true, dryRun: options.dryRun, sourceCommit } : await waitForG14Dev(sourceCommit, options.timeoutSeconds);
const brief = options.dryRun || sourceCommit === null || record(rollout).ok !== true
? { skipped: true, dryRun: options.dryRun }
: appendRolloutBrief({ prNumber: pr.number, sourceCommit, dryRun: false }, rollout);
printEvent("g14.rollout.brief", { cycle, number: pr.number, ok: record(brief).ok, skipped: record(brief).skipped ?? false, brief: record(brief).brief ?? null });
merged.push({ pullRequest: pr, merge: commandData(merge), sourceCommit, rollout, brief });
if (record(rollout).ok === false) return { ok: false, cycle, phase: "rollout", pullRequest: pr, sourceCommit, rollout, merged };
if (record(brief).ok === false) return { ok: false, cycle, phase: "rollout-brief", pullRequest: pr, sourceCommit, rollout, brief, merged };
}
return { ok: true, cycle, action: options.dryRun ? "dry-run-merge" : "merged-and-rolled-dev", merged };
}
export async function runMonitorWorker(options: G14MonitorOptions): Promise<Record<string, unknown>> {
const maxCycles = options.once ? 1 : options.maxCycles;
let cycle = 0;
const results: unknown[] = [];
while (maxCycles === 0 || cycle < maxCycles) {
cycle += 1;
const result = await monitorCycle(options, cycle);
results.push(result);
printEvent(`${options.lane}.monitor.cycle.done`, { cycle, lane: options.lane, ok: record(result).ok, action: record(result).action ?? null, phase: record(result).phase ?? null });
if (record(result).ok !== true) return { ok: false, cycles: cycle, lastResult: result, results };
if (options.once || (options.lane === "g14" && record(result).action !== "none")) return { ok: true, cycles: cycle, lastResult: result, results };
printEvent(`${options.lane}.monitor.sleep`, { cycle, lane: options.lane, intervalSeconds: options.intervalSeconds });
if (options.lane === "v02") printV02PrMonitorProgress({ stage: "idle", status: "waiting", cycle, intervalSeconds: options.intervalSeconds });
else if (options.lane !== "g14") printRuntimeLanePrMonitorProgress(hwlabRuntimeLaneSpec(options.lane), { stage: "idle", status: "waiting", cycle, intervalSeconds: options.intervalSeconds });
await sleep(options.intervalSeconds * 1000);
}
return { ok: true, cycles: cycle, results };
}