From 8b518b0fb28144ad76a75f05a5addc3f19c49d77 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 14 Jul 2026 00:29:19 +0200 Subject: [PATCH] fix: patch runtime GitOps YAML workloads --- .../hwlab/runtime-gitops-postprocess.mjs | 61 +++++++--- .../hwlab/runtime-gitops-postprocess.test.ts | 113 ++++++++++++++++++ 2 files changed, 157 insertions(+), 17 deletions(-) create mode 100644 scripts/native/hwlab/runtime-gitops-postprocess.test.ts diff --git a/scripts/native/hwlab/runtime-gitops-postprocess.mjs b/scripts/native/hwlab/runtime-gitops-postprocess.mjs index fda803ee..a8623f74 100644 --- a/scripts/native/hwlab/runtime-gitops-postprocess.mjs +++ b/scripts/native/hwlab/runtime-gitops-postprocess.mjs @@ -2,8 +2,12 @@ // SPEC: PJ2026-01060703 CI/CD branch follower runtime GitOps postprocess. // Responsibility: mutate rendered runtime GitOps files after HWLAB source render, before publish. import { existsSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; import path from "node:path"; +const requireFromScript = createRequire(import.meta.url); +const requireFromCwd = createRequire(path.join(process.cwd(), "package.json")); +const YAML = requireYaml(); const repoDir = process.cwd(); const overlay = readOverlay(); const runtimePath = requiredOverlayString("runtimePath"); @@ -45,28 +49,29 @@ function patchCodeAgentRuntimeWorkloads() { const kafka = runtime.kafkaEventBridge; let changed = false; for (const file of listYamlFiles(runtimeDir)) { - const parsed = parseJsonDocument(readFileSync(file, "utf8")); - if (parsed === null) continue; - const items = isKubernetesList(parsed) ? parsed.items : [parsed]; + const documents = parseStructuredDocuments(readFileSync(file, "utf8"), file); let fileChanged = false; - for (const item of items) { - const workloadName = String(item?.metadata?.labels?.["app.kubernetes.io/name"] ?? item?.metadata?.name ?? ""); - const containers = item?.spec?.template?.spec?.containers; - if (!Array.isArray(containers)) continue; - for (const container of containers) { - if (!Array.isArray(container?.env)) continue; - if (workloadName === "hwlab-cloud-api" && container.name === "hwlab-cloud-api") { - fileChanged = patchCloudApiKafkaEnv(container, kafka) || fileChanged; - } - if (workloadName === "hwlab-cloud-web" && container.name === "hwlab-cloud-web") { - fileChanged = setEnvValue(container, "HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED", String(kafka.enabled && kafka.features.liveKafkaSse)) || fileChanged; - fileChanged = setEnvValue(container, "HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED", String(kafka.enabled && kafka.features.kafkaRefreshReplay)) || fileChanged; - fileChanged = setEnvValue(container, "HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED", String(kafka.enabled && kafka.features.projectionRealtime)) || fileChanged; + for (const document of documents.values) { + 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 ?? ""); + const containers = item?.spec?.template?.spec?.containers; + if (!Array.isArray(containers)) continue; + for (const container of containers) { + if (!Array.isArray(container?.env)) continue; + if (workloadName === "hwlab-cloud-api" && container.name === "hwlab-cloud-api") { + fileChanged = patchCloudApiKafkaEnv(container, kafka) || fileChanged; + } + if (workloadName === "hwlab-cloud-web" && container.name === "hwlab-cloud-web") { + fileChanged = setEnvValue(container, "HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED", String(kafka.enabled && kafka.features.liveKafkaSse)) || fileChanged; + fileChanged = setEnvValue(container, "HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED", String(kafka.enabled && kafka.features.kafkaRefreshReplay)) || fileChanged; + fileChanged = setEnvValue(container, "HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED", String(kafka.enabled && kafka.features.projectionRealtime)) || fileChanged; + } } } } if (fileChanged) { - writeFileSync(file, `${JSON.stringify(parsed, null, 2)}\n`, "utf8"); + writeFileSync(file, serializeStructuredDocuments(documents), "utf8"); changed = true; } } @@ -237,6 +242,20 @@ function parseJsonDocument(text) { } } +function parseStructuredDocuments(text, file) { + const json = parseJsonDocument(text); + if (json !== null) return { format: "json", values: [json] }; + const documents = YAML.parseAllDocuments(text); + const errors = documents.flatMap((document) => document.errors); + if (errors.length > 0) throw new Error(`invalid YAML in ${file}: ${errors.map((error) => error.message).join("; ")}`); + return { format: "yaml", values: documents.map((document) => document.toJS()).filter((document) => document !== null) }; +} + +function serializeStructuredDocuments(documents) { + if (documents.format === "json") return `${JSON.stringify(documents.values[0], null, 2)}\n`; + return `${documents.values.map((document) => YAML.stringify(document).trimEnd()).join("\n---\n")}\n`; +} + function leadingSpaces(value) { const match = value.match(/^ */u); return match ? match[0].length : 0; @@ -264,6 +283,14 @@ function readOverlay() { return JSON.parse(Buffer.from(encoded, "base64").toString("utf8")); } +function requireYaml() { + try { + return requireFromScript("yaml"); + } catch { + return requireFromCwd("yaml"); + } +} + function requiredOverlayString(name) { const value = overlay[name]; if (typeof value !== "string" || value.length === 0) throw new Error(`overlay.${name} is required`); diff --git a/scripts/native/hwlab/runtime-gitops-postprocess.test.ts b/scripts/native/hwlab/runtime-gitops-postprocess.test.ts new file mode 100644 index 00000000..f0958b6a --- /dev/null +++ b/scripts/native/hwlab/runtime-gitops-postprocess.test.ts @@ -0,0 +1,113 @@ +import { afterEach, expect, test } from "bun:test"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; + +const roots: string[] = []; +const script = resolve(import.meta.dir, "runtime-gitops-postprocess.mjs"); + +afterEach(() => { + while (roots.length > 0) rmSync(roots.pop()!, { recursive: true, force: true }); +}); + +test("patches YAML workloads across ordinary, multi-document, and Kubernetes List files", () => { + const root = mkdtempSync(join(tmpdir(), "runtime-gitops-postprocess-")); + roots.push(root); + const runtimeDir = join(root, "runtime"); + mkdirSync(runtimeDir); + writeFileSync(join(root, "overlay.json"), JSON.stringify(overlay("runtime"))); + writeFileSync(join(runtimeDir, "cloud-api.yaml"), deploymentYaml("hwlab-cloud-api")); + writeFileSync(join(runtimeDir, "cloud-web.yaml"), `apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: unchanged\n---\n${deploymentYaml("hwlab-cloud-web")}`); + writeFileSync(join(runtimeDir, "list.yaml"), `apiVersion: v1\nkind: List\nitems:\n${listItem(deploymentYaml("hwlab-cloud-api"))}\n`); + writeFileSync(join(runtimeDir, "json-workload.yaml"), `${JSON.stringify(Bun.YAML.parse(deploymentYaml("hwlab-cloud-api")), null, 2)}\n`); + + const result = spawnSync(process.execPath, [script], { + cwd: root, + env: { ...process.env, UNIDESK_RUNTIME_GITOPS_OVERLAY_FILE: join(root, "overlay.json") }, + encoding: "utf8", + }); + + if (result.status !== 0) throw new Error(result.stderr); + expect(result.stderr).toContain('"codeAgentRuntimeChanged":true'); + + const api = Bun.YAML.parse(readFileSync(join(runtimeDir, "cloud-api.yaml"), "utf8")) as any; + expect(authorityValues(api)).toEqual(["false", "false", "false", "true", "true", "true"]); + + const multiDocs = readFileSync(join(runtimeDir, "cloud-web.yaml"), "utf8").split(/^---$/mu).map((document) => Bun.YAML.parse(document) as any); + expect(multiDocs[0].kind).toBe("ConfigMap"); + expect(envValues(multiDocs[1])).toMatchObject({ + HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED: "false", + HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED: "false", + HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED: "true", + }); + + const list = Bun.YAML.parse(readFileSync(join(runtimeDir, "list.yaml"), "utf8")) as any; + expect(list.kind).toBe("List"); + expect(authorityValues(list.items[0])).toEqual(["false", "false", "false", "true", "true", "true"]); + + const jsonText = readFileSync(join(runtimeDir, "json-workload.yaml"), "utf8"); + expect(jsonText.startsWith("{\n")).toBe(true); + expect(authorityValues(JSON.parse(jsonText))).toEqual(["false", "false", "false", "true", "true", "true"]); + + const afterFirstRun = snapshot(runtimeDir); + const second = spawnSync(process.execPath, [script], { + cwd: root, + env: { ...process.env, UNIDESK_RUNTIME_GITOPS_OVERLAY_FILE: join(root, "overlay.json") }, + encoding: "utf8", + }); + if (second.status !== 0) throw new Error(second.stderr); + expect(second.stderr).toContain('"codeAgentRuntimeChanged":false'); + expect(snapshot(runtimeDir)).toEqual(afterFirstRun); +}); + +function overlay(runtimePath: string) { + return { + runtimePath, + codeAgentRuntime: { + enabled: true, + kafkaEventBridge: { + enabled: true, + directPublishConsumerGroupId: "hwlab-direct", + transactionalProjectorConsumerGroupId: "hwlab-projector", + features: { + directPublish: false, + liveKafkaSse: false, + kafkaRefreshReplay: false, + transactionalProjector: true, + projectionOutboxRelay: true, + projectionRealtime: true, + }, + }, + }, + }; +} + +function deploymentYaml(name: string) { + return `apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: ${name}\n labels:\n app.kubernetes.io/name: ${name}\nspec:\n template:\n spec:\n containers:\n - name: ${name}\n env:\n - name: EXISTING\n value: preserved\n`; +} + +function listItem(value: string) { + const [first, ...rest] = value.trimEnd().split("\n"); + return [` - ${first}`, ...rest.map((line) => ` ${line}`)].join("\n"); +} + +function envValues(workload: any) { + return Object.fromEntries(workload.spec.template.spec.containers[0].env.map((item: any) => [item.name, item.value])); +} + +function authorityValues(workload: any) { + const env = envValues(workload); + return [ + env.HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED, + env.HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED, + env.HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED, + env.HWLAB_KAFKA_TRANSACTIONAL_PROJECTOR_ENABLED, + env.HWLAB_KAFKA_PROJECTION_OUTBOX_RELAY_ENABLED, + env.HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED, + ]; +} + +function snapshot(runtimeDir: string) { + return ["cloud-api.yaml", "cloud-web.yaml", "list.yaml", "json-workload.yaml"].map((name) => readFileSync(join(runtimeDir, name), "utf8")); +}