feat: add yaml-first AgentRun lane ops

This commit is contained in:
Codex
2026-06-13 04:21:57 +00:00
parent 58d4b6c76f
commit 687310d83c
4 changed files with 928 additions and 4 deletions
+130
View File
@@ -28,3 +28,133 @@ auth:
client:
role: render-only
transport: direct-http
controlPlane:
default:
node: G14
lane: v01
nodes:
G14:
route: G14
kubeRoute: G14:k3s
D601:
route: D601
kubeRoute: D601:k3s
lanes:
v01:
node: G14
version: v0.1
source:
repository: pikasTech/agentrun
branch: v0.1
remote: git@github.com:pikasTech/agentrun.git
workspace: /root/agentrun-v01
runtime:
namespace: agentrun-v01
managerDeployment: agentrun-mgr
managerService: agentrun-mgr
managerPort: 8080
internalBaseUrl: http://agentrun-mgr.agentrun-v01.svc.cluster.local:8080
ci:
namespace: agentrun-ci
pipeline: agentrun-v01-ci-image-publish
pipelineRunPrefix: agentrun-v01-ci
serviceAccountName: agentrun-v01-tekton-runner
registryPrefix: 127.0.0.1:5000/agentrun
toolsImage: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
gitops:
branch: v0.1-gitops
path: deploy/gitops/g14/runtime-v01
argoNamespace: argocd
argoApplication: agentrun-g14-v01
repoURL: http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/agentrun.git
gitMirror:
namespace: devops-infra
readService: git-mirror-http
writeService: git-mirror-write
readUrl: http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/agentrun.git
writeUrl: http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/agentrun.git
cachePvc: git-mirror-cache
toolsImage: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
syncJobPrefix: git-mirror-agentrun-sync-manual
flushJobPrefix: git-mirror-agentrun-flush-manual
repositories:
- key: agentrun
repository: pikasTech/agentrun
sourceBranch: v0.1
gitopsBranch: v0.1-gitops
- key: unidesk
repository: pikasTech/unidesk
sourceBranch: master
- key: agent_skills
repository: pikasTech/agent_skills
sourceBranch: master
database:
mode: local-postgres
secretRef:
name: agentrun-v01-mgr-db
key: DATABASE_URL
localPostgresExpectedAbsent: false
v02:
node: D601
version: v0.2
source:
repository: pikasTech/agentrun
branch: v0.2
remote: git@github.com:pikasTech/agentrun.git
workspace: /home/ubuntu/workspace/agentrun-v02
runtime:
namespace: agentrun-v02
managerDeployment: agentrun-mgr
managerService: agentrun-mgr
managerPort: 8080
internalBaseUrl: http://agentrun-mgr.agentrun-v02.svc.cluster.local:8080
ci:
namespace: agentrun-ci
pipeline: agentrun-v02-ci-image-publish
pipelineRunPrefix: agentrun-v02-ci
serviceAccountName: agentrun-v02-tekton-runner
registryPrefix: 127.0.0.1:5000/agentrun
toolsImage: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
gitops:
branch: v0.2-gitops
path: deploy/gitops/node/d601/runtime-v02
argoNamespace: argocd
argoApplication: agentrun-d601-v02
repoURL: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/agentrun.git
gitMirror:
namespace: devops-infra
readService: git-mirror-http
writeService: git-mirror-write
readUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/agentrun.git
writeUrl: http://git-mirror-write.devops-infra.svc.cluster.local:8080/pikasTech/agentrun.git
cachePvc: hwlab-git-mirror-cache
toolsImage: 127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1
syncJobPrefix: git-mirror-agentrun-d601-v02-sync-manual
flushJobPrefix: git-mirror-agentrun-d601-v02-flush-manual
repositories:
- key: agentrun
repository: pikasTech/agentrun
sourceBranch: v0.2
gitopsBranch: v0.2-gitops
- key: unidesk
repository: pikasTech/unidesk
sourceBranch: master
- key: agent_skills
repository: pikasTech/agent_skills
sourceBranch: master
database:
mode: external-postgres
provider: PK01
configRef: config/platform-db/postgres-pk01.yaml
database: agentrun_v02
user: agentrun_v02
sslmode: require
secretSourceRef: agentrun/d601-v02-mgr-db.env
secretRef:
name: agentrun-v02-mgr-db
key: DATABASE_URL
localPostgresExpectedAbsent: true
+76
View File
@@ -200,6 +200,36 @@ postgres:
user: n8n
address: 202.98.17.68/32
method: scram-sha-256
- type: hostssl
database: agentrun_v02
user: agentrun_v02
address: 10.0.8.0/22
method: scram-sha-256
- type: hostssl
database: postgres
user: agentrun_v02
address: 10.0.8.0/22
method: scram-sha-256
- type: hostssl
database: agentrun_v02
user: agentrun_v02
address: 36.49.29.73/32
method: scram-sha-256
- type: hostssl
database: postgres
user: agentrun_v02
address: 36.49.29.73/32
method: scram-sha-256
- type: hostssl
database: agentrun_v02
user: agentrun_v02
address: 74.48.78.17/32
method: scram-sha-256
- type: hostssl
database: postgres
user: agentrun_v02
address: 74.48.78.17/32
method: scram-sha-256
secrets:
source: master-local
@@ -247,6 +277,20 @@ secrets:
N8N_DB_NAME: n8n
randomHex:
N8N_DB_PASSWORD: 32
- name: agentrun-v02-db-credentials
sourceRef: platform-db/agentrun-v02-db.env
type: env
requiredKeys:
- AGENTRUN_V02_DB_USER
- AGENTRUN_V02_DB_PASSWORD
- AGENTRUN_V02_DB_NAME
createIfMissing:
enabled: true
values:
AGENTRUN_V02_DB_USER: agentrun_v02
AGENTRUN_V02_DB_NAME: agentrun_v02
randomHex:
AGENTRUN_V02_DB_PASSWORD: 32
objects:
roles:
@@ -277,6 +321,15 @@ objects:
createdb: false
createrole: false
superuser: false
- name: agentrun_v02
passwordRef:
sourceRef: platform-db/agentrun-v02-db.env
key: AGENTRUN_V02_DB_PASSWORD
login: true
attributes:
createdb: false
createrole: false
superuser: false
databases:
- name: sub2api
owner: sub2api
@@ -293,6 +346,11 @@ objects:
encoding: UTF8
locale: C.UTF-8
extensions: []
- name: agentrun_v02
owner: agentrun_v02
encoding: UTF8
locale: C.UTF-8
extensions: []
exports:
connectionStrings:
@@ -341,6 +399,21 @@ exports:
- scope: platform-infra
secret: n8n-secrets
key: DATABASE_URL
- name: agentrun-v02-database-url
sourceSecretRef: platform-db/agentrun-v02-db.env
render:
envKey: DATABASE_URL
format: postgresql://$(AGENTRUN_V02_DB_USER):$(AGENTRUN_V02_DB_PASSWORD)@$(PGHOST):5432/$(AGENTRUN_V02_DB_NAME)?sslmode=require
variables:
PGHOST: 82.156.23.220
writeToSecretSource:
sourceRef: agentrun/d601-v02-mgr-db.env
key: DATABASE_URL
mode: update-or-insert
consumers:
- scope: agentrun-v02
secret: agentrun-v02-mgr-db
key: DATABASE_URL
backup:
phase: minimum-restoreable
@@ -377,6 +450,9 @@ observability:
- kind: psql-app-role
database: n8n
user: n8n
- kind: psql-app-role
database: agentrun_v02
user: agentrun_v02
- kind: disk-free
path: /var/lib/postgresql/16/main
minFreeGiB: 10
+393
View File
@@ -0,0 +1,393 @@
import { readFileSync } from "node:fs";
import { rootPath } from "./config";
export const AGENTRUN_CONFIG_PATH = "config/agentrun.yaml";
export interface AgentRunGitMirrorRepositorySpec {
readonly key: string;
readonly repository: string;
readonly sourceBranch: string;
readonly gitopsBranch?: string;
}
export interface AgentRunLaneSpec {
readonly lane: string;
readonly nodeId: string;
readonly nodeRoute: string;
readonly nodeKubeRoute: string;
readonly version: string;
readonly source: {
readonly repository: string;
readonly branch: string;
readonly remote: string;
readonly workspace: string;
};
readonly runtime: {
readonly namespace: string;
readonly managerDeployment: string;
readonly managerService: string;
readonly managerPort: number;
readonly internalBaseUrl: string;
};
readonly ci: {
readonly namespace: string;
readonly pipeline: string;
readonly pipelineRunPrefix: string;
readonly serviceAccountName: string;
readonly registryPrefix: string;
readonly toolsImage: string;
};
readonly gitops: {
readonly branch: string;
readonly path: string;
readonly argoNamespace: string;
readonly argoApplication: string;
readonly repoURL: string;
};
readonly gitMirror: {
readonly namespace: string;
readonly readService: string;
readonly writeService: string;
readonly readUrl: string;
readonly writeUrl: string;
readonly cachePvc: string;
readonly toolsImage: string;
readonly syncJobPrefix: string;
readonly flushJobPrefix: string;
readonly repositories: readonly AgentRunGitMirrorRepositorySpec[];
};
readonly database: {
readonly mode: "local-postgres" | "external-postgres";
readonly provider: string | null;
readonly configRef: string | null;
readonly database: string | null;
readonly user: string | null;
readonly sslmode: "require" | null;
readonly secretSourceRef: string | null;
readonly secretRef: { readonly name: string; readonly key: string };
readonly localPostgresExpectedAbsent: boolean;
};
}
export interface AgentRunLaneTarget {
readonly configPath: string;
readonly spec: AgentRunLaneSpec;
}
interface AgentRunNodeSpec {
readonly id: string;
readonly route: string;
readonly kubeRoute: string;
}
interface AgentRunControlPlaneConfig {
readonly sourcePath: string;
readonly defaultTarget: {
readonly node: string;
readonly lane: string;
};
readonly nodes: Record<string, AgentRunNodeSpec>;
readonly lanes: Record<string, AgentRunLaneSpec>;
}
export function resolveAgentRunLaneTarget(options: { node?: string | null; lane?: string | null }, env: NodeJS.ProcessEnv = process.env): AgentRunLaneTarget {
const config = readAgentRunControlPlaneConfig(env);
const requestedNode = options.node ?? null;
const requestedLane = options.lane ?? null;
if (requestedLane !== null) {
const spec = config.lanes[requestedLane];
if (spec === undefined) throw new Error(`${config.sourcePath}: controlPlane.lanes.${requestedLane} is not declared`);
if (requestedNode !== null && requestedNode !== spec.nodeId) throw new Error(`--node ${requestedNode} does not match controlPlane.lanes.${requestedLane}.node=${spec.nodeId}`);
return { configPath: config.sourcePath, spec };
}
if (requestedNode !== null) {
const candidates = Object.values(config.lanes).filter((lane) => lane.nodeId === requestedNode);
if (candidates.length === 0) throw new Error(`${config.sourcePath}: no AgentRun lane is declared for node ${requestedNode}`);
if (candidates.length > 1) throw new Error(`--lane is required because node ${requestedNode} has multiple AgentRun lanes: ${candidates.map((lane) => lane.lane).join(", ")}`);
return { configPath: config.sourcePath, spec: candidates[0] };
}
const spec = config.lanes[config.defaultTarget.lane];
if (spec === undefined) throw new Error(`${config.sourcePath}: default controlPlane lane ${config.defaultTarget.lane} is not declared`);
if (spec.nodeId !== config.defaultTarget.node) throw new Error(`${config.sourcePath}: controlPlane.default.node does not match default lane node`);
return { configPath: config.sourcePath, spec };
}
export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record<string, unknown> {
return {
node: {
id: spec.nodeId,
route: spec.nodeRoute,
kubeRoute: spec.nodeKubeRoute,
},
lane: spec.lane,
version: spec.version,
source: {
repository: spec.source.repository,
branch: spec.source.branch,
workspace: spec.source.workspace,
},
runtime: {
namespace: spec.runtime.namespace,
managerDeployment: spec.runtime.managerDeployment,
managerService: spec.runtime.managerService,
managerPort: spec.runtime.managerPort,
internalBaseUrl: spec.runtime.internalBaseUrl,
},
ci: {
namespace: spec.ci.namespace,
pipeline: spec.ci.pipeline,
pipelineRunPrefix: spec.ci.pipelineRunPrefix,
serviceAccountName: spec.ci.serviceAccountName,
registryPrefix: spec.ci.registryPrefix,
},
gitops: {
branch: spec.gitops.branch,
path: spec.gitops.path,
argoNamespace: spec.gitops.argoNamespace,
argoApplication: spec.gitops.argoApplication,
repoURL: spec.gitops.repoURL,
},
gitMirror: {
namespace: spec.gitMirror.namespace,
readService: spec.gitMirror.readService,
writeService: spec.gitMirror.writeService,
readUrl: spec.gitMirror.readUrl,
writeUrl: spec.gitMirror.writeUrl,
cachePvc: spec.gitMirror.cachePvc,
repositories: spec.gitMirror.repositories.map((repo) => ({
key: repo.key,
repository: repo.repository,
sourceBranch: repo.sourceBranch,
gitopsBranch: repo.gitopsBranch ?? null,
})),
},
database: {
mode: spec.database.mode,
provider: spec.database.provider,
configRef: spec.database.configRef,
database: spec.database.database,
user: spec.database.user,
sslmode: spec.database.sslmode,
secretSourceRef: spec.database.secretSourceRef,
secretRef: spec.database.secretRef,
localPostgresExpectedAbsent: spec.database.localPostgresExpectedAbsent,
valuesPrinted: false,
},
};
}
export function agentRunPipelineRunName(spec: AgentRunLaneSpec, sourceCommit: string): string {
return `${spec.ci.pipelineRunPrefix}-${sourceCommit.slice(0, 12)}`;
}
function readAgentRunControlPlaneConfig(env: NodeJS.ProcessEnv): AgentRunControlPlaneConfig {
const configPath = env.AGENTRUN_CONTROL_PLANE_CONFIG ?? rootPath(AGENTRUN_CONFIG_PATH);
const root = asRecord(Bun.YAML.parse(readFileSync(configPath, "utf8")) as unknown, configPath);
const controlPlane = recordField(root, "controlPlane", configPath);
const defaultTarget = recordField(controlPlane, "default", `${configPath}.controlPlane`);
const nodes = parseNodes(recordField(controlPlane, "nodes", `${configPath}.controlPlane`), configPath);
const lanes = parseLanes(recordField(controlPlane, "lanes", `${configPath}.controlPlane`), nodes, configPath);
return {
sourcePath: configPath,
defaultTarget: {
node: stringField(defaultTarget, "node", `${configPath}.controlPlane.default`),
lane: stringField(defaultTarget, "lane", `${configPath}.controlPlane.default`),
},
nodes,
lanes,
};
}
function parseNodes(input: Record<string, unknown>, configPath: string): Record<string, AgentRunNodeSpec> {
const result: Record<string, AgentRunNodeSpec> = {};
for (const [id, raw] of Object.entries(input)) {
validateSimpleId(id, `${configPath}.controlPlane.nodes`);
const node = asRecord(raw, `${configPath}.controlPlane.nodes.${id}`);
result[id] = {
id,
route: stringField(node, "route", `${configPath}.controlPlane.nodes.${id}`),
kubeRoute: stringField(node, "kubeRoute", `${configPath}.controlPlane.nodes.${id}`),
};
}
if (Object.keys(result).length === 0) throw new Error(`${configPath}.controlPlane.nodes must declare at least one node`);
return result;
}
function parseLanes(input: Record<string, unknown>, nodes: Record<string, AgentRunNodeSpec>, configPath: string): Record<string, AgentRunLaneSpec> {
const result: Record<string, AgentRunLaneSpec> = {};
for (const [lane, raw] of Object.entries(input)) {
validateSimpleId(lane, `${configPath}.controlPlane.lanes`);
const item = asRecord(raw, `${configPath}.controlPlane.lanes.${lane}`);
const nodeId = stringField(item, "node", `${configPath}.controlPlane.lanes.${lane}`);
const node = nodes[nodeId];
if (node === undefined) throw new Error(`${configPath}.controlPlane.lanes.${lane}.node references undeclared node ${nodeId}`);
result[lane] = parseLane(lane, node, item, configPath);
}
if (Object.keys(result).length === 0) throw new Error(`${configPath}.controlPlane.lanes must declare at least one lane`);
return result;
}
function parseLane(lane: string, node: AgentRunNodeSpec, input: Record<string, unknown>, configPath: string): AgentRunLaneSpec {
const path = `${configPath}.controlPlane.lanes.${lane}`;
const source = recordField(input, "source", path);
const runtime = recordField(input, "runtime", path);
const ci = recordField(input, "ci", path);
const gitops = recordField(input, "gitops", path);
const gitMirror = recordField(input, "gitMirror", path);
const database = recordField(input, "database", path);
return {
lane,
nodeId: node.id,
nodeRoute: node.route,
nodeKubeRoute: node.kubeRoute,
version: stringField(input, "version", path),
source: {
repository: stringField(source, "repository", `${path}.source`),
branch: stringField(source, "branch", `${path}.source`),
remote: stringField(source, "remote", `${path}.source`),
workspace: absolutePathField(source, "workspace", `${path}.source`),
},
runtime: {
namespace: stringField(runtime, "namespace", `${path}.runtime`),
managerDeployment: stringField(runtime, "managerDeployment", `${path}.runtime`),
managerService: stringField(runtime, "managerService", `${path}.runtime`),
managerPort: integerField(runtime, "managerPort", `${path}.runtime`),
internalBaseUrl: urlField(runtime, "internalBaseUrl", `${path}.runtime`),
},
ci: {
namespace: stringField(ci, "namespace", `${path}.ci`),
pipeline: stringField(ci, "pipeline", `${path}.ci`),
pipelineRunPrefix: stringField(ci, "pipelineRunPrefix", `${path}.ci`),
serviceAccountName: stringField(ci, "serviceAccountName", `${path}.ci`),
registryPrefix: stringField(ci, "registryPrefix", `${path}.ci`),
toolsImage: stringField(ci, "toolsImage", `${path}.ci`),
},
gitops: {
branch: stringField(gitops, "branch", `${path}.gitops`),
path: relativePathField(gitops, "path", `${path}.gitops`),
argoNamespace: stringField(gitops, "argoNamespace", `${path}.gitops`),
argoApplication: stringField(gitops, "argoApplication", `${path}.gitops`),
repoURL: urlField(gitops, "repoURL", `${path}.gitops`),
},
gitMirror: {
namespace: stringField(gitMirror, "namespace", `${path}.gitMirror`),
readService: stringField(gitMirror, "readService", `${path}.gitMirror`),
writeService: stringField(gitMirror, "writeService", `${path}.gitMirror`),
readUrl: urlField(gitMirror, "readUrl", `${path}.gitMirror`),
writeUrl: urlField(gitMirror, "writeUrl", `${path}.gitMirror`),
cachePvc: stringField(gitMirror, "cachePvc", `${path}.gitMirror`),
toolsImage: stringField(gitMirror, "toolsImage", `${path}.gitMirror`),
syncJobPrefix: stringField(gitMirror, "syncJobPrefix", `${path}.gitMirror`),
flushJobPrefix: stringField(gitMirror, "flushJobPrefix", `${path}.gitMirror`),
repositories: arrayField(gitMirror, "repositories", `${path}.gitMirror`).map((repo, index) => parseGitMirrorRepository(repo, `${path}.gitMirror.repositories[${index}]`)),
},
database: parseDatabase(database, `${path}.database`),
};
}
function parseGitMirrorRepository(input: Record<string, unknown>, path: string): AgentRunGitMirrorRepositorySpec {
const gitopsBranch = optionalStringField(input, "gitopsBranch", path);
return {
key: stringField(input, "key", path),
repository: stringField(input, "repository", path),
sourceBranch: stringField(input, "sourceBranch", path),
...(gitopsBranch === undefined ? {} : { gitopsBranch }),
};
}
function parseDatabase(input: Record<string, unknown>, path: string): AgentRunLaneSpec["database"] {
const mode = enumField(input, "mode", path, ["local-postgres", "external-postgres"]);
const sslmode = optionalStringField(input, "sslmode", path);
if (sslmode !== undefined && sslmode !== "require") throw new Error(`${path}.sslmode must be require when set`);
return {
mode,
provider: optionalStringField(input, "provider", path) ?? null,
configRef: optionalStringField(input, "configRef", path) ?? null,
database: optionalStringField(input, "database", path) ?? null,
user: optionalStringField(input, "user", path) ?? null,
sslmode: sslmode === undefined ? null : "require",
secretSourceRef: optionalStringField(input, "secretSourceRef", path) ?? null,
secretRef: parseSecretRef(recordField(input, "secretRef", path), `${path}.secretRef`),
localPostgresExpectedAbsent: booleanField(input, "localPostgresExpectedAbsent", path),
};
}
function parseSecretRef(input: Record<string, unknown>, path: string): { name: string; key: string } {
return {
name: stringField(input, "name", path),
key: stringField(input, "key", path),
};
}
function asRecord(value: unknown, path: string): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${path} must be a YAML object`);
return value as Record<string, unknown>;
}
function recordField(obj: Record<string, unknown>, key: string, path: string): Record<string, unknown> {
return asRecord(obj[key], `${path}.${key}`);
}
function stringField(obj: Record<string, unknown>, key: string, path: string): string {
const value = obj[key];
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path}.${key} must be a non-empty string`);
return value.trim();
}
function optionalStringField(obj: Record<string, unknown>, key: string, path: string): string | undefined {
const value = obj[key];
if (value === undefined || value === null) return undefined;
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${path}.${key} must be a non-empty string when set`);
return value.trim();
}
function booleanField(obj: Record<string, unknown>, key: string, path: string): boolean {
const value = obj[key];
if (typeof value !== "boolean") throw new Error(`${path}.${key} must be a boolean`);
return value;
}
function integerField(obj: Record<string, unknown>, key: string, path: string): number {
const value = obj[key];
if (!Number.isInteger(value)) throw new Error(`${path}.${key} must be an integer`);
return Number(value);
}
function arrayField(obj: Record<string, unknown>, key: string, path: string): Record<string, unknown>[] {
const value = obj[key];
if (!Array.isArray(value)) throw new Error(`${path}.${key} must be a YAML array`);
return value.map((item, index) => asRecord(item, `${path}.${key}[${index}]`));
}
function enumField<T extends string>(obj: Record<string, unknown>, key: string, path: string, values: readonly T[]): T {
const value = stringField(obj, key, path);
if (!values.includes(value as T)) throw new Error(`${path}.${key} must be one of ${values.join(", ")}`);
return value as T;
}
function absolutePathField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (!value.startsWith("/") || value.includes("..")) throw new Error(`${path}.${key} must be an absolute path without ..`);
return value;
}
function relativePathField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
if (value.startsWith("/") || value.includes("..")) throw new Error(`${path}.${key} must be a relative path without ..`);
return value;
}
function urlField(obj: Record<string, unknown>, key: string, path: string): string {
const value = stringField(obj, key, path);
try {
const parsed = new URL(value);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("unsupported protocol");
} catch {
throw new Error(`${path}.${key} must be an http(s) URL`);
}
return value;
}
function validateSimpleId(value: string, path: string): void {
if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${path}.${value} must use a simple id`);
}
+329 -4
View File
@@ -5,6 +5,13 @@ import type { RenderedCliResult } from "./output";
import { runSshCommandCapture, type SshCaptureResult } from "./ssh";
import { runRemoteSshCommandCapture } from "./remote";
import { startJob } from "./jobs";
import {
AGENTRUN_CONFIG_PATH,
agentRunLaneSummary,
agentRunPipelineRunName,
resolveAgentRunLaneTarget,
type AgentRunLaneSpec,
} from "./agentrun-lanes";
const g14SourceRoute = "G14:/root/agentrun-v01";
const g14K3sRoute = "G14:k3s";
@@ -56,6 +63,8 @@ export function agentRunHelp(): unknown {
"bun scripts/cli.ts agentrun apply -f - --dry-run",
"bun scripts/cli.ts agentrun send session/<sessionId> --aipod Artificer --prompt-stdin",
"bun scripts/cli.ts agentrun explain task",
"bun scripts/cli.ts agentrun control-plane plan --node D601 --lane v02",
"bun scripts/cli.ts agentrun control-plane status --node D601 --lane v02",
"bun scripts/cli.ts agentrun control-plane status",
"bun scripts/cli.ts agentrun control-plane status --full",
"bun scripts/cli.ts agentrun control-plane status --pipeline-run agentrun-v01-ci-<short-sha>",
@@ -92,6 +101,7 @@ export async function runAgentRunCommand(config: UniDeskConfig | null, args: str
}
if (config === null) throw new Error("agentrun control-plane and git-mirror commands require UniDesk config");
if (group === "control-plane") {
if (action === "plan") return await controlPlanePlan(config, parseStatusOptions(actionArgs));
if (action === "status") return await status(config, parseStatusOptions(actionArgs));
if (action === "expose") return await exposeAgentRun(config, parseConfirmOptions(actionArgs));
if (action === "trigger-current") return await triggerCurrent(config, parseTriggerOptions(actionArgs));
@@ -217,8 +227,10 @@ function agentRunHelpText(args: string[]): string {
return [
"Usage: bun scripts/cli.ts agentrun control-plane <action> [options]",
"",
"Actions: status, expose, trigger-current, refresh, cleanup-runs, cleanup-released-pvs",
"Actions: plan, status, expose, trigger-current, refresh, cleanup-runs, cleanup-released-pvs",
"Examples:",
" bun scripts/cli.ts agentrun control-plane plan --node D601 --lane v02",
" bun scripts/cli.ts agentrun control-plane status --node D601 --lane v02",
" bun scripts/cli.ts agentrun control-plane status",
" bun scripts/cli.ts agentrun control-plane status --pipeline-run agentrun-v01-ci-<short-sha>",
" bun scripts/cli.ts agentrun control-plane expose --dry-run",
@@ -1645,6 +1657,8 @@ interface DisclosureOptions {
}
interface StatusOptions extends DisclosureOptions {
node: string | null;
lane: string | null;
sourceCommit: string | null;
pipelineRun: string | null;
targetMode: "latest-source-head" | "source-commit" | "pipeline-run";
@@ -1656,7 +1670,14 @@ interface TimedValue<T> {
}
function parseDisclosureOptions(args: string[]): DisclosureOptions {
for (const arg of args) {
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--node" || arg === "--lane") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
index += 1;
continue;
}
if (arg !== "--full" && arg !== "--raw") throw new Error(`unsupported status option: ${arg}`);
}
const raw = args.includes("--raw");
@@ -1664,6 +1685,8 @@ function parseDisclosureOptions(args: string[]): DisclosureOptions {
}
function parseStatusOptions(args: string[]): StatusOptions {
let node: string | null = null;
let lane: string | null = null;
let sourceCommit: string | null = null;
let pipelineRun: string | null = null;
let full = false;
@@ -1679,6 +1702,20 @@ function parseStatusOptions(args: string[]): StatusOptions {
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;
}
if (arg === "--source-commit") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--source-commit requires a value");
@@ -1690,7 +1727,7 @@ function parseStatusOptions(args: string[]): StatusOptions {
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");
if (!isAgentRunPipelineRunName(value)) throw new Error("--pipeline-run must be an agentrun-vNN-ci-<12+ hex> PipelineRun name");
pipelineRun = value;
index += 1;
continue;
@@ -1701,6 +1738,8 @@ function parseStatusOptions(args: string[]): StatusOptions {
return {
full,
raw,
node,
lane,
sourceCommit,
pipelineRun,
targetMode: pipelineRun !== null ? "pipeline-run" : sourceCommit !== null ? "source-commit" : "latest-source-head",
@@ -1779,7 +1818,42 @@ function positiveIntegerOption(args: string[], name: string, defaultValue: numbe
return Math.min(value, maxValue);
}
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,
},
valuesPrinted: false,
};
}
async function status(config: UniDeskConfig, options: StatusOptions): Promise<Record<string, unknown>> {
if (options.node !== null || options.lane !== null) {
return await statusYamlLane(config, options, resolveAgentRunLaneTarget(options));
}
const sourceProbe = await timedStatusStage("source", () => capture(config, g14SourceRoute, ["script", "--", [
"cd /root/agentrun-v01",
"git fetch origin v0.1 >/dev/null 2>&1 || true",
@@ -1930,6 +2004,103 @@ async function status(config: UniDeskConfig, options: StatusOptions): Promise<Re
};
}
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}`, ["script", "--", yamlLaneSourceStatusScript(spec)]));
const sourcePayload = captureJsonPayload(sourceProbe.value);
const sourceCommit = options.sourceCommit
?? (options.pipelineRun !== null ? null : stringOrNull(sourcePayload.remoteBranchCommit) ?? stringOrNull(sourcePayload.localHead));
const pipelineRun = options.pipelineRun ?? (sourceCommit ? agentRunPipelineRunName(spec, sourceCommit) : null);
const [runtimeProbe, mirrorProbe] = await Promise.all([
timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["script", "--", yamlLaneRuntimeStatusScript(spec, pipelineRun)])),
timedStatusStage("git-mirror", () => capture(config, spec.nodeKubeRoute, ["script", "--", yamlLaneGitMirrorStatusScript(spec)])),
]);
const runtimePayload = captureJsonPayload(runtimeProbe.value);
const mirrorPayload = captureJsonPayload(mirrorProbe.value);
const pipeline = record(runtimePayload.pipeline);
const argo = record(runtimePayload.argo);
const manager = record(runtimePayload.manager);
const database = record(runtimePayload.database);
const localPostgres = record(runtimePayload.localPostgres);
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"]),
...(runtimePayload.serviceAccountExists === true ? [] : ["ci-serviceaccount-missing"]),
...(argo.exists === true ? [] : ["argo-application-missing"]),
...(runtimePayload.runtimeNamespaceExists === true ? [] : ["runtime-namespace-missing"]),
...(manager.deploymentExists === true ? [] : ["manager-deployment-missing"]),
...(manager.serviceExists === true ? [] : ["manager-service-missing"]),
...(spec.database.mode === "external-postgres" && database.secretPresent !== true ? ["database-secret-missing"] : []),
...(spec.database.localPostgresExpectedAbsent && localPostgres.absent !== true ? ["local-postgres-present"] : []),
];
return {
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: agentRunLaneSummary(spec),
summary: {
aligned: blockers.length === 0,
blockers,
sourceCommit,
expectedPipelineRun: pipelineRun,
source: {
workspaceExists: sourcePayload.workspaceExists ?? false,
workspaceClean: sourcePayload.workspaceClean ?? null,
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,
},
ci: {
namespaceExists: runtimePayload.ciNamespaceExists ?? false,
serviceAccountExists: runtimePayload.serviceAccountExists ?? false,
pipeline,
pipelineRun: record(runtimePayload.pipelineRun),
},
argo,
runtime: {
namespaceExists: runtimePayload.runtimeNamespaceExists ?? false,
manager,
database,
localPostgres,
},
},
timings: {
sourceMs: sourceProbe.elapsedMs,
runtimeMs: runtimeProbe.elapsedMs,
gitMirrorMs: mirrorProbe.elapsedMs,
totalMs: sourceProbe.elapsedMs + Math.max(runtimeProbe.elapsedMs, mirrorProbe.elapsedMs),
},
source: sourcePayload,
runtime: runtimePayload,
gitMirror: mirrorPayload,
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 }),
},
next: {
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,
statusFull: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane} --full`,
},
valuesPrinted: false,
};
}
async function exposeAgentRun(_config: UniDeskConfig, options: ConfirmOptions): Promise<Record<string, unknown>> {
const clientConfig = readAgentRunClientConfig();
const exposure = clientConfig.publicExposure;
@@ -2406,6 +2577,160 @@ function cleanupReleasedPvsScript(options: CleanupReleasedPvOptions): string {
].join("\n");
}
function yamlLaneSourceStatusScript(spec: AgentRunLaneSpec): string {
return [
"set +e",
`expected_workspace=${shQuote(spec.source.workspace)}`,
`source_branch=${shQuote(spec.source.branch)}`,
"workspace_exists=false",
"workspace_clean=null",
"local_head=null",
"branch=null",
"remote_url=null",
"remote_branch_exists=false",
"remote_branch_commit=null",
"status_short=''",
"if [ -d .git ] || git rev-parse --show-toplevel >/dev/null 2>&1; then",
" actual_workspace=$(pwd)",
" workspace_exists=true",
" branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)",
" remote_url=$(git remote get-url origin 2>/dev/null || true)",
" local_head=$(git rev-parse HEAD 2>/dev/null || true)",
" status_short=$(git status --short 2>/dev/null || true)",
" if [ -z \"$status_short\" ]; then workspace_clean=true; else workspace_clean=false; fi",
" git fetch origin \"$source_branch\" >/dev/null 2>&1 || true",
" remote_branch_commit=$(git rev-parse \"refs/remotes/origin/$source_branch\" 2>/dev/null || true)",
" if [ -n \"$remote_branch_commit\" ]; then remote_branch_exists=true; fi",
"else",
" actual_workspace=$(pwd)",
"fi",
"export expected_workspace source_branch workspace_exists workspace_clean local_head branch remote_url remote_branch_exists remote_branch_commit status_short actual_workspace",
"node <<'NODE'",
"function nullable(value) { return value && value !== 'null' ? value : null; }",
"function booleanValue(value) { if (value === 'true') return true; if (value === 'false') return false; return null; }",
"const env = process.env;",
"console.log(JSON.stringify({",
" ok: env.workspace_exists === 'true',",
" expectedWorkspace: env.expected_workspace,",
" actualWorkspace: env.actual_workspace || null,",
" workspaceExists: env.workspace_exists === 'true',",
" workspaceClean: booleanValue(env.workspace_clean),",
" branch: nullable(env.branch),",
" remoteUrl: nullable(env.remote_url),",
" localHead: nullable(env.local_head),",
" remoteBranch: env.source_branch,",
" remoteBranchExists: env.remote_branch_exists === 'true',",
" remoteBranchCommit: nullable(env.remote_branch_commit),",
" statusShort: nullable(env.status_short),",
" valuesPrinted: false",
"}));",
"NODE",
].join("\n");
}
function yamlLaneRuntimeStatusScript(spec: AgentRunLaneSpec, pipelineRun: string | null): string {
return [
"set +e",
`runtime_namespace=${shQuote(spec.runtime.namespace)}`,
`ci_namespace=${shQuote(spec.ci.namespace)}`,
`pipeline_name=${shQuote(spec.ci.pipeline)}`,
`pipeline_run=${pipelineRun === null ? "''" : shQuote(pipelineRun)}`,
`service_account=${shQuote(spec.ci.serviceAccountName)}`,
`argo_namespace=${shQuote(spec.gitops.argoNamespace)}`,
`argo_application=${shQuote(spec.gitops.argoApplication)}`,
`manager_deployment=${shQuote(spec.runtime.managerDeployment)}`,
`manager_service=${shQuote(spec.runtime.managerService)}`,
`database_secret=${shQuote(spec.database.secretRef.name)}`,
`database_key=${shQuote(spec.database.secretRef.key)}`,
"export runtime_namespace ci_namespace pipeline_name pipeline_run service_account argo_namespace argo_application manager_deployment manager_service database_secret database_key",
"tmp_dir=$(mktemp -d)",
"trap 'rm -rf \"$tmp_dir\"' EXIT",
"kubectl get ns \"$runtime_namespace\" -o json > \"$tmp_dir/runtime-ns.json\" 2>/dev/null",
"runtime_ns_exit=$?",
"kubectl get ns \"$ci_namespace\" -o json > \"$tmp_dir/ci-ns.json\" 2>/dev/null",
"ci_ns_exit=$?",
"kubectl -n \"$ci_namespace\" get pipeline \"$pipeline_name\" -o json > \"$tmp_dir/pipeline.json\" 2>/dev/null",
"pipeline_exit=$?",
"kubectl -n \"$ci_namespace\" get serviceaccount \"$service_account\" -o json > \"$tmp_dir/serviceaccount.json\" 2>/dev/null",
"sa_exit=$?",
"if [ -n \"$pipeline_run\" ]; then kubectl -n \"$ci_namespace\" get pipelinerun \"$pipeline_run\" -o json > \"$tmp_dir/pipelinerun.json\" 2>/dev/null; pr_exit=$?; else pr_exit=2; fi",
"kubectl -n \"$argo_namespace\" get application \"$argo_application\" -o json > \"$tmp_dir/argo.json\" 2>/dev/null",
"argo_exit=$?",
"kubectl -n \"$runtime_namespace\" get deploy \"$manager_deployment\" -o json > \"$tmp_dir/manager-deploy.json\" 2>/dev/null",
"manager_deploy_exit=$?",
"kubectl -n \"$runtime_namespace\" get svc \"$manager_service\" -o json > \"$tmp_dir/manager-svc.json\" 2>/dev/null",
"manager_svc_exit=$?",
"kubectl -n \"$runtime_namespace\" get secret \"$database_secret\" -o json > \"$tmp_dir/db-secret.json\" 2>/dev/null",
"db_secret_exit=$?",
"kubectl -n \"$runtime_namespace\" get deploy,sts,svc,secret -o name > \"$tmp_dir/runtime-names.txt\" 2>/dev/null",
"NODE_TMP=\"$tmp_dir\" RUNTIME_NS_EXIT=\"$runtime_ns_exit\" CI_NS_EXIT=\"$ci_ns_exit\" PIPELINE_EXIT=\"$pipeline_exit\" SERVICE_ACCOUNT_EXIT=\"$sa_exit\" PIPELINERUN_EXIT=\"$pr_exit\" ARGO_EXIT=\"$argo_exit\" MANAGER_DEPLOY_EXIT=\"$manager_deploy_exit\" MANAGER_SVC_EXIT=\"$manager_svc_exit\" DB_SECRET_EXIT=\"$db_secret_exit\" node <<'NODE'",
"const fs = require('node:fs');",
"const path = require('node:path');",
"const dir = process.env.NODE_TMP;",
"function readJson(name) { try { return JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8')); } catch { return null; } }",
"function exists(exitName) { return process.env[exitName] === '0'; }",
"function condition(obj) { return obj?.status?.conditions?.[0] || null; }",
"function envValue(deploy, name) { return deploy?.spec?.template?.spec?.containers?.[0]?.env?.find((entry) => entry.name === name)?.value || null; }",
"const pipelineRun = readJson('pipelinerun.json');",
"const argo = readJson('argo.json');",
"const managerDeploy = readJson('manager-deploy.json');",
"const managerSvc = readJson('manager-svc.json');",
"const dbSecret = readJson('db-secret.json');",
"let names = ''; try { names = fs.readFileSync(path.join(dir, 'runtime-names.txt'), 'utf8'); } catch {}",
"const c = condition(pipelineRun);",
"console.log(JSON.stringify({",
" ok: exists('RUNTIME_NS_EXIT') && exists('CI_NS_EXIT'),",
" runtimeNamespaceExists: exists('RUNTIME_NS_EXIT'),",
" ciNamespaceExists: exists('CI_NS_EXIT'),",
" serviceAccountExists: exists('SERVICE_ACCOUNT_EXIT'),",
" pipeline: { exists: exists('PIPELINE_EXIT'), name: process.env.pipeline_name },",
" pipelineRun: { exists: exists('PIPELINERUN_EXIT'), name: process.env.pipeline_run || null, status: c?.status || null, reason: c?.reason || null, startTime: pipelineRun?.status?.startTime || null, completionTime: pipelineRun?.status?.completionTime || null },",
" argo: { exists: exists('ARGO_EXIT'), namespace: process.env.argo_namespace, application: process.env.argo_application, revision: argo?.status?.sync?.revision || null, syncStatus: argo?.status?.sync?.status || null, healthStatus: argo?.status?.health?.status || null },",
" manager: { deploymentExists: exists('MANAGER_DEPLOY_EXIT'), serviceExists: exists('MANAGER_SVC_EXIT'), deployment: process.env.manager_deployment, service: process.env.manager_service, image: managerDeploy?.spec?.template?.spec?.containers?.[0]?.image || null, sourceCommit: envValue(managerDeploy, 'AGENTRUN_SOURCE_COMMIT'), servicePorts: Array.isArray(managerSvc?.spec?.ports) ? managerSvc.spec.ports.map((port) => ({ name: port.name || null, port: port.port || null, targetPort: port.targetPort || null })) : [] },",
" database: { secretPresent: exists('DB_SECRET_EXIT'), secretName: process.env.database_secret, key: process.env.database_key, keyPresent: Boolean(dbSecret?.data && Object.prototype.hasOwnProperty.call(dbSecret.data, process.env.database_key || '')) , valuesPrinted: false },",
" localPostgres: { absent: !/postgres/i.test(names), matchingObjects: names.split(/\\r?\\n/).filter((line) => /postgres/i.test(line)).slice(0, 20) },",
" valuesPrinted: false",
"}));",
"NODE",
].join("\n");
}
function yamlLaneGitMirrorStatusScript(spec: AgentRunLaneSpec): string {
return [
"set +e",
`namespace=${shQuote(spec.gitMirror.namespace)}`,
`read_service=${shQuote(spec.gitMirror.readService)}`,
`write_service=${shQuote(spec.gitMirror.writeService)}`,
`cache_pvc=${shQuote(spec.gitMirror.cachePvc)}`,
`repositories_json=${shQuote(JSON.stringify(spec.gitMirror.repositories))}`,
"kubectl -n \"$namespace\" get svc \"$read_service\" -o json >/tmp/agentrun-gitmirror-read.json 2>/dev/null",
"read_exit=$?",
"kubectl -n \"$namespace\" get svc \"$write_service\" -o json >/tmp/agentrun-gitmirror-write.json 2>/dev/null",
"write_exit=$?",
"kubectl -n \"$namespace\" get pvc \"$cache_pvc\" -o json >/tmp/agentrun-gitmirror-cache.json 2>/dev/null",
"cache_exit=$?",
"kubectl -n \"$namespace\" get deploy,svc,pvc -o name > /tmp/agentrun-gitmirror-names.txt 2>/dev/null",
"NAMESPACE=\"$namespace\" READ_EXIT=\"$read_exit\" WRITE_EXIT=\"$write_exit\" CACHE_EXIT=\"$cache_exit\" REPOSITORIES_JSON=\"$repositories_json\" node <<'NODE'",
"const fs = require('node:fs');",
"const repositories = JSON.parse(process.env.REPOSITORIES_JSON || '[]');",
"let names = ''; try { names = fs.readFileSync('/tmp/agentrun-gitmirror-names.txt', 'utf8'); } catch {}",
"const readReady = process.env.READ_EXIT === '0';",
"const writeReady = process.env.WRITE_EXIT === '0';",
"const cachePvcExists = process.env.CACHE_EXIT === '0';",
"console.log(JSON.stringify({",
" ok: readReady && writeReady && cachePvcExists,",
" namespace: process.env.NAMESPACE,",
" readReady,",
" writeReady,",
" cachePvcExists,",
" resources: names.split(/\\r?\\n/).filter(Boolean).slice(0, 40),",
" repositories,",
" valuesPrinted: false",
"}));",
"NODE",
].join("\n");
}
function cleanupRunsPlanNodeScript(): string {
return String.raw`
const fs = require("node:fs");
@@ -4429,7 +4754,7 @@ function pipelineRunName(sourceCommit: string): string {
}
function isAgentRunPipelineRunName(value: string): boolean {
return /^agentrun-v01-ci-[0-9a-f]{12,40}(?:-[a-z0-9-]+)?$/u.test(value);
return /^agentrun-v[0-9]+-ci-[0-9a-f]{12,40}(?:-[a-z0-9-]+)?$/u.test(value);
}
function statusTargetArg(options: StatusOptions, target: Record<string, unknown>): string {