616 lines
28 KiB
TypeScript
616 lines
28 KiB
TypeScript
// 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.cacheReady === true ? [] : ["git-mirror-cache-not-ready"]),
|
|
...(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,
|
|
cacheMode: mirrorPayload.cacheMode ?? null,
|
|
cacheReady: mirrorPayload.cacheReady ?? false,
|
|
cachePvcExists: mirrorPayload.cachePvcExists ?? false,
|
|
cacheHostPath: mirrorPayload.cacheHostPath ?? null,
|
|
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,
|
|
cacheMode: mirrorPayload.cacheMode ?? null,
|
|
cacheReady: mirrorPayload.cacheReady ?? false,
|
|
cachePvcExists: mirrorPayload.cachePvcExists ?? false,
|
|
cacheHostPath: mirrorPayload.cacheHostPath ?? null,
|
|
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;
|
|
}
|