From 49f13e404e131fc122592144ab1fcc09eab74240 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 12 Jul 2026 00:12:55 +0200 Subject: [PATCH] =?UTF-8?q?feat(cicd):=20=E6=B3=A8=E5=86=8C=20AgentRun=20m?= =?UTF-8?q?anaged=20repository=20fragment?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/src/agentrun-lanes.ts | 13 ++++-- ...trun-managed-repository-reconciler.test.ts | 43 +++++++++++++++++++ .../agentrun-managed-repository-reconciler.ts | 4 +- .../platform-infra-gitea-desired-fragments.ts | 2 + ...atform-infra-gitea-gitops-delivery.test.ts | 3 ++ .../src/platform-infra-pac-provenance.test.ts | 10 ++++- 6 files changed, 69 insertions(+), 6 deletions(-) diff --git a/scripts/src/agentrun-lanes.ts b/scripts/src/agentrun-lanes.ts index fd839deb..af88d634 100644 --- a/scripts/src/agentrun-lanes.ts +++ b/scripts/src/agentrun-lanes.ts @@ -363,13 +363,20 @@ export function resolveAgentRunLaneTarget(options: { node?: string | null; lane? } export function resolveAgentRunLaneTargetsForNode(nodeId: string, env: NodeJS.ProcessEnv = process.env): readonly AgentRunLaneTarget[] { + const targets = listAgentRunLaneTargetsForNode(nodeId, env); + if (targets.length === 0) { + const configPath = env.AGENTRUN_CONTROL_PLANE_CONFIG ?? rootPath(AGENTRUN_CONFIG_PATH); + throw new Error(`${configPath}: no AgentRun lane is declared for node ${nodeId}`); + } + return targets; +} + +export function listAgentRunLaneTargetsForNode(nodeId: string, env: NodeJS.ProcessEnv = process.env): readonly AgentRunLaneTarget[] { const config = readAgentRunControlPlaneConfig(env); - const targets = Object.values(config.lanes) + return Object.values(config.lanes) .filter((lane) => lane.nodeId === nodeId) .sort((left, right) => left.lane.localeCompare(right.lane)) .map((spec) => ({ configPath: config.sourcePath, spec })); - if (targets.length === 0) throw new Error(`${config.sourcePath}: no AgentRun lane is declared for node ${nodeId}`); - return targets; } export function agentRunLaneSummary(spec: AgentRunLaneSpec): Record { diff --git a/scripts/src/agentrun-managed-repository-reconciler.test.ts b/scripts/src/agentrun-managed-repository-reconciler.test.ts index b208722f..0320fb43 100644 --- a/scripts/src/agentrun-managed-repository-reconciler.test.ts +++ b/scripts/src/agentrun-managed-repository-reconciler.test.ts @@ -15,6 +15,7 @@ import { } from "./agentrun-managed-repository-reconciler"; import { resolveAgentRunLaneTarget } from "./agentrun-lanes"; import { yamlLaneGitMirrorStatusScript } from "./agentrun/secrets"; +import { renderPlatformInfraGiteaDesiredFragments } from "./platform-infra-gitea-desired-fragments"; const controllerPath = new URL("../native/agentrun/managed-repository-reconciler.mjs", import.meta.url).pathname; @@ -43,6 +44,7 @@ describe("AgentRun YAML-owned managed repository reconciler", () => { expect(documents[2].spec.strategy.type).toBe("Recreate"); expect(documents[2].spec.template.spec.automountServiceAccountToken).toBe(false); expect(documents[2].spec.template.spec.containers[0].readinessProbe.exec.command).toContain("--health"); + expect(renderAgentRunManagedRepositoryReconcilerFragment("JD01")).toBe(""); const controllerSource = readFileSync(controllerPath, "utf8"); expect(controllerSource).not.toContain("agent_skills"); @@ -50,6 +52,47 @@ describe("AgentRun YAML-owned managed repository reconciler", () => { expect(controllerSource).not.toContain("github.com"); }); + test("registers after PaC-owned admission objects without identity or separator conflicts", () => { + const spec = resolveAgentRunLaneTarget({ node: "NC01", lane: "nc01-v02" }).spec; + const desired = agentRunManagedRepositoryDesiredConfig(spec); + const fragment = renderPlatformInfraGiteaDesiredFragments("NC01"); + expect(fragment).not.toContain("---\n---"); + const documents = fragment + .split(/^---\s*$/mu) + .map((item) => Bun.YAML.parse(item) as any); + expect(documents.map((item) => item.kind)).toEqual([ + "Role", + "RoleBinding", + "ValidatingAdmissionPolicy", + "ValidatingAdmissionPolicyBinding", + "ServiceAccount", + "ConfigMap", + "Deployment", + ]); + const identities = documents.map((item) => [ + item.apiVersion, + item.kind, + item.metadata.namespace ?? "_cluster", + item.metadata.name, + ].join("/")); + expect(new Set(identities).size).toBe(documents.length); + + const managed = documents.slice(-3); + expect(managed.map((item) => item.metadata.name)).toEqual([ + spec.gitMirror.repositoryReconciler.serviceAccountName, + spec.gitMirror.repositoryReconciler.configMapName, + spec.gitMirror.repositoryReconciler.deploymentName, + ]); + const config = JSON.parse(managed[1].data["config.json"]); + expect(config.desiredHash).toBe(desired.desiredHash); + expect(managed[1].metadata.annotations["unidesk.ai/managed-repository-desired-sha"]).toBe(desired.desiredHash); + expect(managed[2].metadata.annotations["unidesk.ai/managed-repository-desired-sha"]).toBe(desired.desiredHash); + expect(managed[1].metadata.annotations["unidesk.ai/managed-repository-render-sha"]).toMatch(/^[0-9a-f]{64}$/u); + expect(managed[2].metadata.annotations["unidesk.ai/managed-repository-render-sha"]).toBe( + managed[1].metadata.annotations["unidesk.ai/managed-repository-render-sha"], + ); + }); + test("requires every repository entry to own a matching remote", () => { const temporary = mkdtempSync("/tmp/unidesk-agentrun-repository-config-"); try { diff --git a/scripts/src/agentrun-managed-repository-reconciler.ts b/scripts/src/agentrun-managed-repository-reconciler.ts index 92e73980..b1dc1c39 100644 --- a/scripts/src/agentrun-managed-repository-reconciler.ts +++ b/scripts/src/agentrun-managed-repository-reconciler.ts @@ -2,7 +2,7 @@ import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; import { rootPath } from "./config"; import { - resolveAgentRunLaneTargetsForNode, + listAgentRunLaneTargetsForNode, type AgentRunLaneSpec, } from "./agentrun-lanes"; @@ -96,7 +96,7 @@ export function agentRunManagedRepositoryRenderHash(spec: AgentRunLaneSpec): str } export function renderAgentRunManagedRepositoryReconcilerFragment(nodeId: string, env: NodeJS.ProcessEnv = process.env): string { - const targets = resolveAgentRunLaneTargetsForNode(nodeId, env) + const targets = listAgentRunLaneTargetsForNode(nodeId, env) .filter(({ spec }) => spec.gitMirror.repositoryReconciler.enabled); assertUniqueManagedRepositoryOwnership(targets.map(({ spec }) => spec)); const objects = targets diff --git a/scripts/src/platform-infra-gitea-desired-fragments.ts b/scripts/src/platform-infra-gitea-desired-fragments.ts index 00f6983a..3dca9c59 100644 --- a/scripts/src/platform-infra-gitea-desired-fragments.ts +++ b/scripts/src/platform-infra-gitea-desired-fragments.ts @@ -1,3 +1,4 @@ +import { renderAgentRunManagedRepositoryReconcilerFragment } from "./agentrun-managed-repository-reconciler"; import { renderPacAdmissionDesiredFragment } from "./platform-infra-pac-provenance"; import { renderPacConsumerRbacDesiredFragment } from "./platform-infra-pac-consumer-rbac"; @@ -5,6 +6,7 @@ export function renderPlatformInfraGiteaDesiredFragments(targetId: string): stri return [ renderPacConsumerRbacDesiredFragment(targetId), renderPacAdmissionDesiredFragment(targetId), + renderAgentRunManagedRepositoryReconcilerFragment(targetId), ].map(normalizeFragment).filter((item) => item.length > 0).join("\n---\n"); } diff --git a/scripts/src/platform-infra-gitea-gitops-delivery.test.ts b/scripts/src/platform-infra-gitea-gitops-delivery.test.ts index c382600c..8cd2e725 100644 --- a/scripts/src/platform-infra-gitea-gitops-delivery.test.ts +++ b/scripts/src/platform-infra-gitea-gitops-delivery.test.ts @@ -79,5 +79,8 @@ test("owning YAML renders one child Application and the complete durable bridge "RoleBinding", "ValidatingAdmissionPolicy", "ValidatingAdmissionPolicyBinding", + "ServiceAccount", + "ConfigMap", + "Deployment", ]); }); diff --git a/scripts/src/platform-infra-pac-provenance.test.ts b/scripts/src/platform-infra-pac-provenance.test.ts index 7a20cb7b..f81ef53d 100644 --- a/scripts/src/platform-infra-pac-provenance.test.ts +++ b/scripts/src/platform-infra-pac-provenance.test.ts @@ -78,7 +78,15 @@ test("required consumer default RBAC has one automatic desired owner and no Tekt const verbs = role.rules.flatMap((rule: any) => rule.verbs); for (const verb of ["create", "patch", "update", "delete", "deletecollection"]) expect(verbs).not.toContain(verb); const desiredKinds = documents(renderPlatformInfraGiteaDesiredFragments("NC01")).map((item) => item.kind); - expect(desiredKinds).toEqual(["Role", "RoleBinding", "ValidatingAdmissionPolicy", "ValidatingAdmissionPolicyBinding"]); + expect(desiredKinds).toEqual([ + "Role", + "RoleBinding", + "ValidatingAdmissionPolicy", + "ValidatingAdmissionPolicyBinding", + "ServiceAccount", + "ConfigMap", + "Deployment", + ]); }); test("live admission evaluator requires exact desired hashes, observed generation, default-SA loss, and a new resource epoch", () => {