refactor: split control-plane cli modules
This commit is contained in:
@@ -0,0 +1,609 @@
|
||||
// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. control-plane module for scripts/src/agentrun.ts.
|
||||
|
||||
// Moved mechanically from scripts/src/agentrun.ts:2022-2592 for #903.
|
||||
|
||||
// SPEC: PJ2026-01020108 cancel lifecycle + PJ2026-01020205 AipodSpec binding + PJ2026-01020302 session policy + PJ2026-01020305 cancel control + PJ2026-01060305/06 YAML execution policy and bounded output draft-2026-06-25-p0.
|
||||
// Exposes AgentRun lane-scoped policy, AipodSpec SecretRef binding, cancel lifecycle, and bounded default output in the UniDesk CLI.
|
||||
import { chmodSync, copyFileSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { rootPath, type UniDeskConfig } from "../config";
|
||||
import type { RenderedCliResult } from "../output";
|
||||
import { applyLocalCaddyManagedSite } from "../pk01-caddy";
|
||||
import { runSshCommandCapture, type SshCaptureResult } from "../ssh";
|
||||
import { runRemoteSshCommandCapture } from "../remote";
|
||||
import { startJob } from "../jobs";
|
||||
import {
|
||||
AGENTRUN_CONFIG_PATH,
|
||||
agentRunLaneSummary,
|
||||
agentRunPipelineRunName,
|
||||
agentRunProviderCredentialRefs,
|
||||
resolveAgentRunLaneTarget,
|
||||
type AgentRunCancelLifecycleSpec,
|
||||
type AgentRunLaneSpec,
|
||||
} from "../agentrun-lanes";
|
||||
import {
|
||||
agentRunImageArtifact,
|
||||
placeholderAgentRunImage,
|
||||
renderedFilesDigest,
|
||||
renderedObjectsDigest,
|
||||
renderAgentRunControlPlaneManifests,
|
||||
renderAgentRunGitopsFiles,
|
||||
type AgentRunArtifactService,
|
||||
} from "../agentrun-manifests";
|
||||
import { sha256Fingerprint } from "../platform-infra-ops-library";
|
||||
|
||||
import type { CleanupReleasedPvOptions, CleanupRunnersOptions, CleanupRunsOptions, ConfirmOptions, GitMirrorOptions, LaneConfirmOptions, RefreshOptions, SecretSyncOptions, StatusOptions } from "./options";
|
||||
import { agentRunControlPlaneStatusCommand } from "./public-exposure";
|
||||
import { applyYamlScript, manifestObjectRef, yamlLaneGitMirrorStatusScript } from "./secrets";
|
||||
import { compactAgentRunLaneStatusTarget, compactLaneSecretsStatus } from "./trigger";
|
||||
import { capture, captureJsonPayload, compactCapture, record, stringOrNull, timedStatusStage } from "./utils";
|
||||
import { yamlLaneRuntimeStatusScript, yamlLaneSourceStatusScript } from "./yaml-lane";
|
||||
|
||||
export function parseSecretSyncOptions(args: string[]): SecretSyncOptions {
|
||||
const base = parseConfirmOptions(args);
|
||||
let node: string | null = null;
|
||||
let lane: string | null = null;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--confirm" || arg === "--dry-run") continue;
|
||||
if (arg === "--node") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--node requires a value");
|
||||
node = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--lane") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--lane requires a value");
|
||||
lane = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`unsupported secret-sync option: ${arg}`);
|
||||
}
|
||||
return { ...base, node, lane };
|
||||
}
|
||||
|
||||
export function parseLaneConfirmOptions(args: string[]): LaneConfirmOptions {
|
||||
const base = parseConfirmOptions(args);
|
||||
let node: string | null = null;
|
||||
let lane: string | null = null;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--confirm" || arg === "--dry-run") continue;
|
||||
if (arg === "--node") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--node requires a value");
|
||||
node = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--lane") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--lane requires a value");
|
||||
lane = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`unsupported control-plane option: ${arg}`);
|
||||
}
|
||||
if (node === null && lane === null) throw new Error("control-plane apply requires --node and --lane");
|
||||
return { ...base, node, lane };
|
||||
}
|
||||
|
||||
export function parseRefreshOptions(args: string[]): RefreshOptions {
|
||||
const base = parseConfirmOptions(args);
|
||||
let node: string | null = null;
|
||||
let lane: string | null = null;
|
||||
let full = false;
|
||||
let raw = false;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--confirm" || arg === "--dry-run") continue;
|
||||
if (arg === "--full") {
|
||||
full = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--raw") {
|
||||
raw = true;
|
||||
full = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--node") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--node requires a value");
|
||||
node = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--lane") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--lane requires a value");
|
||||
lane = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`unsupported refresh option: ${arg}`);
|
||||
}
|
||||
return { ...base, node, lane, full, raw };
|
||||
}
|
||||
|
||||
export function parseConfirmOptions(args: string[]): ConfirmOptions {
|
||||
if (args.includes("--confirm") && args.includes("--dry-run")) throw new Error("accepts only one of --confirm or --dry-run");
|
||||
return {
|
||||
confirm: args.includes("--confirm"),
|
||||
dryRun: args.includes("--dry-run") || !args.includes("--confirm"),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseGitMirrorOptions(args: string[]): GitMirrorOptions {
|
||||
validateOptions(args, new Set(["--confirm", "--dry-run", "--wait"]), new Set(["--timeout-seconds", "--node", "--lane"]));
|
||||
const base = parseConfirmOptions(args);
|
||||
const timeoutIndex = args.indexOf("--timeout-seconds");
|
||||
const timeoutSeconds = timeoutIndex >= 0 ? Number(args[timeoutIndex + 1]) : 300;
|
||||
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 30) throw new Error("--timeout-seconds must be a number >= 30");
|
||||
return {
|
||||
...base,
|
||||
node: optionValue(args, "--node") ?? null,
|
||||
lane: optionValue(args, "--lane") ?? null,
|
||||
timeoutSeconds,
|
||||
wait: args.includes("--wait"),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseCleanupRunnersOptions(args: string[]): CleanupRunnersOptions {
|
||||
validateOptions(args, new Set(["--confirm", "--dry-run", "--force-active", "--full", "--raw"]), new Set(["--timeout-seconds", "--node", "--lane"]));
|
||||
const base = parseConfirmOptions(args);
|
||||
const raw = args.includes("--raw");
|
||||
return {
|
||||
...base,
|
||||
node: optionValue(args, "--node") ?? null,
|
||||
lane: optionValue(args, "--lane") ?? null,
|
||||
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 180, 600),
|
||||
forceActive: args.includes("--force-active"),
|
||||
full: raw || args.includes("--full"),
|
||||
raw,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseCleanupRunsOptions(args: string[]): CleanupRunsOptions {
|
||||
validateOptions(args, new Set(["--confirm", "--dry-run"]), new Set(["--min-age-minutes", "--limit", "--timeout-seconds", "--node", "--lane"]));
|
||||
const base = parseConfirmOptions(args);
|
||||
return {
|
||||
...base,
|
||||
node: optionValue(args, "--node") ?? null,
|
||||
lane: optionValue(args, "--lane") ?? null,
|
||||
minAgeMinutes: positiveIntegerOption(args, "--min-age-minutes", 60, 10080),
|
||||
limit: positiveIntegerOption(args, "--limit", 20, 500),
|
||||
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 180, 600),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseCleanupReleasedPvOptions(args: string[]): CleanupReleasedPvOptions {
|
||||
validateOptions(args, new Set(["--confirm", "--dry-run"]), new Set(["--limit", "--timeout-seconds", "--node", "--lane"]));
|
||||
const base = parseConfirmOptions(args);
|
||||
return {
|
||||
...base,
|
||||
node: optionValue(args, "--node") ?? null,
|
||||
lane: optionValue(args, "--lane") ?? null,
|
||||
limit: positiveIntegerOption(args, "--limit", 20, 500),
|
||||
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 120, 600),
|
||||
};
|
||||
}
|
||||
|
||||
export function validateOptions(args: string[], booleanOptions: Set<string>, valueOptions: Set<string>): void {
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (booleanOptions.has(arg)) continue;
|
||||
if (valueOptions.has(arg)) {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`unsupported option: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function optionValue(args: string[], name: string): string | undefined {
|
||||
const index = args.indexOf(name);
|
||||
if (index === -1) return undefined;
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
||||
return value;
|
||||
}
|
||||
|
||||
export function positiveIntegerOption(args: string[], name: string, defaultValue: number, maxValue: number): number {
|
||||
const raw = optionValue(args, name);
|
||||
if (raw === undefined) return defaultValue;
|
||||
const value = Number(raw);
|
||||
if (!Number.isInteger(value) || value < 0) throw new Error(`${name} must be a non-negative integer`);
|
||||
return Math.min(value, maxValue);
|
||||
}
|
||||
|
||||
export async function controlPlanePlan(_config: UniDeskConfig, options: StatusOptions): Promise<Record<string, unknown>> {
|
||||
const target = resolveAgentRunLaneTarget(options);
|
||||
const spec = target.spec;
|
||||
return {
|
||||
ok: true,
|
||||
command: "agentrun control-plane plan",
|
||||
mode: "yaml-declared-node-lane",
|
||||
configPath: target.configPath,
|
||||
target: agentRunLaneSummary(spec),
|
||||
plannedChecks: [
|
||||
"source-branch-exists",
|
||||
"source-worktree-exists-and-clean",
|
||||
"git-mirror-services-ready",
|
||||
"ci-namespace-pipeline-serviceaccount",
|
||||
"argo-application-alignment",
|
||||
"runtime-namespace-manager-service",
|
||||
"database-secretref-present",
|
||||
"local-postgres-absent-when-external",
|
||||
],
|
||||
deploymentBoundary: {
|
||||
mutation: false,
|
||||
note: "plan/status are read-only. Long writes must use controlled AgentRun/Platform DB commands and this YAML target.",
|
||||
},
|
||||
next: {
|
||||
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
|
||||
postgresStatus: spec.database.configRef ? `bun scripts/cli.ts platform-db postgres status --config ${spec.database.configRef}` : null,
|
||||
postgresApply: spec.database.configRef ? `bun scripts/cli.ts platform-db postgres apply --config ${spec.database.configRef} --confirm` : null,
|
||||
controlPlaneApply: `bun scripts/cli.ts agentrun control-plane apply --node ${spec.nodeId} --lane ${spec.lane} --dry-run`,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
export async function controlPlaneApply(config: UniDeskConfig, options: LaneConfirmOptions): Promise<Record<string, unknown>> {
|
||||
const { configPath, spec } = resolveAgentRunLaneTarget(options);
|
||||
const objects = renderAgentRunControlPlaneManifests(spec);
|
||||
const manifestYaml = `${objects.map((object) => Bun.YAML.stringify(object).trim()).join("\n---\n")}\n`;
|
||||
const objectRefs = objects.map((object) => manifestObjectRef(object));
|
||||
const plan = {
|
||||
node: spec.nodeId,
|
||||
kubeRoute: spec.nodeKubeRoute,
|
||||
lane: spec.lane,
|
||||
version: spec.version,
|
||||
objectCount: objects.length,
|
||||
objects: objectRefs,
|
||||
manifestBytes: Buffer.byteLength(manifestYaml, "utf8"),
|
||||
manifestDigest: renderedObjectsDigest(objects),
|
||||
fieldManager: "unidesk-agentrun-control-plane",
|
||||
valuesPrinted: false,
|
||||
};
|
||||
if (options.dryRun || !options.confirm) {
|
||||
return {
|
||||
ok: true,
|
||||
command: "agentrun control-plane apply",
|
||||
mode: "dry-run",
|
||||
mutation: false,
|
||||
configPath,
|
||||
target: agentRunLaneSummary(spec),
|
||||
plan,
|
||||
next: {
|
||||
confirm: `bun scripts/cli.ts agentrun control-plane apply --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
|
||||
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
const applied = await capture(config, spec.nodeKubeRoute, ["sh", "--", applyYamlScript(manifestYaml, "unidesk-agentrun-control-plane", false)]);
|
||||
const payload = captureJsonPayload(applied);
|
||||
return {
|
||||
ok: applied.exitCode === 0 && payload.ok !== false,
|
||||
command: "agentrun control-plane apply",
|
||||
mode: "confirmed-apply",
|
||||
mutation: true,
|
||||
configPath,
|
||||
target: agentRunLaneSummary(spec),
|
||||
plan,
|
||||
result: payload,
|
||||
capture: compactCapture(applied, { full: applied.exitCode !== 0, stdoutTailChars: 5000, stderrTailChars: 5000 }),
|
||||
next: {
|
||||
secretSync: `bun scripts/cli.ts agentrun control-plane secret-sync --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
|
||||
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
export async function status(config: UniDeskConfig, options: StatusOptions): Promise<Record<string, unknown>> {
|
||||
return await statusYamlLane(config, options, resolveAgentRunLaneTarget(options));
|
||||
}
|
||||
|
||||
export async function statusYamlLane(config: UniDeskConfig, options: StatusOptions, target: { configPath: string; spec: AgentRunLaneSpec }): Promise<Record<string, unknown>> {
|
||||
const spec = target.spec;
|
||||
const sourceProbe = await timedStatusStage("source", () => capture(config, `${spec.nodeRoute}:${spec.source.workspace}`, ["sh", "--", yamlLaneSourceStatusScript(spec)]));
|
||||
const sourcePayload = captureJsonPayload(sourceProbe.value);
|
||||
const branchTipCommit = stringOrNull(sourcePayload.remoteBranchCommit) ?? stringOrNull(sourcePayload.localHead);
|
||||
const initialSourceCommit = options.sourceCommit
|
||||
?? stringOrNull(sourcePayload.remoteBranchCommit)
|
||||
?? stringOrNull(sourcePayload.localHead);
|
||||
const pipelineRunName = options.pipelineRun ?? (initialSourceCommit ? agentRunPipelineRunName(spec, initialSourceCommit) : null);
|
||||
const [runtimeProbe, mirrorProbe] = await Promise.all([
|
||||
timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneRuntimeStatusScript(spec, pipelineRunName)])),
|
||||
timedStatusStage("git-mirror", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneGitMirrorStatusScript(spec)])),
|
||||
]);
|
||||
const runtimePayload = captureJsonPayload(runtimeProbe.value);
|
||||
const mirrorPayload = captureJsonPayload(mirrorProbe.value);
|
||||
const pipeline = record(runtimePayload.pipeline);
|
||||
const pipelineRunStatus = record(runtimePayload.pipelineRun);
|
||||
const argo = record(runtimePayload.argo);
|
||||
const manager = record(runtimePayload.manager);
|
||||
const database = record(runtimePayload.database);
|
||||
const secrets = record(runtimePayload.secrets);
|
||||
const localPostgres = record(runtimePayload.localPostgres);
|
||||
const expectedGitopsRevision = stringOrNull(mirrorPayload.gitopsCommit);
|
||||
const mirrorSourceCommit = stringOrNull(mirrorPayload.sourceCommit);
|
||||
const pipelineRunSourceCommit = stringOrNull(pipelineRunStatus.sourceCommit);
|
||||
const sourceCommit = options.sourceCommit
|
||||
?? (options.pipelineRun !== null ? pipelineRunSourceCommit ?? initialSourceCommit : initialSourceCommit);
|
||||
const managerSourceMatchesExpected = Boolean(sourceCommit && manager.sourceCommit === sourceCommit);
|
||||
const argoSyncedToGitops = Boolean(expectedGitopsRevision && argo.revision === expectedGitopsRevision);
|
||||
const pipelineSucceeded = pipelineRunStatus.status === "True";
|
||||
const sourceWorktreeDetached = sourcePayload.branch === "HEAD";
|
||||
const targetMode = options.pipelineRun !== null ? "pipeline-run" : options.sourceCommit !== null ? "source-commit" : "latest-source-head";
|
||||
const sourceBranchAdvanced = targetMode !== "latest-source-head"
|
||||
&& sourceCommit !== null
|
||||
&& branchTipCommit !== null
|
||||
&& sourceCommit !== branchTipCommit;
|
||||
const branchDrift = {
|
||||
targetMode,
|
||||
sourceBranch: spec.source.branch,
|
||||
currentBranchTipCommit: branchTipCommit,
|
||||
targetSourceCommit: sourceCommit,
|
||||
pipelineRunSourceCommit,
|
||||
sourceBranchAdvanced,
|
||||
targetSupersededByCurrentBranch: sourceBranchAdvanced,
|
||||
closeoutHint: sourceBranchAdvanced
|
||||
? "target PipelineRun/source commit is not the current branch tip; verify the branch tip contains the fix, then trigger-current again for latest-tip closeout"
|
||||
: null,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
const warnings = [
|
||||
...(sourceWorktreeDetached ? ["source-worktree-detached"] : []),
|
||||
...(sourceBranchAdvanced ? ["source-branch-advanced-after-target"] : []),
|
||||
];
|
||||
const blockers = [
|
||||
...(sourcePayload.workspaceExists === true ? [] : ["source-worktree-missing"]),
|
||||
...(sourcePayload.remoteBranchExists === true ? [] : ["source-branch-missing"]),
|
||||
...(sourcePayload.workspaceClean === true || sourcePayload.workspaceExists !== true ? [] : ["source-worktree-dirty"]),
|
||||
...(mirrorPayload.readReady === true ? [] : ["git-mirror-read-not-ready"]),
|
||||
...(mirrorPayload.writeReady === true ? [] : ["git-mirror-write-not-ready"]),
|
||||
...(mirrorPayload.cachePvcExists === true ? [] : ["git-mirror-cache-pvc-missing"]),
|
||||
...(runtimePayload.ciNamespaceExists === true ? [] : ["ci-namespace-missing"]),
|
||||
...(pipeline.exists === true ? [] : ["pipeline-missing"]),
|
||||
...(pipelineRunName !== null && pipelineRunStatus.exists !== true ? ["pipelinerun-missing"] : []),
|
||||
...(pipelineRunStatus.exists !== true || pipelineRunStatus.status === undefined || pipelineRunStatus.status === null || pipelineRunStatus.status === "True" ? [] : ["pipelinerun-not-succeeded"]),
|
||||
...(runtimePayload.serviceAccountExists === true ? [] : ["ci-serviceaccount-missing"]),
|
||||
...(argo.exists === true ? [] : ["argo-application-missing"]),
|
||||
...(mirrorPayload.readReady === true && expectedGitopsRevision === null ? ["gitops-revision-unresolved"] : []),
|
||||
...(argo.exists !== true || expectedGitopsRevision === null || argoSyncedToGitops ? [] : ["argo-revision-stale"]),
|
||||
...(runtimePayload.runtimeNamespaceExists === true ? [] : ["runtime-namespace-missing"]),
|
||||
...(manager.deploymentExists === true ? [] : ["manager-deployment-missing"]),
|
||||
...(manager.deploymentExists !== true || sourceCommit === null || managerSourceMatchesExpected ? [] : ["manager-source-stale"]),
|
||||
...(manager.serviceExists === true ? [] : ["manager-service-missing"]),
|
||||
...(spec.database.mode === "external-postgres" && database.secretPresent !== true ? ["database-secret-missing"] : []),
|
||||
...(secrets.ready === true ? [] : ["lane-secret-missing"]),
|
||||
...(spec.database.localPostgresExpectedAbsent && localPostgres.absent !== true ? ["local-postgres-present"] : []),
|
||||
];
|
||||
const ciBlockerNames = new Set(["ci-namespace-missing", "pipeline-missing", "pipelinerun-missing", "pipelinerun-not-succeeded", "ci-serviceaccount-missing"]);
|
||||
const ciBlockers = blockers.filter((blocker) => ciBlockerNames.has(blocker));
|
||||
const runtimeBlockers = blockers.filter((blocker) => !ciBlockerNames.has(blocker));
|
||||
const aligned = blockers.length === 0 && pipelineSucceeded && argoSyncedToGitops && managerSourceMatchesExpected;
|
||||
const runtimeAligned = runtimeBlockers.length === 0 && argoSyncedToGitops && managerSourceMatchesExpected;
|
||||
const ciEvidenceMissing = pipelineRunName !== null && pipelineRunStatus.exists !== true;
|
||||
const mirrorAlreadySynced = mirrorPayload.readReady === true
|
||||
&& mirrorPayload.writeReady === true
|
||||
&& sourceCommit !== null
|
||||
&& mirrorSourceCommit === sourceCommit
|
||||
&& expectedGitopsRevision !== null;
|
||||
const runtimeAlignment = {
|
||||
pipelineSucceeded,
|
||||
argoRevision: stringOrNull(argo.revision),
|
||||
argoSyncedToGitops,
|
||||
managerSourceCommit: stringOrNull(manager.sourceCommit),
|
||||
managerSourceMatchesExpected,
|
||||
runtimeAligned,
|
||||
};
|
||||
const statusFullCommand = agentRunControlPlaneStatusCommand(spec, options, true);
|
||||
const triggerCurrentCommand = `bun scripts/cli.ts agentrun control-plane trigger-current --node ${spec.nodeId} --lane ${spec.lane} --confirm`;
|
||||
const nextAction = aligned
|
||||
? { code: "none", summary: "lane aligned; no action required", command: null }
|
||||
: ciEvidenceMissing && runtimeAligned && mirrorAlreadySynced
|
||||
? { code: "ci-evidence-missing", summary: "runtime and git mirror are aligned, but the expected PipelineRun evidence is missing; rerun trigger-current to recreate CI evidence", command: triggerCurrentCommand }
|
||||
: sourceBranchAdvanced
|
||||
? { code: "target-superseded", summary: "target commit is no longer the branch tip; trigger-current against latest source before closeout", command: triggerCurrentCommand }
|
||||
: ciBlockers.length > 0
|
||||
? { code: "ci-blocked", summary: `CI evidence is blocked: ${ciBlockers.join(", ")}`, command: statusFullCommand }
|
||||
: runtimeBlockers.length > 0
|
||||
? { code: "runtime-blocked", summary: `runtime alignment is blocked: ${runtimeBlockers.join(", ")}`, command: statusFullCommand }
|
||||
: { code: "inspect-full-status", summary: "alignment is inconclusive; inspect full status details", command: statusFullCommand };
|
||||
const compactSourceStatus = {
|
||||
workspaceExists: sourcePayload.workspaceExists ?? false,
|
||||
workspaceClean: sourcePayload.workspaceClean ?? null,
|
||||
branch: sourcePayload.branch ?? null,
|
||||
remoteBranchExists: sourcePayload.remoteBranchExists ?? false,
|
||||
remoteBranchCommit: sourcePayload.remoteBranchCommit ?? null,
|
||||
workspaceDetached: sourceWorktreeDetached,
|
||||
};
|
||||
const compactGitMirrorStatus = {
|
||||
readReady: mirrorPayload.readReady ?? false,
|
||||
writeReady: mirrorPayload.writeReady ?? false,
|
||||
cachePvcExists: mirrorPayload.cachePvcExists ?? false,
|
||||
sourceCommit: mirrorSourceCommit,
|
||||
gitopsCommit: expectedGitopsRevision,
|
||||
alreadySynced: mirrorAlreadySynced,
|
||||
};
|
||||
const compactCiStatus = {
|
||||
namespaceExists: runtimePayload.ciNamespaceExists ?? false,
|
||||
serviceAccountExists: runtimePayload.serviceAccountExists ?? false,
|
||||
pipelineExists: pipeline.exists ?? false,
|
||||
pipelineRun: {
|
||||
name: pipelineRunName,
|
||||
exists: pipelineRunStatus.exists ?? false,
|
||||
status: pipelineRunStatus.status ?? null,
|
||||
reason: pipelineRunStatus.reason ?? null,
|
||||
sourceCommit: pipelineRunSourceCommit,
|
||||
},
|
||||
evidenceMissing: ciEvidenceMissing,
|
||||
blockers: ciBlockers,
|
||||
};
|
||||
const compactArgoStatus = {
|
||||
exists: argo.exists ?? false,
|
||||
application: argo.application ?? null,
|
||||
revision: stringOrNull(argo.revision),
|
||||
syncStatus: argo.syncStatus ?? null,
|
||||
healthStatus: argo.healthStatus ?? null,
|
||||
syncedToGitops: argoSyncedToGitops,
|
||||
};
|
||||
const compactRuntimeStatus = {
|
||||
namespaceExists: runtimePayload.runtimeNamespaceExists ?? false,
|
||||
manager: {
|
||||
deploymentExists: manager.deploymentExists ?? false,
|
||||
serviceExists: manager.serviceExists ?? false,
|
||||
sourceCommit: stringOrNull(manager.sourceCommit),
|
||||
sourceMatchesExpected: managerSourceMatchesExpected,
|
||||
},
|
||||
databaseSecretPresent: database.secretPresent ?? null,
|
||||
secretsReady: secrets.ready ?? null,
|
||||
localPostgresAbsent: localPostgres.absent ?? null,
|
||||
blockers: runtimeBlockers,
|
||||
};
|
||||
const detailedSummary = {
|
||||
aligned,
|
||||
runtimeAligned,
|
||||
ciEvidenceMissing,
|
||||
mirrorAlreadySynced,
|
||||
blockers,
|
||||
warnings,
|
||||
sourceCommit,
|
||||
expectedPipelineRun: pipelineRunName,
|
||||
expectedGitopsRevision,
|
||||
runtimeAlignment,
|
||||
branchDrift,
|
||||
source: {
|
||||
workspaceExists: sourcePayload.workspaceExists ?? false,
|
||||
workspaceClean: sourcePayload.workspaceClean ?? null,
|
||||
branch: sourcePayload.branch ?? null,
|
||||
workspaceDetached: sourceWorktreeDetached,
|
||||
localHead: sourcePayload.localHead ?? null,
|
||||
remoteBranchExists: sourcePayload.remoteBranchExists ?? false,
|
||||
remoteBranchCommit: sourcePayload.remoteBranchCommit ?? null,
|
||||
},
|
||||
gitMirror: {
|
||||
readReady: mirrorPayload.readReady ?? false,
|
||||
writeReady: mirrorPayload.writeReady ?? false,
|
||||
cachePvcExists: mirrorPayload.cachePvcExists ?? false,
|
||||
repositoryCount: Array.isArray(mirrorPayload.repositories) ? mirrorPayload.repositories.length : null,
|
||||
sourceCommit: mirrorSourceCommit,
|
||||
gitopsCommit: expectedGitopsRevision,
|
||||
alreadySynced: mirrorAlreadySynced,
|
||||
},
|
||||
ci: {
|
||||
namespaceExists: runtimePayload.ciNamespaceExists ?? false,
|
||||
serviceAccountExists: runtimePayload.serviceAccountExists ?? false,
|
||||
pipeline,
|
||||
pipelineRun: pipelineRunStatus,
|
||||
evidenceMissing: ciEvidenceMissing,
|
||||
blockers: ciBlockers,
|
||||
},
|
||||
argo,
|
||||
runtime: {
|
||||
namespaceExists: runtimePayload.runtimeNamespaceExists ?? false,
|
||||
manager: {
|
||||
deploymentExists: manager.deploymentExists ?? false,
|
||||
serviceExists: manager.serviceExists ?? false,
|
||||
deployment: manager.deployment ?? null,
|
||||
service: manager.service ?? null,
|
||||
image: manager.image ?? null,
|
||||
sourceCommit: stringOrNull(manager.sourceCommit),
|
||||
sourceMatchesExpected: managerSourceMatchesExpected,
|
||||
},
|
||||
database,
|
||||
secrets: compactLaneSecretsStatus(secrets),
|
||||
localPostgres,
|
||||
},
|
||||
nextAction,
|
||||
};
|
||||
const commanderSummary = {
|
||||
aligned,
|
||||
runtimeAligned,
|
||||
ciEvidenceMissing,
|
||||
mirrorAlreadySynced,
|
||||
blockers,
|
||||
warnings,
|
||||
sourceCommit,
|
||||
expectedPipelineRun: pipelineRunName,
|
||||
expectedGitopsRevision,
|
||||
runtimeAlignment,
|
||||
branchDrift,
|
||||
source: compactSourceStatus,
|
||||
gitMirror: compactGitMirrorStatus,
|
||||
ci: compactCiStatus,
|
||||
argo: compactArgoStatus,
|
||||
runtime: compactRuntimeStatus,
|
||||
nextAction,
|
||||
};
|
||||
const result: Record<string, unknown> = {
|
||||
ok: sourceProbe.value.exitCode === 0 && runtimeProbe.value.exitCode === 0 && mirrorProbe.value.exitCode === 0 && blockers.length === 0,
|
||||
command: "agentrun control-plane status",
|
||||
mode: "yaml-declared-node-lane",
|
||||
configPath: target.configPath,
|
||||
target: options.full || options.raw ? agentRunLaneSummary(spec) : compactAgentRunLaneStatusTarget(spec),
|
||||
summary: options.full || options.raw ? detailedSummary : commanderSummary,
|
||||
alignment: {
|
||||
aligned,
|
||||
runtimeAligned,
|
||||
ciEvidenceMissing,
|
||||
mirrorAlreadySynced,
|
||||
blockers,
|
||||
warnings,
|
||||
sourceCommit,
|
||||
expectedPipelineRun: pipelineRunName,
|
||||
expectedGitopsRevision,
|
||||
runtimeAlignment,
|
||||
branchDrift,
|
||||
},
|
||||
timings: {
|
||||
sourceMs: sourceProbe.elapsedMs,
|
||||
runtimeMs: runtimeProbe.elapsedMs,
|
||||
gitMirrorMs: mirrorProbe.elapsedMs,
|
||||
totalMs: sourceProbe.elapsedMs + Math.max(runtimeProbe.elapsedMs, mirrorProbe.elapsedMs),
|
||||
},
|
||||
disclosure: {
|
||||
output: options.full || options.raw ? "full" : "compact-summary",
|
||||
full: options.full,
|
||||
raw: options.raw,
|
||||
omittedWhenCompact: options.full || options.raw ? [] : ["full target YAML summary", "detailed source/runtime/gitMirror objects", "capture stdout/stderr tails for successful probes"],
|
||||
fullCommand: statusFullCommand,
|
||||
},
|
||||
next: {
|
||||
action: nextAction,
|
||||
plan: `bun scripts/cli.ts agentrun control-plane plan --node ${spec.nodeId} --lane ${spec.lane}`,
|
||||
postgresStatus: spec.database.configRef ? `bun scripts/cli.ts platform-db postgres status --config ${spec.database.configRef}` : null,
|
||||
postgresApply: spec.database.configRef ? `bun scripts/cli.ts platform-db postgres apply --config ${spec.database.configRef} --confirm` : null,
|
||||
secretSync: `bun scripts/cli.ts agentrun control-plane secret-sync --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
|
||||
restart: `bun scripts/cli.ts agentrun control-plane restart --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
|
||||
statusFull: statusFullCommand,
|
||||
triggerCurrent: ciEvidenceMissing || sourceBranchAdvanced ? triggerCurrentCommand : null,
|
||||
triggerLatest: sourceBranchAdvanced ? `bun scripts/cli.ts agentrun control-plane trigger-current --node ${spec.nodeId} --lane ${spec.lane} --confirm` : null,
|
||||
},
|
||||
valuesPrinted: false,
|
||||
};
|
||||
if (options.full || options.raw || sourceProbe.value.exitCode !== 0 || runtimeProbe.value.exitCode !== 0 || mirrorProbe.value.exitCode !== 0) {
|
||||
result.captures = {
|
||||
source: compactCapture(sourceProbe.value, { full: options.full || options.raw || sourceProbe.value.exitCode !== 0 }),
|
||||
runtime: compactCapture(runtimeProbe.value, { full: options.full || options.raw || runtimeProbe.value.exitCode !== 0 }),
|
||||
gitMirror: compactCapture(mirrorProbe.value, { full: options.full || options.raw || mirrorProbe.value.exitCode !== 0 }),
|
||||
};
|
||||
}
|
||||
if (options.full || options.raw) {
|
||||
result.source = sourcePayload;
|
||||
result.runtime = runtimePayload;
|
||||
result.gitMirror = mirrorPayload;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
Reference in New Issue
Block a user