feat(cicd): 注册 AgentRun managed repository fragment

This commit is contained in:
Codex
2026-07-12 00:12:55 +02:00
parent c466f54ba4
commit 49f13e404e
6 changed files with 69 additions and 6 deletions
+10 -3
View File
@@ -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<string, unknown> {
@@ -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 {
@@ -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
@@ -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");
}
@@ -79,5 +79,8 @@ test("owning YAML renders one child Application and the complete durable bridge
"RoleBinding",
"ValidatingAdmissionPolicy",
"ValidatingAdmissionPolicyBinding",
"ServiceAccount",
"ConfigMap",
"Deployment",
]);
});
@@ -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", () => {