diff --git a/docs/reference/agentrun.md b/docs/reference/agentrun.md index d1ac2796..8e99843c 100644 --- a/docs/reference/agentrun.md +++ b/docs/reference/agentrun.md @@ -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 ` 与 `--source-commit ` 定点查询,返回 `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`。 diff --git a/scripts/agentrun-cli-contract-test.ts b/scripts/agentrun-cli-contract-test.ts index d3fe11ba..97ed2b35 100644 --- a/scripts/agentrun-cli-contract-test.ts +++ b/scripts/agentrun-cli-contract-test.ts @@ -19,6 +19,13 @@ assertCondition( agentRunUsage, ); +assertCondition( + agentRunUsage.some((line) => line.includes("control-plane status --pipeline-run agentrun-v01-ci-")) + && agentRunUsage.some((line) => line.includes("control-plane status --source-commit ")), + "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 --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", diff --git a/scripts/src/agentrun.ts b/scripts/src/agentrun.ts index bb580724..7588db7d 100644 --- a/scripts/src/agentrun.ts +++ b/scripts/src/agentrun.ts @@ -48,6 +48,8 @@ export function agentRunHelp(): unknown { "bun scripts/cli.ts agentrun v01 sessions read --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-", + "bun scripts/cli.ts agentrun v01 control-plane status --source-commit ", "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 { 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> { +async function status(config: UniDeskConfig, options: StatusOptions): Promise> { 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 { + 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; + sourceCommit: string | null; + targetIsLatestSource: boolean; + managerSourceMatchesExpected: boolean; +}): Record { + 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;