From 30cd8ded85147ae8b831238ff4630bb05ea43dae Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 16 Jul 2026 23:02:18 +0200 Subject: [PATCH] fix: restore HWLAB Kafka runtime env rendering --- .../hwlab/runtime-gitops-postprocess.mjs | 5 ++ .../hwlab/runtime-gitops-postprocess.test.ts | 10 ++++ .../native/hwlab/runtime-gitops-verify.mjs | 33 +++++++++++ .../hwlab/runtime-gitops-verify.test.ts | 59 +++++++++++++++++-- ...-pipelines-as-code-source-artifact.test.ts | 4 +- 5 files changed, 103 insertions(+), 8 deletions(-) diff --git a/scripts/native/hwlab/runtime-gitops-postprocess.mjs b/scripts/native/hwlab/runtime-gitops-postprocess.mjs index d459b648..fbdbf81d 100644 --- a/scripts/native/hwlab/runtime-gitops-postprocess.mjs +++ b/scripts/native/hwlab/runtime-gitops-postprocess.mjs @@ -95,6 +95,11 @@ function patchCloudApiKafkaEnv(container, kafka) { let changed = false; changed = setEnvValue(container, "HWLAB_KAFKA_ENABLED", String(kafka.enabled)) || changed; changed = setEnvValue(container, "HWLAB_KAFKA_AGENTRUN_EVENT_CONSUME_ENABLED", String(kafka.enabled && (kafka.features.directPublish || kafka.features.transactionalProjector))) || changed; + changed = setEnvValue(container, "HWLAB_KAFKA_BOOTSTRAP_SERVERS", String(kafka.bootstrapServers)) || changed; + changed = setEnvValue(container, "HWLAB_KAFKA_STDIO_TOPIC", String(kafka.stdioTopic)) || changed; + changed = setEnvValue(container, "HWLAB_KAFKA_AGENTRUN_EVENT_TOPIC", String(kafka.agentRunEventTopic)) || changed; + changed = setEnvValue(container, "HWLAB_KAFKA_EVENT_TOPIC", String(kafka.hwlabEventTopic)) || changed; + changed = setEnvValue(container, "HWLAB_KAFKA_CLIENT_ID", String(kafka.clientId)) || changed; changed = setEnvValue(container, "HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED", String(kafka.enabled && kafka.features.directPublish)) || changed; changed = setEnvValue(container, "HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED", String(kafka.enabled && kafka.features.liveKafkaSse)) || changed; const refreshReplayEnabled = kafka.enabled && kafka.features.kafkaRefreshReplay; diff --git a/scripts/native/hwlab/runtime-gitops-postprocess.test.ts b/scripts/native/hwlab/runtime-gitops-postprocess.test.ts index 31875505..28bf8b28 100644 --- a/scripts/native/hwlab/runtime-gitops-postprocess.test.ts +++ b/scripts/native/hwlab/runtime-gitops-postprocess.test.ts @@ -39,6 +39,11 @@ test("patches YAML-owned Code Agent and Kafka runtime settings", () => { HWLAB_CODE_AGENT_AGENTRUN_SECRET_NAMESPACE: "agentrun-v02", HWLAB_KAFKA_ENABLED: "true", HWLAB_KAFKA_AGENTRUN_EVENT_CONSUME_ENABLED: "true", + HWLAB_KAFKA_BOOTSTRAP_SERVERS: "kafka.platform-infra.svc.cluster.local:9092", + HWLAB_KAFKA_STDIO_TOPIC: "agentrun.stdio.v1", + HWLAB_KAFKA_AGENTRUN_EVENT_TOPIC: "agentrun.event.v1", + HWLAB_KAFKA_EVENT_TOPIC: "hwlab.event.v1", + HWLAB_KAFKA_CLIENT_ID: "hwlab-v03", HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED: "true", HWLAB_KAFKA_TRANSACTIONAL_PROJECTOR_ENABLED: "false", HWLAB_KAFKA_PROJECTOR_GROUP_ID: "hwlab-projector", @@ -88,6 +93,11 @@ function overlay(runtimePath: string) { codexStdioSupervisor: "repo-owned", kafkaEventBridge: { enabled: true, + bootstrapServers: "kafka.platform-infra.svc.cluster.local:9092", + stdioTopic: "agentrun.stdio.v1", + agentRunEventTopic: "agentrun.event.v1", + hwlabEventTopic: "hwlab.event.v1", + clientId: "hwlab-v03", features: { directPublish: true, liveKafkaSse: true, diff --git a/scripts/native/hwlab/runtime-gitops-verify.mjs b/scripts/native/hwlab/runtime-gitops-verify.mjs index 88076b18..50049d6d 100644 --- a/scripts/native/hwlab/runtime-gitops-verify.mjs +++ b/scripts/native/hwlab/runtime-gitops-verify.mjs @@ -26,10 +26,43 @@ if (overlay?.observability?.prometheusOperator === false) { } if (overlay?.codeAgentRuntime?.enabled && overlay.codeAgentRuntime.kafkaEventBridge?.enabled) { checks.push("code-agent-runtime-kafka-event-bridge-enabled"); + verifyCloudApiKafkaEnv(overlay.codeAgentRuntime.kafkaEventBridge); } console.error(JSON.stringify({ event: "unidesk-runtime-gitops-verify", ok: true, runtimePath, checks })); +function verifyCloudApiKafkaEnv(kafka) { + const expected = { + HWLAB_KAFKA_BOOTSTRAP_SERVERS: String(kafka.bootstrapServers), + HWLAB_KAFKA_STDIO_TOPIC: String(kafka.stdioTopic), + HWLAB_KAFKA_AGENTRUN_EVENT_TOPIC: String(kafka.agentRunEventTopic), + HWLAB_KAFKA_EVENT_TOPIC: String(kafka.hwlabEventTopic), + HWLAB_KAFKA_CLIENT_ID: String(kafka.clientId), + }; + const workloads = []; + const mismatches = []; + for (const file of listYamlFiles(runtimeDir)) { + for (const document of parseStructuredDocuments(readFileSync(file, "utf8"), file)) { + const items = isKubernetesList(document) ? document.items : [document]; + for (const item of items) { + const workloadName = String(item?.metadata?.labels?.["app.kubernetes.io/name"] ?? item?.metadata?.name ?? ""); + if (workloadName !== "hwlab-cloud-api") continue; + for (const container of item?.spec?.template?.spec?.containers ?? []) { + if (container?.name !== "hwlab-cloud-api") continue; + const ref = { file: path.relative(repoDir, file), workload: workloadName, container: container.name }; + workloads.push(ref); + const env = new Map((container.env ?? []).map((entry) => [entry?.name, entry?.value])); + for (const [name, value] of Object.entries(expected)) { + if (env.get(name) !== value) mismatches.push({ ...ref, name, expected: value, actual: env.get(name) ?? null }); + } + } + } + } + } + if (workloads.length === 0) fail("code-agent-runtime-workload-missing", { runtimePath, workload: "hwlab-cloud-api" }); + if (mismatches.length > 0) fail("code-agent-runtime-kafka-env-invalid", { runtimePath, mismatches: mismatches.slice(0, 12), mismatchCount: mismatches.length }); +} + function findPrometheusOperatorResources() { const refs = []; for (const file of listYamlFiles(runtimeDir)) { diff --git a/scripts/native/hwlab/runtime-gitops-verify.test.ts b/scripts/native/hwlab/runtime-gitops-verify.test.ts index 3f2da472..4dd766b1 100644 --- a/scripts/native/hwlab/runtime-gitops-verify.test.ts +++ b/scripts/native/hwlab/runtime-gitops-verify.test.ts @@ -11,16 +11,16 @@ afterEach(() => { while (roots.length > 0) rmSync(roots.pop()!, { recursive: true, force: true }); }); -test("architecture capability env and workload matches are not runtime GitOps gates", () => { +test("accepts YAML-owned Kafka env on the cloud-api workload", () => { const root = mkdtempSync(join(tmpdir(), "runtime-gitops-verify-")); roots.push(root); mkdirSync(join(root, "runtime")); - writeFileSync(join(root, "runtime", "unrelated.yaml"), "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: unrelated\n"); + writeFileSync(join(root, "runtime", "cloud-api.yaml"), deploymentYaml(kafkaEnv())); writeFileSync(join(root, "overlay.json"), JSON.stringify({ runtimePath: "runtime", codeAgentRuntime: { enabled: true, - kafkaEventBridge: { enabled: true }, + kafkaEventBridge: { enabled: true, ...kafkaValues() }, }, })); @@ -32,7 +32,54 @@ test("architecture capability env and workload matches are not runtime GitOps ga expect(result.status).toBe(0); expect(result.stderr).toContain('"code-agent-runtime-kafka-event-bridge-enabled"'); - expect(result.stderr).not.toContain("capability-env-invalid"); - expect(result.stderr).not.toContain("workload-missing"); - expect(result.stderr).not.toContain("authority-invalid"); }); + +test("rejects missing YAML-owned Kafka env before GitOps publish", () => { + const root = mkdtempSync(join(tmpdir(), "runtime-gitops-verify-")); + roots.push(root); + mkdirSync(join(root, "runtime")); + writeFileSync(join(root, "runtime", "cloud-api.yaml"), deploymentYaml({ ...kafkaEnv(), HWLAB_KAFKA_CLIENT_ID: undefined })); + writeFileSync(join(root, "overlay.json"), JSON.stringify({ + runtimePath: "runtime", + codeAgentRuntime: { enabled: true, kafkaEventBridge: { enabled: true, ...kafkaValues() } }, + })); + + const result = spawnSync(process.execPath, [script], { + cwd: root, + env: { ...process.env, UNIDESK_RUNTIME_GITOPS_OVERLAY_FILE: join(root, "overlay.json") }, + encoding: "utf8", + }); + + expect(result.status).toBe(48); + expect(result.stderr).toContain('"reason":"code-agent-runtime-kafka-env-invalid"'); + expect(result.stderr).toContain('"name":"HWLAB_KAFKA_CLIENT_ID"'); +}); + +function kafkaValues() { + return { + bootstrapServers: "kafka.platform-infra.svc.cluster.local:9092", + stdioTopic: "agentrun.stdio.v1", + agentRunEventTopic: "agentrun.event.v1", + hwlabEventTopic: "hwlab.event.v1", + clientId: "hwlab-production", + }; +} + +function kafkaEnv(): Record { + const kafka = kafkaValues(); + return { + HWLAB_KAFKA_BOOTSTRAP_SERVERS: kafka.bootstrapServers, + HWLAB_KAFKA_STDIO_TOPIC: kafka.stdioTopic, + HWLAB_KAFKA_AGENTRUN_EVENT_TOPIC: kafka.agentRunEventTopic, + HWLAB_KAFKA_EVENT_TOPIC: kafka.hwlabEventTopic, + HWLAB_KAFKA_CLIENT_ID: kafka.clientId, + }; +} + +function deploymentYaml(env: Record) { + const entries = Object.entries(env) + .filter((entry): entry is [string, string] => entry[1] !== undefined) + .map(([name, value]) => ` - name: ${name}\n value: ${value}`) + .join("\n"); + return `apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: hwlab-cloud-api\n labels:\n app.kubernetes.io/name: hwlab-cloud-api\nspec:\n template:\n spec:\n containers:\n - name: hwlab-cloud-api\n env:\n${entries}\n`; +} diff --git a/scripts/src/platform-infra-pipelines-as-code-source-artifact.test.ts b/scripts/src/platform-infra-pipelines-as-code-source-artifact.test.ts index 02c910f5..e068c79f 100644 --- a/scripts/src/platform-infra-pipelines-as-code-source-artifact.test.ts +++ b/scripts/src/platform-infra-pipelines-as-code-source-artifact.test.ts @@ -777,9 +777,9 @@ describe("runtime source-commit selection", () => { enabled: true, transactionalProjectorConsumerGroupId: "hwlab-v03-agentrun-event-projector", hwlabEventConsumerGroupId: "hwlab-v03-workbench-live-sse", + features: expect.any(Object), + refreshReplay: expect.any(Object), }); - expect(spec.codeAgentRuntime?.kafkaEventBridge).not.toHaveProperty("features"); - expect(spec.codeAgentRuntime?.kafkaEventBridge).not.toHaveProperty("refreshReplay"); expect(remoteAnnotations).toEqual(pipelineProvenanceAnnotations(remoteProvenance)); expect(Object.keys(remoteAnnotations).sort()).toEqual(Object.values(pipelineProvenanceAnnotationKeys).sort()); expect(readFileSync(rootPath("scripts", "src", "hwlab-node", "render.ts"), "utf8")).not.toContain("overlay.sourceArtifactProvenance");