feat: add AgentRun v0.1 control CLI
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
import type { UniDeskConfig } from "./config";
|
||||
import { runSshCommandCapture, type SshCaptureResult } from "./ssh";
|
||||
|
||||
const g14SourceRoute = "G14:/root/agentrun-v01";
|
||||
const g14K3sRoute = "G14:k3s";
|
||||
const sourceBranch = "v0.1";
|
||||
const runtimeNamespace = "agentrun-v01";
|
||||
const ciNamespace = "agentrun-ci";
|
||||
const pipelineName = "agentrun-v01-ci-image-publish";
|
||||
|
||||
export function agentRunHelp(): unknown {
|
||||
return {
|
||||
command: "agentrun v01 control-plane status|trigger-current",
|
||||
output: "json",
|
||||
usage: [
|
||||
"bun scripts/cli.ts agentrun v01 control-plane status",
|
||||
"bun scripts/cli.ts agentrun v01 control-plane trigger-current --dry-run",
|
||||
"bun scripts/cli.ts agentrun v01 control-plane trigger-current --confirm",
|
||||
],
|
||||
description: "Operate AgentRun v0.1 Tekton/Argo control plane through G14 routes; trigger-current is short-return and status is read-only.",
|
||||
};
|
||||
}
|
||||
|
||||
export async function runAgentRunCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown>> {
|
||||
const [lane, group, action] = args;
|
||||
if (lane !== "v01" || group !== "control-plane") return unsupported(args);
|
||||
if (action === "status") return await status(config);
|
||||
if (action === "trigger-current") return await triggerCurrent(config, parseTriggerOptions(args.slice(3)));
|
||||
return unsupported(args);
|
||||
}
|
||||
|
||||
interface TriggerOptions {
|
||||
confirm: boolean;
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
function parseTriggerOptions(args: string[]): TriggerOptions {
|
||||
return {
|
||||
confirm: args.includes("--confirm"),
|
||||
dryRun: args.includes("--dry-run") || !args.includes("--confirm"),
|
||||
};
|
||||
}
|
||||
|
||||
async function status(config: UniDeskConfig): Promise<Record<string, unknown>> {
|
||||
const source = await capture(config, g14SourceRoute, ["script", "--", [
|
||||
"cd /root/agentrun-v01",
|
||||
"git fetch origin v0.1 >/dev/null 2>&1 || true",
|
||||
"printf 'sourceCommit='",
|
||||
"git rev-parse HEAD",
|
||||
"printf 'originV01='",
|
||||
"git rev-parse origin/v0.1 2>/dev/null || true",
|
||||
"git status --short --branch",
|
||||
].join("\n")]);
|
||||
const sourceCommit = matchLine(source.stdout, "sourceCommit=");
|
||||
const pipelineRun = sourceCommit ? pipelineRunName(sourceCommit) : null;
|
||||
const k3s = await capture(config, g14K3sRoute, ["script", "--", statusScript(pipelineRun)]);
|
||||
return {
|
||||
ok: source.exitCode === 0 && k3s.exitCode === 0,
|
||||
command: "agentrun v01 control-plane status",
|
||||
lane: "v0.1",
|
||||
sourceCommit,
|
||||
expectedPipelineRun: pipelineRun,
|
||||
source: compactCapture(source),
|
||||
runtime: compactCapture(k3s),
|
||||
next: { triggerCurrent: "bun scripts/cli.ts agentrun v01 control-plane trigger-current --confirm" },
|
||||
};
|
||||
}
|
||||
|
||||
async function triggerCurrent(config: UniDeskConfig, options: TriggerOptions): Promise<Record<string, unknown>> {
|
||||
const source = await capture(config, g14SourceRoute, ["script", "--", [
|
||||
"set -eu",
|
||||
"cd /root/agentrun-v01",
|
||||
"git fetch origin v0.1",
|
||||
"git pull --ff-only origin v0.1",
|
||||
"printf 'sourceCommit='",
|
||||
"git rev-parse HEAD",
|
||||
].join("\n")]);
|
||||
const sourceCommit = matchLine(source.stdout, "sourceCommit=");
|
||||
const pipelineRun = sourceCommit ? pipelineRunName(sourceCommit) : null;
|
||||
if (source.exitCode !== 0 || !sourceCommit || !isGitSha(sourceCommit) || !pipelineRun) {
|
||||
return { ok: false, command: "agentrun v01 control-plane trigger-current", degradedReason: "source-head-unresolved", source: compactCapture(source) };
|
||||
}
|
||||
const plan = {
|
||||
lane: "v0.1",
|
||||
sourceBranch,
|
||||
sourceCommit,
|
||||
pipelineRun,
|
||||
namespace: ciNamespace,
|
||||
pipeline: pipelineName,
|
||||
runtimeNamespace,
|
||||
};
|
||||
if (options.dryRun || !options.confirm) {
|
||||
return {
|
||||
ok: true,
|
||||
command: "agentrun v01 control-plane trigger-current",
|
||||
dryRun: true,
|
||||
plan,
|
||||
next: { confirm: "bun scripts/cli.ts agentrun v01 control-plane trigger-current --confirm" },
|
||||
};
|
||||
}
|
||||
const created = await capture(config, g14K3sRoute, ["script", "--", triggerScript(sourceCommit, pipelineRun)]);
|
||||
return {
|
||||
ok: created.exitCode === 0,
|
||||
command: "agentrun v01 control-plane trigger-current",
|
||||
dryRun: false,
|
||||
plan,
|
||||
created: compactCapture(created),
|
||||
next: {
|
||||
status: "bun scripts/cli.ts agentrun v01 control-plane status",
|
||||
logs: `bun scripts/cli.ts ssh G14:k3s logs -n ${ciNamespace} -l tekton.dev/pipelineRun=${pipelineRun} --tail 120`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function statusScript(pipelineRun: string | null): string {
|
||||
const pr = pipelineRun ?? "";
|
||||
return [
|
||||
"set -eu",
|
||||
"printf 'pipelineRun\\tstatus\\treason\\tstart\\tcompletion\\n'",
|
||||
pr.length > 0
|
||||
? `kubectl -n ${ciNamespace} get pipelinerun ${shQuote(pr)} -o 'jsonpath={.metadata.name}{\"\\t\"}{.status.conditions[0].status}{\"\\t\"}{.status.conditions[0].reason}{\"\\t\"}{.status.startTime}{\"\\t\"}{.status.completionTime}{\"\\n\"}' 2>/dev/null || true`
|
||||
: "true",
|
||||
"printf 'recentPipelineRuns\\n'",
|
||||
`kubectl -n ${ciNamespace} get pipelinerun --sort-by=.metadata.creationTimestamp -o 'custom-columns=NAME:.metadata.name,STATUS:.status.conditions[0].status,REASON:.status.conditions[0].reason,CREATED:.metadata.creationTimestamp' --no-headers 2>/dev/null | tail -n 5 || true`,
|
||||
"printf 'argo\\n'",
|
||||
"kubectl -n argocd get application agentrun-g14-v01 -o 'jsonpath={.status.sync.revision}{\"\\t\"}{.status.sync.status}{\"\\t\"}{.status.health.status}{\"\\n\"}' 2>/dev/null || true",
|
||||
"printf 'workloads\\n'",
|
||||
`kubectl -n ${runtimeNamespace} get deploy,sts,pod -o wide 2>/dev/null || true`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function triggerScript(sourceCommit: string, pipelineRun: string): string {
|
||||
return [
|
||||
"set -eu",
|
||||
"cd /root/agentrun-v01",
|
||||
`if kubectl -n ${ciNamespace} get pipelinerun ${shQuote(pipelineRun)} >/dev/null 2>&1; then`,
|
||||
` existing_status="$(kubectl -n ${ciNamespace} get pipelinerun ${shQuote(pipelineRun)} -o 'jsonpath={.status.conditions[0].status}:{.status.conditions[0].reason}' 2>/dev/null || true)"`,
|
||||
" case \"$existing_status\" in",
|
||||
" False:*)",
|
||||
" printf 'deleteExisting=%s\\n' \"$existing_status\"",
|
||||
` kubectl -n ${ciNamespace} delete pipelinerun ${shQuote(pipelineRun)} --wait=true --timeout=20s`,
|
||||
" ;;",
|
||||
" *)",
|
||||
" printf 'refuseExisting=%s\\n' \"$existing_status\"",
|
||||
" printf 'reason=existing-pipelinerun-active-or-succeeded\\n'",
|
||||
" exit 20",
|
||||
" ;;",
|
||||
" esac",
|
||||
"fi",
|
||||
"kubectl apply -f deploy/templates/tekton/rbac.yaml",
|
||||
"kubectl apply -f deploy/templates/tekton/pipeline.yaml",
|
||||
"kubectl apply -f deploy/templates/argocd/project.yaml",
|
||||
"kubectl apply -f deploy/templates/argocd/application-v01.yaml",
|
||||
"cat <<'YAML' | kubectl create -f -",
|
||||
"apiVersion: tekton.dev/v1",
|
||||
"kind: PipelineRun",
|
||||
"metadata:",
|
||||
` name: ${pipelineRun}`,
|
||||
` namespace: ${ciNamespace}`,
|
||||
"spec:",
|
||||
" pipelineRef:",
|
||||
` name: ${pipelineName}`,
|
||||
" taskRunTemplate:",
|
||||
" serviceAccountName: agentrun-v01-tekton-runner",
|
||||
" podTemplate:",
|
||||
" hostNetwork: true",
|
||||
" dnsPolicy: ClusterFirstWithHostNet",
|
||||
" securityContext:",
|
||||
" fsGroup: 1000",
|
||||
" params:",
|
||||
" - name: revision",
|
||||
` value: ${sourceCommit}`,
|
||||
" workspaces:",
|
||||
" - name: source",
|
||||
" volumeClaimTemplate:",
|
||||
" spec:",
|
||||
" accessModes: [\"ReadWriteOnce\"]",
|
||||
" resources:",
|
||||
" requests:",
|
||||
" storage: 5Gi",
|
||||
" - name: git-ssh",
|
||||
" secret:",
|
||||
" secretName: agentrun-git-ssh",
|
||||
"YAML",
|
||||
`printf 'created=${pipelineRun}\\n'`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
async function capture(config: UniDeskConfig, target: string, args: string[]): Promise<SshCaptureResult> {
|
||||
return await runSshCommandCapture(config, target, args);
|
||||
}
|
||||
|
||||
function compactCapture(result: SshCaptureResult): Record<string, unknown> {
|
||||
return {
|
||||
exitCode: result.exitCode,
|
||||
stdoutTail: tail(result.stdout, 8000),
|
||||
stderrTail: tail(result.stderr, 4000),
|
||||
};
|
||||
}
|
||||
|
||||
function pipelineRunName(sourceCommit: string): string {
|
||||
return `agentrun-v01-ci-${sourceCommit.slice(0, 12)}`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function isGitSha(value: string): boolean {
|
||||
return /^[0-9a-f]{40}$/u.test(value);
|
||||
}
|
||||
|
||||
function tail(text: string, maxChars: number): string {
|
||||
return text.length > maxChars ? text.slice(-maxChars) : text;
|
||||
}
|
||||
|
||||
function shQuote(value: string): string {
|
||||
return `'${value.replace(/'/gu, "'\\''")}'`;
|
||||
}
|
||||
|
||||
function unsupported(args: string[]): Record<string, unknown> {
|
||||
return {
|
||||
ok: false,
|
||||
command: `agentrun ${args.join(" ")}`.trim(),
|
||||
degradedReason: "unsupported-command",
|
||||
message: "supported commands: agentrun v01 control-plane status|trigger-current",
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user