fix: 增强 AgentRun control-plane status drill-down
This commit is contained in:
@@ -77,7 +77,7 @@ bun scripts/cli.ts agentrun v01 control-plane cleanup-released-pvs --limit 200 -
|
||||
bun scripts/cli.ts agentrun v01 control-plane cleanup-released-pvs --limit 200 --confirm
|
||||
```
|
||||
|
||||
`status` 只读观察 `G14:/root/agentrun-v01` 当前 commit、对应 PipelineRun、GitOps latest、Argo Application、`agentrun-v01` workload、manager source commit 和 git mirror 摘要,并报告 Argo revision 是否对齐 `v0.1-gitops` latest。默认输出是 compact commander 视图,只保留 `summary`、阶段耗时、对齐状态和 drill-down 命令;需要远端 stdout/stderr tail 时显式加 `--full`,需要原始 git mirror cache 输出时显式加 `--raw`。`status` 会向 stderr 输出 `agentrun.control-plane.status.progress` 阶段事件,覆盖 `source`、`runtime` 和 `git-mirror`,避免长时间聚合时无可见进展。`trigger-current` 会先把固定 source worktree 快进到 `origin/v0.1`,再以当前 commit 创建 commit-pinned PipelineRun;同名 PipelineRun 正在运行或已经成功时必须拒绝重复触发,只允许在失败态或不存在时创建。该命令只提交 CI/CD 工作,不等待完整 PipelineRun 或 rollout 完成,后续用 `status` 轮询。`refresh` 只对 `argocd/agentrun-g14-v01` 执行 hard refresh,用于 GitOps promotion 已完成但 Argo 仍停留旧 revision 时的受控同步入口;它不直接 patch runtime workload。
|
||||
`status` 只读观察 `G14:/root/agentrun-v01` 当前 commit、对应 PipelineRun、GitOps latest、Argo Application、`agentrun-v01` workload、manager source commit 和 git mirror 摘要,并报告 Argo revision 是否对齐 `v0.1-gitops` latest。默认输出是 compact commander 视图,只保留 `summary`、阶段耗时、对齐状态和 drill-down 命令;需要远端 stdout/stderr tail 时显式加 `--full`,需要原始 git mirror cache 输出时显式加 `--raw`。`status` 额外支持 `--pipeline-run <name>` 与 `--source-commit <sha>` 定点查询,返回 `target`、`targetValidation` 和 `next.*` drill-down,便于直接判断某次 run 是成功、历史成功、运行中、缺失还是 source mismatch。`status` 会向 stderr 输出 `agentrun.control-plane.status.progress` 阶段事件,覆盖 `source`、`runtime` 和 `git-mirror`,避免长时间聚合时无可见进展。`trigger-current` 会先把固定 source worktree 快进到 `origin/v0.1`,再以当前 commit 创建 commit-pinned PipelineRun;同名 PipelineRun 正在运行或已经成功时必须拒绝重复触发,只允许在失败态或不存在时创建。该命令只提交 CI/CD 工作,不等待完整 PipelineRun 或 rollout 完成,后续用 `status` 轮询。`refresh` 只对 `argocd/agentrun-g14-v01` 执行 hard refresh,用于 GitOps promotion 已完成但 Argo 仍停留旧 revision 时的受控同步入口;它不直接 patch runtime workload。
|
||||
|
||||
`cleanup-runs` 是 AgentRun `v0.1` 完成态 CI workspace retention 入口,只清理 `agentrun-ci` namespace 中超过 `--min-age-minutes` 的 `agentrun-v01-ci-*` PipelineRun,通过 Tekton ownerRef 释放临时 workspace PVC。dry-run 必须披露候选 PipelineRun、owned PVC、active mount 保护、local-path 实际估算 bytes 和 confirm 命令。默认保护最新完成的 PipelineRun,保留当前 CI/CD 状态证据。`cleanup-released-pvs` 是二次回收入口,只处理 `agentrun-ci`、`local-path`、`Delete` reclaim policy 的 `Released` PV;它不触碰 `agentrun-v01` runtime namespace、业务 PVC、Secret、registry storage 或 GitOps desired state。磁盘治理和 G14 safe-stop 规则见 `docs/reference/gc.md`。
|
||||
|
||||
|
||||
@@ -19,6 +19,13 @@ assertCondition(
|
||||
agentRunUsage,
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
agentRunUsage.some((line) => line.includes("control-plane status --pipeline-run agentrun-v01-ci-<short-sha>"))
|
||||
&& agentRunUsage.some((line) => line.includes("control-plane status --source-commit <full-sha>")),
|
||||
"AgentRun help must expose targeted control-plane status drill-down options",
|
||||
agentRunUsage,
|
||||
);
|
||||
|
||||
assertCondition(
|
||||
agentRunUsage.some((line) => line.includes("queue submit --json-stdin --dry-run"))
|
||||
&& agentRunUsage.some((line) => line.includes("queue dispatch <taskId> --json-stdin --dry-run"))
|
||||
@@ -46,7 +53,7 @@ assertCondition(
|
||||
const globalHelp = JSON.stringify(rootHelp());
|
||||
|
||||
assertCondition(
|
||||
globalHelp.includes("agentrun v01 queue|sessions|control-plane|git-mirror"),
|
||||
globalHelp.includes("agentrun v01 aipod-specs|queue|sessions|control-plane|git-mirror"),
|
||||
"global help must index AgentRun v0.1 entrypoints",
|
||||
rootHelp(),
|
||||
);
|
||||
@@ -54,14 +61,9 @@ assertCondition(
|
||||
const agentRunSource = readFileSync("scripts/src/agentrun.ts", "utf8");
|
||||
const runtimeJsonFallback = "\"node <<'NODE' || printf '{}\\\\n'\"";
|
||||
|
||||
function statusScriptSection(label: string): string {
|
||||
const start = agentRunSource.indexOf(`printf '${label}='`);
|
||||
return start >= 0 ? agentRunSource.slice(start, start + 500) : "";
|
||||
}
|
||||
|
||||
assertCondition(
|
||||
statusScriptSection("pipelineRunCondition").includes(runtimeJsonFallback)
|
||||
&& statusScriptSection("ciSummary").includes(runtimeJsonFallback),
|
||||
agentRunSource.includes(runtimeJsonFallback)
|
||||
&& agentRunSource.includes("const sourceCommit = params.find((entry) => entry?.name === 'revision')?.value || null;"),
|
||||
"AgentRun control-plane status must degrade empty runtime JSON snippets instead of failing the whole status probe",
|
||||
);
|
||||
|
||||
@@ -69,6 +71,7 @@ console.log(JSON.stringify({
|
||||
ok: true,
|
||||
checks: [
|
||||
"AgentRun command help exposes cleanup-runs and cleanup-released-pvs",
|
||||
"AgentRun command help exposes targeted control-plane status drill-down options",
|
||||
"AgentRun command help exposes queue dry-run and compact commander usage",
|
||||
"AgentRun command help presents heredoc/stdin before reusable file fallbacks",
|
||||
"global help indexes AgentRun v0.1 entrypoints",
|
||||
|
||||
+176
-7
@@ -48,6 +48,8 @@ export function agentRunHelp(): unknown {
|
||||
"bun scripts/cli.ts agentrun v01 sessions read <sessionId> --reader-id cli",
|
||||
"bun scripts/cli.ts agentrun v01 control-plane status",
|
||||
"bun scripts/cli.ts agentrun v01 control-plane status --full",
|
||||
"bun scripts/cli.ts agentrun v01 control-plane status --pipeline-run agentrun-v01-ci-<short-sha>",
|
||||
"bun scripts/cli.ts agentrun v01 control-plane status --source-commit <full-sha>",
|
||||
"bun scripts/cli.ts agentrun v01 control-plane trigger-current --dry-run",
|
||||
"bun scripts/cli.ts agentrun v01 control-plane trigger-current --confirm",
|
||||
"bun scripts/cli.ts agentrun v01 control-plane refresh --dry-run",
|
||||
@@ -69,7 +71,7 @@ export async function runAgentRunCommand(config: UniDeskConfig, args: string[]):
|
||||
const [lane, group, action] = args;
|
||||
if (lane !== "v01") return unsupported(args);
|
||||
if (group === "control-plane") {
|
||||
if (action === "status") return await status(config, parseDisclosureOptions(args.slice(3)));
|
||||
if (action === "status") return await status(config, parseStatusOptions(args.slice(3)));
|
||||
if (action === "trigger-current") return await triggerCurrent(config, parseTriggerOptions(args.slice(3)));
|
||||
if (action === "refresh") return await refresh(config, parseConfirmOptions(args.slice(3)));
|
||||
if (action === "cleanup-runs") return await cleanupRuns(config, parseCleanupRunsOptions(args.slice(3)));
|
||||
@@ -124,6 +126,12 @@ interface DisclosureOptions {
|
||||
raw: boolean;
|
||||
}
|
||||
|
||||
interface StatusOptions extends DisclosureOptions {
|
||||
sourceCommit: string | null;
|
||||
pipelineRun: string | null;
|
||||
targetMode: "latest-source-head" | "source-commit" | "pipeline-run";
|
||||
}
|
||||
|
||||
interface TimedValue<T> {
|
||||
value: T;
|
||||
elapsedMs: number;
|
||||
@@ -137,6 +145,50 @@ function parseDisclosureOptions(args: string[]): DisclosureOptions {
|
||||
return { full: raw || args.includes("--full"), raw };
|
||||
}
|
||||
|
||||
function parseStatusOptions(args: string[]): StatusOptions {
|
||||
let sourceCommit: string | null = null;
|
||||
let pipelineRun: string | null = null;
|
||||
let full = false;
|
||||
let raw = false;
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
const arg = args[index];
|
||||
if (arg === "--full") {
|
||||
full = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--raw") {
|
||||
raw = true;
|
||||
full = true;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--source-commit") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--source-commit requires a value");
|
||||
if (!isGitSha(value)) throw new Error("--source-commit must be a full 40-character git SHA");
|
||||
sourceCommit = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--pipeline-run") {
|
||||
const value = args[index + 1];
|
||||
if (value === undefined || value.startsWith("--")) throw new Error("--pipeline-run requires a value");
|
||||
if (!isAgentRunPipelineRunName(value)) throw new Error("--pipeline-run must be an agentrun-v01-ci-<12+ hex> PipelineRun name");
|
||||
pipelineRun = value;
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
throw new Error(`unsupported status option: ${arg}`);
|
||||
}
|
||||
if (sourceCommit !== null && pipelineRun !== null) throw new Error("control-plane status accepts only one of --source-commit or --pipeline-run");
|
||||
return {
|
||||
full,
|
||||
raw,
|
||||
sourceCommit,
|
||||
pipelineRun,
|
||||
targetMode: pipelineRun !== null ? "pipeline-run" : sourceCommit !== null ? "source-commit" : "latest-source-head",
|
||||
};
|
||||
}
|
||||
|
||||
function parseTriggerOptions(args: string[]): TriggerOptions {
|
||||
return parseConfirmOptions(args);
|
||||
}
|
||||
@@ -208,7 +260,7 @@ function positiveIntegerOption(args: string[], name: string, defaultValue: numbe
|
||||
return Math.min(value, maxValue);
|
||||
}
|
||||
|
||||
async function status(config: UniDeskConfig, options: DisclosureOptions): Promise<Record<string, unknown>> {
|
||||
async function status(config: UniDeskConfig, options: StatusOptions): Promise<Record<string, unknown>> {
|
||||
const sourceProbe = await timedStatusStage("source", () => capture(config, g14SourceRoute, ["script", "--", [
|
||||
"cd /root/agentrun-v01",
|
||||
"git fetch origin v0.1 >/dev/null 2>&1 || true",
|
||||
@@ -223,9 +275,17 @@ async function status(config: UniDeskConfig, options: DisclosureOptions): Promis
|
||||
const source = sourceProbe.value;
|
||||
const localSourceCommit = matchLine(source.stdout, "sourceCommit=");
|
||||
const originSourceCommit = matchLine(source.stdout, "originV01=");
|
||||
const sourceCommit = isGitSha(originSourceCommit ?? "") ? originSourceCommit : localSourceCommit;
|
||||
const latestSourceCommit = isGitSha(originSourceCommit ?? "") ? originSourceCommit : localSourceCommit;
|
||||
const gitopsLatest = matchLine(source.stdout, "gitopsLatest=");
|
||||
const pipelineRun = sourceCommit ? pipelineRunName(sourceCommit) : null;
|
||||
let sourceCommit = options.sourceCommit ?? (options.pipelineRun !== null ? null : latestSourceCommit);
|
||||
let sourceCommitSource = options.sourceCommit !== null
|
||||
? "option"
|
||||
: options.pipelineRun !== null
|
||||
? "pipeline-run-param"
|
||||
: sourceCommit === originSourceCommit
|
||||
? "origin/v0.1"
|
||||
: "local-head";
|
||||
const pipelineRun = options.pipelineRun ?? (sourceCommit ? pipelineRunName(sourceCommit) : null);
|
||||
const [runtimeProbe, mirrorProbe] = await Promise.all([
|
||||
timedStatusStage("runtime", () => capture(config, g14K3sRoute, ["script", "--", statusScript(pipelineRun)])),
|
||||
timedStatusStage("git-mirror", () => readGitMirrorStatus(config)),
|
||||
@@ -237,6 +297,22 @@ async function status(config: UniDeskConfig, options: DisclosureOptions): Promis
|
||||
const pipelineRunCondition = labeledJson(k3s.stdout, "pipelineRunCondition");
|
||||
const managerImage = parseManagerImage(k3s.stdout);
|
||||
const mirrorSummary = mirror.summary;
|
||||
const pipelineRunSourceCommit = stringOrNull(pipelineRunCondition.sourceCommit);
|
||||
if (options.pipelineRun !== null && sourceCommit === null && pipelineRunSourceCommit !== null && isGitSha(pipelineRunSourceCommit)) {
|
||||
sourceCommit = pipelineRunSourceCommit;
|
||||
sourceCommitSource = "pipeline-run-param";
|
||||
}
|
||||
const target = {
|
||||
mode: options.targetMode,
|
||||
sourceCommit,
|
||||
sourceCommitSource,
|
||||
pipelineRun,
|
||||
pipelineRunSourceCommit,
|
||||
latestSourceCommit,
|
||||
latestPipelineRun: latestSourceCommit ? pipelineRunName(latestSourceCommit) : null,
|
||||
isLatestSource: Boolean(sourceCommit && latestSourceCommit && sourceCommit === latestSourceCommit),
|
||||
sourceMatchesPipelineRun: sourceCommit === null || pipelineRunSourceCommit === null ? null : sourceCommit === pipelineRunSourceCommit,
|
||||
};
|
||||
const runtimeAlignment = {
|
||||
localHeadMatchesOrigin: Boolean(localSourceCommit && originSourceCommit && localSourceCommit === originSourceCommit),
|
||||
argoRevision: argo.revision,
|
||||
@@ -246,7 +322,15 @@ async function status(config: UniDeskConfig, options: DisclosureOptions): Promis
|
||||
managerSourceCommit: managerImage.sourceCommit,
|
||||
managerSourceMatchesExpected: Boolean(sourceCommit && managerImage.sourceCommit === sourceCommit),
|
||||
};
|
||||
const targetValidation = buildAgentRunTargetValidation({
|
||||
pipelineRun,
|
||||
pipelineRunCondition,
|
||||
sourceCommit,
|
||||
targetIsLatestSource: target.isLatestSource,
|
||||
managerSourceMatchesExpected: runtimeAlignment.managerSourceMatchesExpected,
|
||||
});
|
||||
const summary = {
|
||||
target,
|
||||
sourceCommit,
|
||||
expectedPipelineRun: pipelineRun,
|
||||
pipelineRun: {
|
||||
@@ -254,6 +338,7 @@ async function status(config: UniDeskConfig, options: DisclosureOptions): Promis
|
||||
reason: pipelineRunCondition.reason ?? null,
|
||||
completionTime: pipelineRunCondition.completionTime ?? null,
|
||||
},
|
||||
targetValidation,
|
||||
argo,
|
||||
managerImage,
|
||||
gitMirror: {
|
||||
@@ -278,8 +363,10 @@ async function status(config: UniDeskConfig, options: DisclosureOptions): Promis
|
||||
command: "agentrun v01 control-plane status",
|
||||
lane: "v0.1",
|
||||
summary,
|
||||
target,
|
||||
sourceCommit,
|
||||
sourceCommitSource: sourceCommit === originSourceCommit ? "origin/v0.1" : "local-head",
|
||||
sourceCommitSource,
|
||||
latestSourceCommit,
|
||||
localSourceCommit,
|
||||
originSourceCommit,
|
||||
gitopsLatest,
|
||||
@@ -303,16 +390,21 @@ async function status(config: UniDeskConfig, options: DisclosureOptions): Promis
|
||||
...(options.raw ? { raw: mirror.raw } : {}),
|
||||
},
|
||||
runtimeAlignment,
|
||||
targetValidation,
|
||||
disclosure: {
|
||||
defaultView: "compact-low-noise",
|
||||
full: options.full,
|
||||
raw: options.raw,
|
||||
stdoutTailOmitted: !(options.full || options.raw),
|
||||
rawGitMirrorOmitted: !options.raw,
|
||||
expandWith: "bun scripts/cli.ts agentrun v01 control-plane status --full",
|
||||
rawWith: "bun scripts/cli.ts agentrun v01 control-plane status --raw",
|
||||
expandWith: `bun scripts/cli.ts agentrun v01 control-plane status${statusTargetArg(options, target)} --full`,
|
||||
rawWith: `bun scripts/cli.ts agentrun v01 control-plane status${statusTargetArg(options, target)} --raw`,
|
||||
},
|
||||
next: {
|
||||
statusByPipelineRun: pipelineRun ? `bun scripts/cli.ts agentrun v01 control-plane status --pipeline-run ${pipelineRun} --full` : null,
|
||||
statusBySourceCommit: sourceCommit ? `bun scripts/cli.ts agentrun v01 control-plane status --source-commit ${sourceCommit} --full` : null,
|
||||
taskRuns: pipelineRun ? `trans G14:k3s kubectl get taskrun -n ${ciNamespace} -l tekton.dev/pipelineRun=${pipelineRun} -o wide` : null,
|
||||
logs: pipelineRun ? `trans G14:k3s logs -n ${ciNamespace} -l tekton.dev/pipelineRun=${pipelineRun} --tail 120` : null,
|
||||
triggerCurrent: "bun scripts/cli.ts agentrun v01 control-plane trigger-current --confirm",
|
||||
refresh: "bun scripts/cli.ts agentrun v01 control-plane refresh --confirm",
|
||||
},
|
||||
@@ -935,8 +1027,12 @@ function statusScript(pipelineRun: string | null): string {
|
||||
"const fs = require('node:fs');",
|
||||
"const pr = JSON.parse(fs.readFileSync('/tmp/agentrun-v01-pipelinerun.json', 'utf8'));",
|
||||
"const condition = pr?.status?.conditions?.[0] || {};",
|
||||
"const params = Array.isArray(pr?.spec?.params) ? pr.spec.params : [];",
|
||||
"const sourceCommit = params.find((entry) => entry?.name === 'revision')?.value || null;",
|
||||
"console.log(JSON.stringify({",
|
||||
" name: pr?.metadata?.name || null,",
|
||||
" sourceCommit,",
|
||||
" createdAt: pr?.metadata?.creationTimestamp || null,",
|
||||
" status: condition.status || null,",
|
||||
" reason: condition.reason || null,",
|
||||
" message: condition.message || null,",
|
||||
@@ -1709,6 +1805,79 @@ function pipelineRunName(sourceCommit: string): string {
|
||||
return `agentrun-v01-ci-${sourceCommit.slice(0, 12)}`;
|
||||
}
|
||||
|
||||
function isAgentRunPipelineRunName(value: string): boolean {
|
||||
return /^agentrun-v01-ci-[0-9a-f]{12,40}(?:-[a-z0-9-]+)?$/u.test(value);
|
||||
}
|
||||
|
||||
function statusTargetArg(options: StatusOptions, target: Record<string, unknown>): string {
|
||||
if (options.pipelineRun !== null) return ` --pipeline-run ${options.pipelineRun}`;
|
||||
if (options.sourceCommit !== null) return ` --source-commit ${options.sourceCommit}`;
|
||||
const sourceCommit = stringOrNull(target.sourceCommit);
|
||||
return sourceCommit ? ` --source-commit ${sourceCommit}` : "";
|
||||
}
|
||||
|
||||
function buildAgentRunTargetValidation(input: {
|
||||
pipelineRun: string | null;
|
||||
pipelineRunCondition: Record<string, unknown>;
|
||||
sourceCommit: string | null;
|
||||
targetIsLatestSource: boolean;
|
||||
managerSourceMatchesExpected: boolean;
|
||||
}): Record<string, unknown> {
|
||||
const pipelineRunExists = input.pipelineRun !== null && stringOrNull(input.pipelineRunCondition.name) !== null;
|
||||
const pipelineStatus = stringOrNull(input.pipelineRunCondition.status);
|
||||
const pipelineReason = stringOrNull(input.pipelineRunCondition.reason);
|
||||
const pipelineRunSourceCommit = stringOrNull(input.pipelineRunCondition.sourceCommit);
|
||||
const sourceMatchesPipelineRun = input.sourceCommit === null || pipelineRunSourceCommit === null ? null : input.sourceCommit === pipelineRunSourceCommit;
|
||||
let state = "unknown";
|
||||
const blockers: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
if (input.pipelineRun === null) {
|
||||
blockers.push("pipeline-run-unresolved");
|
||||
} else if (!pipelineRunExists) {
|
||||
state = "missing-pipelinerun";
|
||||
blockers.push("pipeline-run-not-found");
|
||||
} else if (sourceMatchesPipelineRun === false) {
|
||||
state = "source-mismatch";
|
||||
blockers.push("source-commit-does-not-match-pipelinerun-param");
|
||||
} else if (pipelineStatus === "True") {
|
||||
state = input.targetIsLatestSource && input.managerSourceMatchesExpected ? "current-succeeded" : "historical-succeeded";
|
||||
} else if (pipelineStatus === "False") {
|
||||
state = "pipeline-failed";
|
||||
blockers.push(pipelineReason ?? "pipeline-failed");
|
||||
} else if (pipelineStatus === "Unknown") {
|
||||
state = "pipeline-running";
|
||||
} else {
|
||||
state = "pipeline-status-unknown";
|
||||
warnings.push("pipeline-condition-missing");
|
||||
}
|
||||
|
||||
if (pipelineRunExists && input.targetIsLatestSource && !input.managerSourceMatchesExpected) {
|
||||
warnings.push("runtime-manager-source-not-yet-aligned");
|
||||
}
|
||||
if (pipelineRunExists && !input.targetIsLatestSource) {
|
||||
warnings.push("target-is-not-latest-source-head");
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
pipelineRunExists,
|
||||
pipelineStatus,
|
||||
pipelineReason,
|
||||
sourceMatchesPipelineRun,
|
||||
targetIsLatestSource: input.targetIsLatestSource,
|
||||
managerSourceMatchesExpected: input.managerSourceMatchesExpected,
|
||||
blockers,
|
||||
warnings,
|
||||
interruptedOrUnknown: state === "pipeline-status-unknown" || state === "missing-pipelinerun",
|
||||
nextActions: {
|
||||
inspectPipelineRun: input.pipelineRun ? `bun scripts/cli.ts agentrun v01 control-plane status --pipeline-run ${input.pipelineRun} --full` : null,
|
||||
inspectSourceCommit: input.sourceCommit ? `bun scripts/cli.ts agentrun v01 control-plane status --source-commit ${input.sourceCommit} --full` : null,
|
||||
triggerCurrent: "bun scripts/cli.ts agentrun v01 control-plane trigger-current --confirm",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function matchLine(text: string, prefix: string): string | null {
|
||||
const line = text.split(/\r?\n/u).find((item) => item.startsWith(prefix));
|
||||
return line ? line.slice(prefix.length).trim() || null : null;
|
||||
|
||||
Reference in New Issue
Block a user