Merge pull request #1987 from pikasTech/fix/1981-feature-config-schema
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
Pipelines as Code CI / platform-infra-gitea-nc01- Success
Pipelines as Code CI / unidesk-host- Failed

feat(cicd): 接入非阻塞 feature config schema 校验
This commit is contained in:
Lyon
2026-07-14 10:55:00 +08:00
committed by GitHub
38 changed files with 1160 additions and 493 deletions
+16
View File
@@ -2,6 +2,8 @@
// Renders AgentRun YAML lane policy into runtime manager environment.
import { createHash } from "node:crypto";
import type { AgentRunLaneSpec } from "./agentrun-lanes";
import { gitRepositoryIdentity, resolveCicdDeliveryAuthority } from "./cicd-delivery-authority";
import { renderPacFeatureConfigSchemaStage } from "./pac-feature-config-schema-stage";
import { stableJsonSha256 } from "./stable-json";
export interface AgentRunArtifactService {
@@ -408,6 +410,19 @@ function agentRunTektonGitopsPublishScript(spec: AgentRunLaneSpec): string {
const templateImage = agentRunImageArtifact(spec, { sourceCommit: sourcePlaceholder, envIdentity: envPlaceholder, digest: digestPlaceholder, status: statusPlaceholder });
const templateFiles = renderAgentRunGitopsFiles(spec, { sourceCommit: sourcePlaceholder, image: templateImage });
const templateB64 = Buffer.from(JSON.stringify(templateFiles), "utf8").toString("base64");
const authority = resolveCicdDeliveryAuthority({
node: spec.nodeId,
lane: spec.lane,
sourceRepository: gitRepositoryIdentity(spec.source.repository),
sourceBranch: spec.source.branch,
});
if (authority.kind !== "pac-pr-merge") throw new Error(`AgentRun ${spec.nodeId}/${spec.lane} requires exact PaC consumer authority for feature config validation`);
const featureConfigSchemaStage = renderPacFeatureConfigSchemaStage({
repositoryId: authority.consumer.repositoryRef,
consumer: authority.consumer.consumerId,
repoDirExpression: '"$root"',
renderedRootExpression: '"$PWD/$(params.gitops-root)"',
});
return [
"#!/bin/sh",
"set -eu",
@@ -446,6 +461,7 @@ function agentRunTektonGitopsPublishScript(spec: AgentRunLaneSpec): string {
" writeFileSync(file.path, content);",
"}",
"NODE",
featureConfigSchemaStage,
"git add source.json \"$(params.artifact-catalog)\" \"$(params.gitops-root)\"",
"if git diff --quiet --cached; then changed=false; else changed=true; git -c user.email=agentrun@unidesk.local -c user.name='UniDesk AgentRun PaC' commit -m \"deploy: render AgentRun $(params.gitops-branch) from PaC\"; fi",
"git remote set-url origin \"$git_auth_url\"",
+9 -68
View File
@@ -545,22 +545,6 @@ export interface HwlabRuntimeKafkaShadowProducerSpec {
export interface HwlabRuntimeKafkaEventBridgeSpec {
readonly enabled: boolean;
readonly authority: "transactional-projection";
readonly features: {
readonly directPublish: boolean;
readonly liveKafkaSse: boolean;
readonly kafkaRefreshReplay: boolean;
readonly transactionalProjector: boolean;
readonly projectionOutboxRelay: boolean;
readonly projectionRealtime: boolean;
};
readonly refreshReplay?: {
readonly groupIdPrefix: string;
readonly timeoutMs: number;
readonly scanLimit: number;
readonly matchedEventLimit: number;
readonly liveBufferLimit: number;
};
readonly transactionalProjector?: {
readonly heartbeatIntervalMs: number;
};
@@ -589,7 +573,6 @@ export interface HwlabRuntimeKafkaEventBridgeSpec {
readonly agentRunEventTopic: string;
readonly hwlabEventTopic: string;
readonly clientId: string;
readonly directPublishConsumerGroupId: string;
readonly transactionalProjectorConsumerGroupId: string;
readonly hwlabEventConsumerGroupId: string;
}
@@ -1245,27 +1228,8 @@ function codeAgentKafkaShadowProducerConfig(value: unknown, path: string): Hwlab
function codeAgentKafkaEventBridgeConfig(value: unknown, path: string): HwlabRuntimeKafkaEventBridgeSpec {
const raw = asRecord(value, path);
const features = asRecord(raw.features, `${path}.features`);
if (raw.refreshReplay !== undefined) throw new Error(`${path}.refreshReplay was removed with the fixed projection realtime architecture`);
const enabled = booleanField(raw, "enabled", path);
const authority = enumStringField(raw, "authority", path, ["transactional-projection"] as const);
const directPublish = booleanField(features, "directPublish", `${path}.features`);
const liveKafkaSse = booleanField(features, "liveKafkaSse", `${path}.features`);
const kafkaRefreshReplay = booleanField(features, "kafkaRefreshReplay", `${path}.features`);
const transactionalProjector = booleanField(features, "transactionalProjector", `${path}.features`);
const projectionOutboxRelay = booleanField(features, "projectionOutboxRelay", `${path}.features`);
const projectionRealtime = booleanField(features, "projectionRealtime", `${path}.features`);
const refreshReplay = raw.refreshReplay === undefined
? undefined
: codeAgentKafkaRefreshReplayConfig(raw.refreshReplay, `${path}.refreshReplay`);
if (kafkaRefreshReplay && refreshReplay === undefined) {
throw new Error(`${path}.refreshReplay is required when ${path}.features.kafkaRefreshReplay is true`);
}
if (kafkaRefreshReplay && !liveKafkaSse) {
throw new Error(`${path}.features.liveKafkaSse must be true when ${path}.features.kafkaRefreshReplay is true`);
}
if (!enabled || directPublish || liveKafkaSse || kafkaRefreshReplay || !transactionalProjector || !projectionOutboxRelay || !projectionRealtime) {
throw new Error(`${path}.authority transactional-projection requires enabled transactionalProjector/projectionOutboxRelay/projectionRealtime and disables directPublish/liveKafkaSse/kafkaRefreshReplay`);
}
const transactionalProjectorConfig = raw.transactionalProjector === undefined
? undefined
: codeAgentKafkaTransactionalProjectorConfig(raw.transactionalProjector, `${path}.transactionalProjector`);
@@ -1275,30 +1239,19 @@ function codeAgentKafkaEventBridgeConfig(value: unknown, path: string): HwlabRun
const projectionRealtimeConfig = raw.projectionRealtime === undefined
? undefined
: codeAgentKafkaProjectionRealtimeConfig(raw.projectionRealtime, `${path}.projectionRealtime`);
if (transactionalProjector && transactionalProjectorConfig === undefined) throw new Error(`${path}.transactionalProjector is required when ${path}.features.transactionalProjector is true`);
if (projectionOutboxRelay && projectionOutboxRelayConfig === undefined) throw new Error(`${path}.projectionOutboxRelay is required when ${path}.features.projectionOutboxRelay is true`);
if (projectionRealtime && projectionRealtimeConfig === undefined) throw new Error(`${path}.projectionRealtime is required when ${path}.features.projectionRealtime is true`);
const directPublishConsumerGroupId = stringField(raw, "directPublishConsumerGroupId", path);
if (transactionalProjectorConfig === undefined) throw new Error(`${path}.transactionalProjector is required by the fixed transactional projection architecture`);
if (projectionOutboxRelayConfig === undefined) throw new Error(`${path}.projectionOutboxRelay is required by the fixed transactional projection architecture`);
if (projectionRealtimeConfig === undefined) throw new Error(`${path}.projectionRealtime is required by the fixed transactional projection architecture`);
const transactionalProjectorConsumerGroupId = stringField(raw, "transactionalProjectorConsumerGroupId", path);
const hwlabEventConsumerGroupId = stringField(raw, "hwlabEventConsumerGroupId", path);
if (new Set([directPublishConsumerGroupId, transactionalProjectorConsumerGroupId, hwlabEventConsumerGroupId]).size !== 3) {
throw new Error(`${path} consumer group ids must be distinct so enabled capabilities receive independent event streams`);
if (transactionalProjectorConsumerGroupId === hwlabEventConsumerGroupId) {
throw new Error(`${path} projector and realtime consumer group ids must be distinct`);
}
return {
enabled,
authority,
features: {
directPublish,
liveKafkaSse,
kafkaRefreshReplay,
transactionalProjector,
projectionOutboxRelay,
projectionRealtime,
},
...(refreshReplay === undefined ? {} : { refreshReplay }),
...(transactionalProjectorConfig === undefined ? {} : { transactionalProjector: transactionalProjectorConfig }),
...(projectionOutboxRelayConfig === undefined ? {} : { projectionOutboxRelay: projectionOutboxRelayConfig }),
...(projectionRealtimeConfig === undefined ? {} : { projectionRealtime: projectionRealtimeConfig }),
transactionalProjector: transactionalProjectorConfig,
projectionOutboxRelay: projectionOutboxRelayConfig,
projectionRealtime: projectionRealtimeConfig,
healthThresholds: codeAgentKafkaHealthThresholdsConfig(raw.healthThresholds, `${path}.healthThresholds`),
configRef: stringField(raw, "configRef", path),
bootstrapServers: stringField(raw, "bootstrapServers", path),
@@ -1306,7 +1259,6 @@ function codeAgentKafkaEventBridgeConfig(value: unknown, path: string): HwlabRun
agentRunEventTopic: stringField(raw, "agentRunEventTopic", path),
hwlabEventTopic: stringField(raw, "hwlabEventTopic", path),
clientId: stringField(raw, "clientId", path),
directPublishConsumerGroupId,
transactionalProjectorConsumerGroupId,
hwlabEventConsumerGroupId,
};
@@ -1348,17 +1300,6 @@ function codeAgentKafkaHealthThresholdsConfig(value: unknown, path: string): Hwl
};
}
function codeAgentKafkaRefreshReplayConfig(value: unknown, path: string): NonNullable<HwlabRuntimeKafkaEventBridgeSpec["refreshReplay"]> {
const raw = asRecord(value, path);
return {
groupIdPrefix: stringField(raw, "groupIdPrefix", path),
timeoutMs: numberField(raw, "timeoutMs", path),
scanLimit: numberField(raw, "scanLimit", path),
matchedEventLimit: numberField(raw, "matchedEventLimit", path),
liveBufferLimit: numberField(raw, "liveBufferLimit", path),
};
}
function commandNameField(value: string, path: string): string {
if (!/^[A-Za-z0-9._+-]+$/u.test(value)) throw new Error(`${path} must be a simple command name`);
return value;
@@ -51,8 +51,8 @@ test("realtime fanout keeps the YAML terminal timeout when durationMs is absent"
conditionalEventTypePairs: { "tool-call": { agentrun: "tool_call", hwlab: "tool" } },
expectedKafka: {
topics: { stdio: "codex-stdio.raw.v1", agentrun: "agentrun.event.v1", hwlab: "hwlab.event.v1" },
groups: { directPublish: "direct", transactionalProjector: "projector", liveSse: "live" },
capabilities: { directPublish: true, liveKafkaSse: true, kafkaRefreshReplay: true },
groups: { transactionalProjector: "projector", liveSse: "live" },
capabilities: { transactionalProjector: true, projectionOutboxRelay: true, projectionRealtime: true },
},
};
@@ -194,7 +194,7 @@ test("realtime fanout validates stdio against the scoped AgentRun session lineag
assert.equal(expectedStreamSessionId("hwlab", hwlabSessionId), hwlabSessionId);
});
test("realtime fanout validates YAML-owned refresh and legacy live-only connected contracts", () => {
test("realtime fanout validates the fixed transactional replay and live connected contract", () => {
const source = nodeWebObserveRunnerRealtimeSource();
const build = new Function(`${source}\nreturn realtimeAssertConnectedContract;`) as () => (
connected: Record<string, unknown>,
@@ -207,7 +207,7 @@ test("realtime fanout validates YAML-owned refresh and legacy live-only connecte
const sessionId = "ses_refresh_contract";
const expectedKafka = {
topics: { hwlab: "hwlab.event.v1" },
capabilities: { directPublish: true, liveKafkaSse: true, kafkaRefreshReplay: true },
capabilities: { transactionalProjector: true, projectionOutboxRelay: true, projectionRealtime: true },
};
const refreshProfile = {
expectedKafka,
@@ -249,28 +249,6 @@ test("realtime fanout validates YAML-owned refresh and legacy live-only connecte
/replayed count exceeds matched count/u,
);
const liveProfile = {
expectedKafka: { topics: { hwlab: "hwlab.event.v1" }, capabilities: { directPublish: true, liveKafkaSse: true, kafkaRefreshReplay: false } },
expectedProductSse: {
deliverySemantics: "live-only",
liveOnly: true,
replay: false,
replaySupported: false,
lossPossible: true,
refreshHandoff: false,
replayPriorTraceOnReconnect: false,
},
};
assert.doesNotThrow(() => assertConnected({
realtimeSource: "hwlab.event.v1",
deliverySemantics: "live-only",
liveOnly: true,
replay: false,
replaySupported: false,
lossPossible: true,
filters: { sessionId, traceId: null },
capabilities: liveProfile.expectedKafka.capabilities,
}, sessionId, "live", liveProfile));
});
test("existing session refresh rejects duplicate or reordered Workbench identities", () => {
@@ -326,7 +304,7 @@ test("existing session refresh preserves rejected connected evidence and rejects
counts: { matched: 8, replayed: 8, buffered: 1, bufferedDelivered: 1, liveDelivered: 0, deduplicated: 0 },
};
const profile = {
expectedKafka: { topics: { hwlab: "hwlab.event.v1" }, capabilities: { directPublish: true, liveKafkaSse: true, kafkaRefreshReplay: true } },
expectedKafka: { topics: { hwlab: "hwlab.event.v1" }, capabilities: { transactionalProjector: true, projectionOutboxRelay: true, projectionRealtime: true } },
expectedProductSse: { deliverySemantics: "kafka-retention-then-live", liveOnly: false, replay: true, replaySupported: true, lossPossible: false, refreshHandoff: true },
};
const connected = {
@@ -803,11 +803,18 @@ function realtimeFanoutEffectiveProfile(profile, durationMs) {
if (!expectedProductSse || typeof expectedProductSse.deliverySemantics !== "string") throw new Error("realtime fanout profile lacks YAML-owned product SSE expectations");
const expectedKafka = profile.expectedKafka && typeof profile.expectedKafka === "object" ? profile.expectedKafka : null;
if (!expectedKafka?.topics || !expectedKafka?.groups || !expectedKafka?.capabilities) throw new Error("realtime fanout profile lacks YAML-derived Kafka expectations");
if (expectedKafka.capabilities.directPublish !== true || expectedKafka.capabilities.liveKafkaSse !== true) {
throw new Error("realtime fanout profile requires YAML-enabled directPublish and liveKafkaSse capabilities");
if (expectedKafka.capabilities.transactionalProjector !== true
|| expectedKafka.capabilities.projectionOutboxRelay !== true
|| expectedKafka.capabilities.projectionRealtime !== true) {
throw new Error("realtime fanout profile requires the fixed transactional projection capabilities");
}
if (expectedProductSse.refreshHandoff !== (expectedKafka.capabilities.kafkaRefreshReplay === true)) {
throw new Error("realtime fanout product SSE refreshHandoff differs from the YAML Kafka capability");
if (expectedProductSse.deliverySemantics !== "kafka-retention-then-live"
|| expectedProductSse.liveOnly !== false
|| expectedProductSse.replay !== true
|| expectedProductSse.replaySupported !== true
|| expectedProductSse.lossPossible !== false
|| expectedProductSse.refreshHandoff !== true) {
throw new Error("realtime fanout product SSE must use fixed retention replay and live handoff semantics");
}
if (expectedProductSse.replayPriorTraceOnReconnect === true && expectedProductSse.replay !== true) {
throw new Error("realtime fanout replayPriorTraceOnReconnect requires replay=true");
@@ -791,7 +791,7 @@ export function sentinelPublishImageBuildShell(state: SentinelCicdState, jobName
].join("\n");
}
export function sentinelPublishShell(state: SentinelCicdState, jobName: string, publishGitops: boolean): string {
export function sentinelPublishShell(state: SentinelCicdState, jobName: string, publishGitops: boolean, featureConfigSchemaStage = ""): string {
const gitopsFiles = publishGitops ? sentinelGitopsFiles(state) : [];
const filesB64 = Buffer.from(JSON.stringify(gitopsFiles.map((file) => ({
path: file.path,
@@ -871,6 +871,7 @@ export function sentinelPublishShell(state: SentinelCicdState, jobName: string,
"}",
"console.error(JSON.stringify({event:'web-probe-sentinel-gitops-files', fileCount: files.length, valuesRedacted:true}));",
"NODE",
...(featureConfigSchemaStage.length === 0 ? [] : [featureConfigSchemaStage]),
" git add .",
" file_count=$(git diff --cached --name-only | wc -l | tr -d ' ')",
" if git diff --quiet --cached; then changed=false; else changed=true; git -c user.email=web-probe-sentinel@unidesk.local -c user.name='UniDesk Web Probe Sentinel' commit -m \"deploy: render web-probe sentinel ${source_commit}\"; fi",
+29 -55
View File
@@ -44,12 +44,15 @@ import { nodeRuntimeDeployYamlOverlayShellScript } from "./deploy-overlay";
import { pipelineProvenanceAnnotationKeys } from "../pipeline-provenance";
import { hwlabNodeDeliveryAuthority, hwlabNodeDeliveryNext } from "./delivery-authority";
import type { CicdDeliveryAuthority } from "../cicd-delivery-authority";
import { renderPacFeatureConfigSchemaStage } from "../pac-feature-config-schema-stage";
const runtimeGitopsObservabilityNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-gitops-observability.mjs"), "utf8").trimEnd();
const runtimeGitopsPipelineGuardNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-gitops-pipeline-guard.mjs"), "utf8").trimEnd();
const runtimeGitopsScriptsConfigMapNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-gitops-scripts-configmap.mjs"), "utf8").trimEnd();
const runtimeGitopsPostprocessNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-gitops-postprocess.mjs"), "utf8").trimEnd();
const runtimeGitopsVerifyNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-gitops-verify.mjs"), "utf8").trimEnd();
const featureConfigSchemaWarningNativeScript = readFileSync(rootPath("scripts/native/cicd/feature-config-schema-warning.mjs"), "utf8").trimEnd();
const ajv2020BundleScript = readFileSync(rootPath("node_modules/ajv-dist/dist/ajv2020.min.js"), "utf8").trimEnd();
const runtimePipelineProvenanceNativeScript = readFileSync(rootPath("scripts/native/hwlab/runtime-pipeline-provenance.mjs"), "utf8").trimEnd();
export function nodeRuntimeGitMirrorJobName(mirror: NodeRuntimeGitMirrorTargetSpec, action: "sync" | "flush"): string {
@@ -1540,12 +1543,24 @@ export function renderNodeRuntimeControlPlaneOnNode(spec: HwlabRuntimeLaneSpec,
`--web-endpoint ${shellQuote(spec.publicWebUrl)}`,
`--out ${shellQuote(renderDir)}`,
].join(" "),
...nodeRuntimePipelinePostprocessScript(),
...nodeRuntimePipelinePostprocessScript(nodeRuntimeFeatureConfigSchemaStage(spec)),
].join("\n");
return { result: runNodeHostScriptAsync(spec, script, timeoutSeconds, `${spec.nodeId.toLowerCase()}-${spec.lane}-render`), renderDir, worktreeDir, location: "node-host" };
}
export function nodeRuntimePipelinePostprocessScript(): string[] {
export function nodeRuntimeFeatureConfigSchemaStage(spec: HwlabRuntimeLaneSpec): string {
const authority = hwlabNodeDeliveryAuthority({ node: spec.nodeId, lane: spec.lane, spec });
if (authority.kind !== "pac-pr-merge") throw new Error(`HWLAB ${spec.nodeId}/${spec.lane} requires exact PaC consumer authority for feature config validation`);
return renderPacFeatureConfigSchemaStage({
repositoryId: authority.consumer.repositoryRef,
consumer: authority.consumer.consumerId,
repoDirExpression: '"$PWD"',
renderedRootExpression: JSON.stringify(spec.runtimePath),
ajvBundleExpression: "'/etc/unidesk-cicd-runtime-gitops/ajv2020.min.js'",
});
}
export function nodeRuntimePipelinePostprocessScript(featureConfigSchemaStage: string): string[] {
return [
"node - \"$render_dir\" \"$overlay_b64\" <<'NODE'",
"const fs = require('fs');",
@@ -1554,6 +1569,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
"const renderDir = process.argv[2];",
"const overlay = JSON.parse(Buffer.from(process.argv[3], 'base64').toString('utf8'));",
`const runtimeGitopsObservabilityNativeScript = ${JSON.stringify(runtimeGitopsObservabilityNativeScript)};`,
`const featureConfigSchemaStage = ${JSON.stringify(featureConfigSchemaStage)};`,
"const pipelinePath = path.join(renderDir, overlay.tektonDir, 'pipeline.yaml');",
"let text = fs.readFileSync(pipelinePath, 'utf8');",
"let YAML = null;",
@@ -1582,6 +1598,9 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
"echo '{\"event\":\"prepare-source-dependencies\",\"status\":\"not-required\",\"dependency\":\"yaml\",\"manager\":\"inline-service-id-parser\"}' >&2",
"ci_timing_emit prepare-source-dependencies succeeded \"$prepare_source_dependencies_started_ms\"`;",
"}",
"function featureConfigSchemaValidationScript() {",
" return featureConfigSchemaStage;",
"}",
"function validatePrepareSourceDependencyScript(script) {",
" const marker = 'NODE_UNIDESK_YAML_DEPENDENCY';",
" let offset = 0;",
@@ -1838,12 +1857,6 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
"function cloudWebRuntimeEnvEntries() {",
" const exposure = overlay.publicExposure;",
" const entries = !exposure || !Array.isArray(exposure.extraProxies) ? [] : exposure.extraProxies.filter((proxy) => proxy && proxy.cloudWebEnvName && proxy.publicBaseUrl).map((proxy) => ({ name: String(proxy.cloudWebEnvName), value: String(proxy.publicBaseUrl) }));",
" const kafka = overlay.codeAgentRuntime && overlay.codeAgentRuntime.kafkaEventBridge;",
" if (kafka) {",
" entries.push({ name: 'HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED', value: String(kafka.enabled && kafka.features.liveKafkaSse) });",
" entries.push({ name: 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED', value: String(kafka.enabled && kafka.features.kafkaRefreshReplay) });",
" entries.push({ name: 'HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED', value: String(kafka.enabled && kafka.features.projectionRealtime) });",
" }",
" return entries;",
"}",
"function startupProbeFrom(probe) {",
@@ -1941,37 +1954,20 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
" if (codeAgentRuntime.kafkaEventBridge) {",
" const kafka = codeAgentRuntime.kafkaEventBridge;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_ENABLED', String(kafka.enabled)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_AGENTRUN_EVENT_CONSUME_ENABLED', String(kafka.enabled && (kafka.features.directPublish || kafka.features.transactionalProjector))) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_AGENTRUN_EVENT_CONSUME_ENABLED', String(kafka.enabled)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_BOOTSTRAP_SERVERS', String(kafka.bootstrapServers)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_STDIO_TOPIC', String(kafka.stdioTopic)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_AGENTRUN_EVENT_TOPIC', String(kafka.agentRunEventTopic)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_EVENT_TOPIC', String(kafka.hwlabEventTopic)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_CLIENT_ID', String(kafka.clientId)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_AGENTRUN_EVENT_GROUP_ID', String(kafka.directPublishConsumerGroupId)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_PROJECTOR_GROUP_ID', String(kafka.transactionalProjectorConsumerGroupId)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_HWLAB_EVENT_GROUP_ID', String(kafka.hwlabEventConsumerGroupId)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED', String(kafka.enabled && kafka.features.directPublish)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED', String(kafka.enabled && kafka.features.liveKafkaSse)) || codeAgentRuntimeChanged;",
" const refreshReplayEnabled = kafka.enabled && kafka.features.kafkaRefreshReplay;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED', String(refreshReplayEnabled)) || codeAgentRuntimeChanged;",
" if (refreshReplayEnabled) {",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_GROUP_PREFIX', String(kafka.refreshReplay.groupIdPrefix)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_TIMEOUT_MS', String(kafka.refreshReplay.timeoutMs)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_SCAN_LIMIT', String(kafka.refreshReplay.scanLimit)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_MATCHED_EVENT_LIMIT', String(kafka.refreshReplay.matchedEventLimit)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_LIVE_BUFFER_LIMIT', String(kafka.refreshReplay.liveBufferLimit)) || codeAgentRuntimeChanged;",
" } else {",
" codeAgentRuntimeChanged = removeEnvValues(container, ['HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_GROUP_PREFIX', 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_TIMEOUT_MS', 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_SCAN_LIMIT', 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_MATCHED_EVENT_LIMIT', 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_LIVE_BUFFER_LIMIT']) || codeAgentRuntimeChanged;",
" }",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_TRANSACTIONAL_PROJECTOR_ENABLED', String(kafka.enabled && kafka.features.transactionalProjector)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_PROJECTION_OUTBOX_RELAY_ENABLED', String(kafka.enabled && kafka.features.projectionOutboxRelay)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED', String(kafka.enabled && kafka.features.projectionRealtime)) || codeAgentRuntimeChanged;",
" if (kafka.enabled && kafka.features.transactionalProjector) {",
" if (kafka.enabled) {",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_PROJECTOR_HEARTBEAT_INTERVAL_MS', String(kafka.transactionalProjector.heartbeatIntervalMs)) || codeAgentRuntimeChanged;",
" } else {",
" codeAgentRuntimeChanged = removeEnvValues(container, ['HWLAB_KAFKA_PROJECTOR_HEARTBEAT_INTERVAL_MS']) || codeAgentRuntimeChanged;",
" }",
" if (kafka.enabled && kafka.features.projectionOutboxRelay) {",
" if (kafka.enabled) {",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_OUTBOX_RELAY_INTERVAL_MS', String(kafka.projectionOutboxRelay.intervalMs)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_OUTBOX_RELAY_BATCH_SIZE', String(kafka.projectionOutboxRelay.batchSize)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_KAFKA_OUTBOX_RELAY_LEASE_MS', String(kafka.projectionOutboxRelay.leaseMs)) || codeAgentRuntimeChanged;",
@@ -1980,7 +1976,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
" } else {",
" codeAgentRuntimeChanged = removeEnvValues(container, ['HWLAB_KAFKA_OUTBOX_RELAY_INTERVAL_MS', 'HWLAB_KAFKA_OUTBOX_RELAY_BATCH_SIZE', 'HWLAB_KAFKA_OUTBOX_RELAY_LEASE_MS', 'HWLAB_KAFKA_OUTBOX_RELAY_SEND_TIMEOUT_MS', 'HWLAB_KAFKA_OUTBOX_RELAY_RETRY_BACKOFF_MS']) || codeAgentRuntimeChanged;",
" }",
" if (kafka.enabled && kafka.features.projectionRealtime) {",
" if (kafka.enabled) {",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_WORKBENCH_OUTBOX_TAIL_BATCH_SIZE', String(kafka.projectionRealtime.outboxTailBatchSize)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_WORKBENCH_SSE_HEARTBEAT_MS', String(kafka.projectionRealtime.sseHeartbeatMs)) || codeAgentRuntimeChanged;",
" codeAgentRuntimeChanged = setEnvValue(container, 'HWLAB_WORKBENCH_SSE_DRAIN_TIMEOUT_MS', String(kafka.projectionRealtime.sseDrainTimeoutMs)) || codeAgentRuntimeChanged;",
@@ -2311,12 +2307,6 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
"function cloudWebRuntimeEnvEntries() {",
" const exposure = overlay.publicExposure;",
" const entries = !exposure || !Array.isArray(exposure.extraProxies) ? [] : exposure.extraProxies.filter((proxy) => proxy && proxy.cloudWebEnvName && proxy.publicBaseUrl).map((proxy) => ({ name: String(proxy.cloudWebEnvName), value: String(proxy.publicBaseUrl) }));",
" const kafka = overlay.codeAgentRuntime && overlay.codeAgentRuntime.kafkaEventBridge;",
" if (kafka) {",
" entries.push({ name: 'HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED', value: String(kafka.enabled && kafka.features.liveKafkaSse) });",
" entries.push({ name: 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED', value: String(kafka.enabled && kafka.features.kafkaRefreshReplay) });",
" entries.push({ name: 'HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED', value: String(kafka.enabled && kafka.features.projectionRealtime) });",
" }",
" return entries;",
"}",
"function workloadRef(item, file, container) { return { file, kind: item && item.kind, name: item && item.metadata && item.metadata.name, container: container && container.name }; }",
@@ -2390,31 +2380,14 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
" if (codeAgentRuntime.kafkaEventBridge) {",
" const kafka = codeAgentRuntime.kafkaEventBridge;",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_ENABLED', String(kafka.enabled));",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_AGENTRUN_EVENT_CONSUME_ENABLED', String(kafka.enabled && (kafka.features.directPublish || kafka.features.transactionalProjector)));",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_AGENTRUN_EVENT_CONSUME_ENABLED', String(kafka.enabled));",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_BOOTSTRAP_SERVERS', String(kafka.bootstrapServers));",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_STDIO_TOPIC', String(kafka.stdioTopic));",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_AGENTRUN_EVENT_TOPIC', String(kafka.agentRunEventTopic));",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_EVENT_TOPIC', String(kafka.hwlabEventTopic));",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_CLIENT_ID', String(kafka.clientId));",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_AGENTRUN_EVENT_GROUP_ID', String(kafka.directPublishConsumerGroupId));",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_PROJECTOR_GROUP_ID', String(kafka.transactionalProjectorConsumerGroupId));",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_HWLAB_EVENT_GROUP_ID', String(kafka.hwlabEventConsumerGroupId));",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED', String(kafka.enabled && kafka.features.directPublish));",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED', String(kafka.enabled && kafka.features.liveKafkaSse));",
" const refreshReplayEnabled = kafka.enabled && kafka.features.kafkaRefreshReplay;",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED', String(refreshReplayEnabled));",
" if (refreshReplayEnabled) {",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_GROUP_PREFIX', String(kafka.refreshReplay.groupIdPrefix));",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_TIMEOUT_MS', String(kafka.refreshReplay.timeoutMs));",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_SCAN_LIMIT', String(kafka.refreshReplay.scanLimit));",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_MATCHED_EVENT_LIMIT', String(kafka.refreshReplay.matchedEventLimit));",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_LIVE_BUFFER_LIMIT', String(kafka.refreshReplay.liveBufferLimit));",
" } else {",
" for (const envName of ['HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_GROUP_PREFIX', 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_TIMEOUT_MS', 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_SCAN_LIMIT', 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_MATCHED_EVENT_LIMIT', 'HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_LIVE_BUFFER_LIMIT']) checkCodeAgentRuntimeAbsent(item, file, container, envName);",
" }",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_TRANSACTIONAL_PROJECTOR_ENABLED', String(kafka.enabled && kafka.features.transactionalProjector));",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_KAFKA_PROJECTION_OUTBOX_RELAY_ENABLED', String(kafka.enabled && kafka.features.projectionOutboxRelay));",
" checkCodeAgentRuntimeValue(item, file, container, 'HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED', String(kafka.enabled && kafka.features.projectionRealtime));",
" }",
" }",
" }",
@@ -2448,7 +2421,6 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
"if (workloadCheck.wrongCodeAgentRuntimeEnvs.length > 0) fail('code-agent-runtime-env-mismatch', { refs: workloadCheck.wrongCodeAgentRuntimeEnvs.slice(0, 12), count: workloadCheck.wrongCodeAgentRuntimeEnvs.length });",
"if (overlay.codeAgentRuntime && overlay.codeAgentRuntime.enabled) checks.push('code-agent-runtime-env');",
"if (workloadCheck.wrongCloudWebRuntimeEnvs.length > 0) fail('cloud-web-runtime-env-mismatch', { refs: workloadCheck.wrongCloudWebRuntimeEnvs.slice(0, 12), count: workloadCheck.wrongCloudWebRuntimeEnvs.length });",
"if (cloudWebRuntimeEnvEntries().length > 0) checks.push('cloud-web-runtime-env');",
"const pg = overlay.externalPostgres;",
"if (pg && pg.serviceName) {",
" const access = pg.runtimeAccess || { endpointAddress: pg.endpointAddress, port: pg.port };",
@@ -2570,7 +2542,7 @@ export function nodeRuntimePipelinePostprocessScript(): string[] {
" });",
" result = result.replace(/(node scripts\\/run-bun\\.mjs scripts\\/gitops-render\\.mjs[^\\n]*--use-deploy-images[^\\n]*)/g, (match) => {",
" if (match.includes('--check')) return match;",
" return `${match}\\n${runtimePathOverlayScript()}`;",
" return `${match}\\n${featureConfigSchemaValidationScript()}\\n${runtimePathOverlayScript()}`;",
" });",
" result = result.replace(/node scripts\\/run-bun\\.mjs scripts\\/gitops-render\\.mjs([^\\n]*?)--out \"\\$render_check_dir\"/g, (match) => match.includes('--gitops-root ') ? match : `${match} --gitops-root ${JSON.stringify(overlay.gitopsRoot)} --node ${shellSingle(overlay.nodeId)} --git-read-url ${shellSingle(overlay.gitReadUrl)} --git-write-url ${shellSingle(overlay.gitWriteUrl)} --runtime-endpoint ${shellSingle(overlay.publicApiUrl)} --web-endpoint ${shellSingle(overlay.publicWebUrl)}`);",
" validatePrepareSourceDependencyScript(result);",
@@ -2932,6 +2904,8 @@ function runtimeGitopsPipelineGuardScript(): string[] {
...writeRuntimeGitopsNativeScript("runtime-gitops-observability.mjs", runtimeGitopsObservabilityNativeScript, "UNIDESK_RUNTIME_GITOPS_OBSERVABILITY_MJS"),
...writeRuntimeGitopsNativeScript("runtime-gitops-postprocess.mjs", runtimeGitopsPostprocessNativeScript, "UNIDESK_RUNTIME_GITOPS_POSTPROCESS_MJS"),
...writeRuntimeGitopsNativeScript("runtime-gitops-verify.mjs", runtimeGitopsVerifyNativeScript, "UNIDESK_RUNTIME_GITOPS_VERIFY_MJS"),
...writeRuntimeGitopsNativeScript("feature-config-schema-warning.mjs", featureConfigSchemaWarningNativeScript, "UNIDESK_FEATURE_CONFIG_SCHEMA_WARNING_MJS"),
...writeRuntimeGitopsNativeScript("ajv2020.min.js", ajv2020BundleScript, "UNIDESK_AJV2020_MIN_JS"),
[
"UNIDESK_RUNTIME_GITOPS_OVERLAY_B64=\"$overlay_b64\"",
"node \"$runtime_gitops_guard_dir/runtime-gitops-pipeline-guard.mjs\"",
@@ -18,20 +18,15 @@ test("realtime authority runner contract derives topics, groups, and transaction
hwlab: "hwlab.event.v1",
});
assert.deepEqual(expectedKafka.groups, {
directPublish: "hwlab-v03-agentrun-event-direct-publish",
transactionalProjector: "hwlab-v03-agentrun-event-projector",
liveSse: "hwlab-v03-workbench-live-sse",
});
assert.deepEqual(expectedKafka.capabilities, {
directPublish: false,
liveKafkaSse: false,
kafkaRefreshReplay: false,
transactionalProjector: true,
projectionOutboxRelay: true,
projectionRealtime: true,
});
const kafka = hwlabRuntimeLaneSpecForNode("v03", "NC01").codeAgentRuntime?.kafkaEventBridge;
assert.equal(kafka?.authority, "transactional-projection");
assert.deepEqual(kafka?.transactionalProjector, { heartbeatIntervalMs: 2_000 });
assert.deepEqual(kafka?.projectionOutboxRelay, {
intervalMs: 250,
@@ -23,7 +23,11 @@ export function nodeWebProbeRealtimeFanoutProfiles(spec: HwlabRuntimeLaneSpec):
if (Object.keys(profiles).length === 0) return {};
const kafka = spec.codeAgentRuntime?.kafkaEventBridge;
if (kafka === undefined) throw new Error("realtimeFanoutProfiles requires codeAgentRuntime.kafkaEventBridge in the owning YAML");
const capabilities = Object.fromEntries(Object.entries(kafka.features).map(([name, enabled]) => [name, kafka.enabled && enabled]));
const capabilities = {
transactionalProjector: kafka.enabled,
projectionOutboxRelay: kafka.enabled,
projectionRealtime: kafka.enabled,
};
return Object.fromEntries(Object.entries(profiles).map(([id, profile]) => {
if (profile.businessEvent !== kafka.hwlabEventTopic) {
throw new Error(`realtimeFanoutProfiles.${id}.businessEvent must match codeAgentRuntime.kafkaEventBridge.hwlabEventTopic`);
@@ -38,7 +42,6 @@ export function nodeWebProbeRealtimeFanoutProfiles(spec: HwlabRuntimeLaneSpec):
hwlab: kafka.hwlabEventTopic,
},
groups: {
directPublish: kafka.directPublishConsumerGroupId,
transactionalProjector: kafka.transactionalProjectorConsumerGroupId,
liveSse: kafka.hwlabEventConsumerGroupId,
},
+18 -85
View File
@@ -1548,7 +1548,7 @@ export function nodeRuntimeCodeAgentRuntimeStatus(spec: HwlabRuntimeLaneSpec, na
function nodeRuntimeKafkaAuthorityStatus(spec: HwlabRuntimeLaneSpec, runtime: NonNullable<HwlabRuntimeLaneSpec["codeAgentRuntime"]>): Record<string, unknown> {
const kafka = runtime.kafkaEventBridge;
if (kafka === undefined || kafka.enabled === false) return { required: false, authority: kafka?.authority ?? null, valuesPrinted: false };
if (kafka === undefined || kafka.enabled === false) return { required: false, authority: kafka === undefined ? null : "transactional-projection", valuesPrinted: false };
const healthUrl = `${spec.publicApiUrl.replace(/\/+$/u, "")}/health/ready`;
const result = runCommand(["curl", "-k", "-sS", "--connect-timeout", "5", "--max-time", "12", healthUrl], repoRoot, { timeoutMs: 15_000 });
let health: Record<string, unknown> = {};
@@ -1575,17 +1575,20 @@ function nodeRuntimeKafkaAuthorityStatus(spec: HwlabRuntimeLaneSpec, runtime: No
].filter((value): value is string => value !== null);
return {
required: true,
authority: kafka.authority,
authority: "transactional-projection",
fixedGroups: {
directPublish: kafka.directPublishConsumerGroupId,
transactionalProjector: kafka.transactionalProjectorConsumerGroupId,
liveSse: kafka.hwlabEventConsumerGroupId,
},
capabilities: kafka.features,
capabilities: {
transactionalProjector: true,
projectionOutboxRelay: true,
projectionRealtime: true,
},
liveReplay: {
live: kafka.features.projectionRealtime || kafka.features.liveKafkaSse,
replay: kafka.features.projectionRealtime || kafka.features.kafkaRefreshReplay,
cursor: kafka.features.projectionRealtime ? "projection-outbox-seq" : kafka.features.kafkaRefreshReplay ? "kafka-refresh-replay" : "none",
live: true,
replay: true,
cursor: "projection-outbox-seq",
},
healthObserved: Object.keys(projector).length > 0,
healthStatus: projector.status ?? null,
@@ -1691,41 +1694,15 @@ function nodeRuntimeCodeAgentCloudApiEnvStatus(spec: HwlabRuntimeLaneSpec, runti
const kafkaEventBridge = runtime.kafkaEventBridge;
if (kafkaEventBridge !== undefined) {
expectValue("HWLAB_KAFKA_ENABLED", String(kafkaEventBridge.enabled));
expectValue("HWLAB_KAFKA_AGENTRUN_EVENT_CONSUME_ENABLED", String(kafkaEventBridge.enabled && (kafkaEventBridge.features.directPublish || kafkaEventBridge.features.transactionalProjector)));
expectValue("HWLAB_KAFKA_AGENTRUN_EVENT_CONSUME_ENABLED", String(kafkaEventBridge.enabled));
expectValue("HWLAB_KAFKA_BOOTSTRAP_SERVERS", kafkaEventBridge.bootstrapServers);
expectValue("HWLAB_KAFKA_STDIO_TOPIC", kafkaEventBridge.stdioTopic);
expectValue("HWLAB_KAFKA_AGENTRUN_EVENT_TOPIC", kafkaEventBridge.agentRunEventTopic);
expectValue("HWLAB_KAFKA_EVENT_TOPIC", kafkaEventBridge.hwlabEventTopic);
expectValue("HWLAB_KAFKA_CLIENT_ID", kafkaEventBridge.clientId);
expectValue("HWLAB_KAFKA_AGENTRUN_EVENT_GROUP_ID", kafkaEventBridge.directPublishConsumerGroupId);
expectValue("HWLAB_KAFKA_PROJECTOR_GROUP_ID", kafkaEventBridge.transactionalProjectorConsumerGroupId);
expectValue("HWLAB_KAFKA_HWLAB_EVENT_GROUP_ID", kafkaEventBridge.hwlabEventConsumerGroupId);
expectValue("HWLAB_KAFKA_DIRECT_PUBLISH_ENABLED", String(kafkaEventBridge.enabled && kafkaEventBridge.features.directPublish));
expectValue("HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED", String(kafkaEventBridge.enabled && kafkaEventBridge.features.liveKafkaSse));
const refreshReplayEnabled = kafkaEventBridge.enabled && kafkaEventBridge.features.kafkaRefreshReplay;
expectValue("HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED", String(refreshReplayEnabled));
const refreshReplayEnvNames = [
"HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_GROUP_PREFIX",
"HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_TIMEOUT_MS",
"HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_SCAN_LIMIT",
"HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_MATCHED_EVENT_LIMIT",
"HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_LIVE_BUFFER_LIMIT",
];
if (refreshReplayEnabled) {
const refreshReplay = kafkaEventBridge.refreshReplay;
if (refreshReplay === undefined) throw new Error("codeAgentRuntime.kafkaEventBridge.refreshReplay is required when Kafka refresh replay is enabled");
expectValue("HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_GROUP_PREFIX", refreshReplay.groupIdPrefix);
expectValue("HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_TIMEOUT_MS", String(refreshReplay.timeoutMs));
expectValue("HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_SCAN_LIMIT", String(refreshReplay.scanLimit));
expectValue("HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_MATCHED_EVENT_LIMIT", String(refreshReplay.matchedEventLimit));
expectValue("HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_LIVE_BUFFER_LIMIT", String(refreshReplay.liveBufferLimit));
} else {
for (const name of refreshReplayEnvNames) expectAbsent(name);
}
expectValue("HWLAB_KAFKA_TRANSACTIONAL_PROJECTOR_ENABLED", String(kafkaEventBridge.enabled && kafkaEventBridge.features.transactionalProjector));
expectValue("HWLAB_KAFKA_PROJECTION_OUTBOX_RELAY_ENABLED", String(kafkaEventBridge.enabled && kafkaEventBridge.features.projectionOutboxRelay));
expectValue("HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED", String(kafkaEventBridge.enabled && kafkaEventBridge.features.projectionRealtime));
if (kafkaEventBridge.enabled && kafkaEventBridge.features.transactionalProjector) {
if (kafkaEventBridge.enabled) {
if (kafkaEventBridge.transactionalProjector === undefined) throw new Error("codeAgentRuntime.kafkaEventBridge.transactionalProjector is required when transactional projector is enabled");
expectValue("HWLAB_KAFKA_PROJECTOR_HEARTBEAT_INTERVAL_MS", String(kafkaEventBridge.transactionalProjector.heartbeatIntervalMs));
} else {
@@ -1738,7 +1715,7 @@ function nodeRuntimeCodeAgentCloudApiEnvStatus(spec: HwlabRuntimeLaneSpec, runti
"HWLAB_KAFKA_OUTBOX_RELAY_SEND_TIMEOUT_MS",
"HWLAB_KAFKA_OUTBOX_RELAY_RETRY_BACKOFF_MS",
];
if (kafkaEventBridge.enabled && kafkaEventBridge.features.projectionOutboxRelay) {
if (kafkaEventBridge.enabled) {
const relay = kafkaEventBridge.projectionOutboxRelay;
if (relay === undefined) throw new Error("codeAgentRuntime.kafkaEventBridge.projectionOutboxRelay is required when projection outbox relay is enabled");
expectValue("HWLAB_KAFKA_OUTBOX_RELAY_INTERVAL_MS", String(relay.intervalMs));
@@ -1750,7 +1727,7 @@ function nodeRuntimeCodeAgentCloudApiEnvStatus(spec: HwlabRuntimeLaneSpec, runti
for (const name of relayEnvNames) expectAbsent(name);
}
const projectionRealtimeEnvNames = ["HWLAB_WORKBENCH_OUTBOX_TAIL_BATCH_SIZE", "HWLAB_WORKBENCH_SSE_HEARTBEAT_MS", "HWLAB_WORKBENCH_SSE_DRAIN_TIMEOUT_MS"];
if (kafkaEventBridge.enabled && kafkaEventBridge.features.projectionRealtime) {
if (kafkaEventBridge.enabled) {
const realtime = kafkaEventBridge.projectionRealtime;
if (realtime === undefined) throw new Error("codeAgentRuntime.kafkaEventBridge.projectionRealtime is required when projection realtime is enabled");
expectValue("HWLAB_WORKBENCH_OUTBOX_TAIL_BATCH_SIZE", String(realtime.outboxTailBatchSize));
@@ -1765,7 +1742,7 @@ function nodeRuntimeCodeAgentCloudApiEnvStatus(spec: HwlabRuntimeLaneSpec, runti
deploymentExists: true,
deploymentName: "hwlab-cloud-api",
containerName: "hwlab-cloud-api",
checkedEnvCount: 9 + (kafkaShadowProducer === undefined ? 0 : 5) + (kafkaEventBridge === undefined ? 0 : 30),
checkedEnvCount: 9 + (kafkaShadowProducer === undefined ? 0 : 5) + (kafkaEventBridge === undefined ? 0 : 22),
envCount: envByName.size,
mismatches,
result: compactCodeAgentDeploymentProbeResult(envProbe),
@@ -1774,53 +1751,9 @@ function nodeRuntimeCodeAgentCloudApiEnvStatus(spec: HwlabRuntimeLaneSpec, runti
}
function nodeRuntimeCodeAgentCloudWebEnvStatus(spec: HwlabRuntimeLaneSpec, runtime: NonNullable<HwlabRuntimeLaneSpec["codeAgentRuntime"]>): Record<string, unknown> {
const kafkaEventBridge = runtime.kafkaEventBridge;
if (kafkaEventBridge === undefined) return { required: false, ready: true, checkedEnvCount: 0, valuesPrinted: false };
const envProbe = runNodeK3sArgs(spec, [
"kubectl",
"-n",
spec.runtimeNamespace,
"get",
"deployment",
"hwlab-cloud-web",
"-o",
'jsonpath={range .spec.template.spec.containers[?(@.name=="hwlab-cloud-web")].env[*]}{.name}{"\\t"}{.value}{"\\t"}{.valueFrom.secretKeyRef.name}{"\\t"}{.valueFrom.secretKeyRef.key}{"\\n"}{end}',
], 60);
if (envProbe.exitCode !== 0) {
return {
required: true,
ready: false,
deploymentExists: false,
mismatches: [{ name: "hwlab-cloud-web", reason: "deployment-missing" }],
result: compactCodeAgentDeploymentProbeResult(envProbe),
valuesPrinted: false,
};
}
const envByName = parseCodeAgentEnvProbe(envProbe.stdout);
const mismatches: Array<Record<string, unknown>> = [];
const expectValue = (name: string, expected: string): void => {
const item = envByName.get(name);
if (item === undefined) {
mismatches.push({ name, expected, present: false, kind: "value" });
return;
}
if (item.value !== expected) mismatches.push({ name, expected, value: item.value, present: true, kind: "value" });
};
expectValue("HWLAB_WORKBENCH_LIVE_KAFKA_SSE_ENABLED", String(kafkaEventBridge.enabled && kafkaEventBridge.features.liveKafkaSse));
expectValue("HWLAB_WORKBENCH_KAFKA_REFRESH_REPLAY_ENABLED", String(kafkaEventBridge.enabled && kafkaEventBridge.features.kafkaRefreshReplay));
expectValue("HWLAB_WORKBENCH_PROJECTION_REALTIME_ENABLED", String(kafkaEventBridge.enabled && kafkaEventBridge.features.projectionRealtime));
return {
required: true,
ready: mismatches.length === 0,
deploymentExists: true,
deploymentName: "hwlab-cloud-web",
containerName: "hwlab-cloud-web",
checkedEnvCount: 3,
envCount: envByName.size,
mismatches,
result: compactCodeAgentDeploymentProbeResult(envProbe),
valuesPrinted: false,
};
void spec;
void runtime;
return { required: false, ready: true, checkedEnvCount: 0, valuesPrinted: false };
}
type CodeAgentEnvProbeRow = {
@@ -0,0 +1,71 @@
import { expect, test } from "bun:test";
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import { resolveAgentRunLaneTarget } from "./agentrun-lanes";
import { renderAgentRunPipelineManifest } from "./agentrun-manifests";
import { renderPacFeatureConfigSchemaStage } from "./pac-feature-config-schema-stage";
test("shared stage injects owning YAML OTel values for all three PaC repositories", () => {
for (const [repositoryId, consumer] of [
["hwlab-nc01-v03", "hwlab-nc01-v03"],
["agentrun-nc01-v02", "agentrun-nc01-v02"],
["sentinel-nc01-v03", "sentinel-nc01-v03"],
] as const) {
const stage = renderPacFeatureConfigSchemaStage({
repositoryId,
consumer,
repoDirExpression: "'/workspace/source'",
renderedRootExpression: "'/workspace/rendered'",
});
expect(stage).toContain("feature-config-schema-validation");
expect(stage).toContain("cicd.feature_config.schema");
expect(stage).toContain("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT='http://otel-collector.platform-infra.svc.cluster.local:4318/v1/traces'");
expect(stage).toContain("OTEL_TRACES_SAMPLER_ARG='1'");
expect(stage).toContain("OTEL_EXPORTER_TIMEOUT_MS='300'");
expect(stage).not.toContain("process.env.webProbeSentinel");
}
});
test("embedded node process failure remains a typed warning with zero shell exit", () => {
const root = mkdtempSync(join(tmpdir(), "feature-config-stage-"));
try {
const bin = join(root, "bin");
mkdirSync(bin);
writeFileSync(join(bin, "node"), "#!/bin/sh\nexit 71\n", { mode: 0o755 });
const stage = renderPacFeatureConfigSchemaStage({
repositoryId: "sentinel-nc01-v03",
consumer: "sentinel-nc01-v03",
repoDirExpression: "'/workspace/source'",
renderedRootExpression: "'/workspace/rendered'",
});
const result = spawnSync("sh", [], {
input: `set -eu\n${stage}\n`,
env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}` },
encoding: "utf8",
});
expect(result.status).toBe(0);
expect(result.stderr).toContain("feature-config-validator-process-failure");
} finally {
rmSync(root, { recursive: true, force: true });
}
});
test("AgentRun normal GitOps publish task contains the shared warning-only stage", () => {
const { spec } = resolveAgentRunLaneTarget({ node: "NC01", lane: "nc01-v02" });
const pipeline = renderAgentRunPipelineManifest(spec) as Record<string, any>;
const scripts = pipeline.spec.tasks.flatMap((task: Record<string, any>) => task.taskSpec.steps.map((step: Record<string, any>) => String(step.script ?? ""))).join("\n");
expect(scripts).toContain("UNIDESK_PAC_CONSUMER='agentrun-nc01-v02'");
expect(scripts).toContain("feature-config-schema-validation");
expect(scripts).toContain("feature-config-validator-process-failure");
expect(scripts).toContain("OTEL_EXPORTER_TIMEOUT_MS='300'");
});
test("repo-owned schema is Draft 2020-12 with one canonical feature property", () => {
const schema = JSON.parse(readFileSync("config/feature-config.schema.json", "utf8"));
expect(schema.$schema).toBe("https://json-schema.org/draft/2020-12/schema");
expect(schema.properties.webProbeSentinel["x-unidesk-feature"]).toBe("web-probe-sentinel");
expect(Object.keys(schema.properties)).toEqual(["webProbeSentinel"]);
});
@@ -0,0 +1,51 @@
import { readFileSync } from "node:fs";
import { rootPath } from "./config";
import { loadCicdOtelRuntimeConfig } from "../native/cicd/otel-runtime-config";
const validatorSource = readFileSync(rootPath("scripts/native/cicd/feature-config-schema-warning.mjs"), "utf8").trimEnd();
export interface PacFeatureConfigSchemaStageOptions {
readonly repositoryId: string;
readonly consumer: string;
readonly repoDirExpression: string;
readonly renderedRootExpression: string;
readonly ajvBundleExpression?: string | null;
}
export function renderPacFeatureConfigSchemaStage(options: PacFeatureConfigSchemaStageOptions): string {
const otel = loadCicdOtelRuntimeConfig(options.repositoryId);
return [
"feature_config_schema_started_ms=\"$(date +%s%3N 2>/dev/null || date +%s000)\"",
`export UNIDESK_FEATURE_CONFIG_REPO_DIR=${options.repoDirExpression}`,
`export UNIDESK_FEATURE_CONFIG_RENDERED_ROOT=${options.renderedRootExpression}`,
`export UNIDESK_PAC_CONSUMER=${shellSingle(options.consumer)}`,
`export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=${shellSingle(otel.endpoint)}`,
`export OTEL_SERVICE_NAME=${shellSingle(otel.serviceName)}`,
`export OTEL_TRACES_SAMPLER_ARG=${shellSingle(String(otel.samplingRatio))}`,
`export OTEL_EXPORTER_TIMEOUT_MS=${shellSingle(String(otel.timeoutMs))}`,
...(options.ajvBundleExpression === undefined || options.ajvBundleExpression === null
? []
: [`export UNIDESK_AJV2020_BUNDLE=${options.ajvBundleExpression}`]),
"if ! node --input-type=module <<'NODE_UNIDESK_FEATURE_CONFIG_SCHEMA'",
validatorSource,
"await runFeatureConfigSchemaValidation({",
" repoDir: process.env.UNIDESK_FEATURE_CONFIG_REPO_DIR,",
" renderedRoot: process.env.UNIDESK_FEATURE_CONFIG_RENDERED_ROOT,",
" consumer: process.env.UNIDESK_PAC_CONSUMER,",
"});",
"NODE_UNIDESK_FEATURE_CONFIG_SCHEMA",
"then",
" printf '{\"event\":\"feature-config-schema-validation\",\"warning\":true,\"blocking\":false,\"code\":\"feature-config-validator-process-failure\",\"mutation\":false,\"valuesPrinted\":false}\\n' >&2",
"fi",
"if command -v ci_timing_emit >/dev/null 2>&1; then",
" ci_timing_emit feature-config-schema-validation succeeded \"$feature_config_schema_started_ms\"",
"else",
" printf '{\"event\":\"ci.stage.timing\",\"stage\":\"feature-config-schema-validation\",\"status\":\"succeeded\",\"blocking\":false}\\n' >&2",
"fi",
].join("\n");
}
function shellSingle(value: string): string {
return `'${value.replaceAll("'", `'"'"'`)}'`;
}
@@ -11,6 +11,9 @@ const require = createRequire(import.meta.url);
const { parsePacLogRecords } = require("../native/cicd/pac-status-evaluator.cjs") as {
parsePacLogRecords(value: string): Array<Record<string, unknown>>;
};
const { extractPacSourceObservation } = require("../native/cicd/pac-status-evaluator.cjs") as {
extractPacSourceObservation(records: Array<Record<string, unknown>>, sourceCommit: string): Record<string, unknown>;
};
describe("PaC 失败证据合同", () => {
test("真实 sentinel TaskRun 日志保留 typed 首断点", () => {
@@ -52,6 +55,39 @@ describe("PaC 失败证据合同", () => {
expect(compactStatus).toMatchObject({ artifact: { traceId: row.traceId, firstBreak: row.firstBreak, error: row.error } });
});
test("feature-config warning 在 status、history、debug-step 同构有界投影", () => {
const observation = extractPacSourceObservation([
{
event: "pac-delivery-plan",
sourceCommitId: "a".repeat(40),
affectedServices: [],
rolloutServices: [],
buildServices: [],
reusedServices: [],
},
{
event: "feature-config-schema-validation",
warning: true,
blocking: false,
code: "feature-config-input-missing",
schemaPath: "config/feature-config.schema.json",
errorCount: 1,
firstError: "rendered workload candidate snapshot is missing",
mutation: false,
valuesPrinted: false,
},
]);
const expected = "feature-config-schema: warning=true blocking=false code=feature-config-input-missing path=config/feature-config.schema.json errors=1 first=rendered workload candidate snapshot is missing";
const history = renderHistory({ rows: [{ id: "run", sourceObservation: observation }], detailId: "run", config: {}, historyErrors: [], next: {} }).renderedText;
const debug = renderDebugStep({ realRun: { found: true, pipelineRun: {}, artifact: { sourceObservation: observation } }, checks: [], target: {}, next: {} }).renderedText;
const status = renderStatus({ summary: { latestPipelineRun: {}, artifact: {}, diagnostics: {}, sourceObservation: observation }, consumer: {}, deliveryAuthority: {}, config: {}, coverage: [], target: {}, next: {} }).renderedText;
expect(status).toContain(expected);
expect(debug).toContain(expected);
expect(history).toContain("featureConfigCode");
expect(history).toContain("feature-config-input-missing");
expect(JSON.stringify(observation)).not.toContain("process.env");
});
test("collector 遍历全部 matching TaskRunterminal record 仍限定合同任务", () => {
const remote = readFileSync(resolve(import.meta.dir, "platform-infra-pipelines-as-code-remote.sh"), "utf8");
expect(remote).toContain("for (const item of matching)");
@@ -0,0 +1,35 @@
import { expect, test } from "bun:test";
import { featureConfigSchemaHistoryFields, renderFeatureConfigSchemaLine } from "./platform-infra-pac-feature-config-projection";
test("status and debug share one bounded feature config line", () => {
const observation = {
warning: true,
blocking: false,
code: "feature-config-value-mismatch",
schemaPath: "config/feature-config.schema.json",
errorCount: 2,
firstError: " /webProbeSentinel required must have required property ",
};
const line = renderFeatureConfigSchemaLine(observation);
expect(line).toBe(" feature-config-schema: warning=true blocking=false code=feature-config-value-mismatch path=config/feature-config.schema.json errors=2 first=/webProbeSentinel required must have required property");
expect(line.length).toBeLessThan(300);
});
test("history uses the same bounded observation fields", () => {
expect(featureConfigSchemaHistoryFields({
warning: true,
blocking: false,
code: "feature-config-schema-missing",
schemaPath: "config/feature-config.schema.json",
errorCount: 1,
firstError: "schema file is missing",
})).toEqual([
["featureConfigWarning", "true"],
["featureConfigBlocking", "false"],
["featureConfigCode", "feature-config-schema-missing"],
["featureConfigSchemaPath", "config/feature-config.schema.json"],
["featureConfigErrorCount", "1"],
["featureConfigFirstError", "schema file is missing"],
]);
});
@@ -0,0 +1,35 @@
export function renderFeatureConfigSchemaLine(value: unknown): string {
const featureConfigSchema = record(value);
return ` feature-config-schema: warning=${boolText(featureConfigSchema.warning)} blocking=${boolText(featureConfigSchema.blocking)} code=${stringValue(featureConfigSchema.code)} path=${stringValue(featureConfigSchema.schemaPath)} errors=${stringValue(featureConfigSchema.errorCount)} first=${compactLine(stringValue(featureConfigSchema.firstError))}`;
}
export function featureConfigSchemaHistoryFields(value: unknown): string[][] {
const featureConfigSchema = record(value);
return [
["featureConfigWarning", stringValue(featureConfigSchema.warning)],
["featureConfigBlocking", stringValue(featureConfigSchema.blocking)],
["featureConfigCode", stringValue(featureConfigSchema.code)],
["featureConfigSchemaPath", stringValue(featureConfigSchema.schemaPath)],
["featureConfigErrorCount", stringValue(featureConfigSchema.errorCount)],
["featureConfigFirstError", compactLine(stringValue(featureConfigSchema.firstError))],
];
}
function record(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function stringValue(value: unknown, fallback = "-"): string {
if (typeof value === "string" && value.length > 0) return value;
if (typeof value === "number" || typeof value === "boolean") return String(value);
return fallback;
}
function boolText(value: unknown): string {
return value === true ? "true" : "false";
}
function compactLine(value: string): string {
const trimmed = value.replace(/\s+/gu, " ").trim();
return trimmed.length > 0 ? trimmed.slice(0, 120) : "-";
}
@@ -7,7 +7,7 @@ import { resolve } from "node:path";
import { rootPath } from "./config";
import { applyNodeRuntimeDeployYamlOverlay, nodeRuntimeDeployYamlOverlayShellScript } from "./hwlab-node/deploy-overlay";
import { hwlabRuntimePipelineProvenance } from "./hwlab-node/pipeline-provenance";
import { nodeRuntimePipelinePostprocessScript, nodeRuntimePipelineProvenancePostprocessScript } from "./hwlab-node/render";
import { nodeRuntimeFeatureConfigSchemaStage, nodeRuntimePipelinePostprocessScript, nodeRuntimePipelineProvenancePostprocessScript } from "./hwlab-node/render";
import { nodeRuntimeRenderOverlay } from "./hwlab-node/web-probe";
import { hwlabRuntimeLaneSpecForNode, parseHwlabRuntimePipelineProvenance } from "./hwlab-node-lanes";
import { pipelineProvenanceAnnotationKeys, pipelineProvenanceAnnotations } from "./pipeline-provenance";
@@ -307,7 +307,7 @@ describe("HWLAB YAML-owned Pipeline provenance", () => {
"set -eu",
`render_dir=${JSON.stringify(temporary)}`,
`overlay_b64=${JSON.stringify(overlayBase64)}`,
...nodeRuntimePipelinePostprocessScript(),
...nodeRuntimePipelinePostprocessScript(nodeRuntimeFeatureConfigSchemaStage(spec)),
].join("\n");
const result = spawnSync("sh", [], {
cwd: rootPath(),
@@ -336,14 +336,26 @@ describe("HWLAB YAML-owned Pipeline provenance", () => {
expect(scripts.join("\n")).toContain("no-build-no-rollout-plan-gitops-verify");
expect(scripts.join("\n")).not.toContain('"status":"skipped","reason":"no-build-no-rollout-plan"');
expect(scripts.join("\n")).toContain("UNIDESK_RUNTIME_GITOPS_OVERLAY_FILE=/etc/unidesk-cicd-runtime-gitops/runtime-gitops-overlay.json");
expect(scripts.join("\n")).toContain("export UNIDESK_AJV2020_BUNDLE='/etc/unidesk-cicd-runtime-gitops/ajv2020.min.js'");
expect(scripts.join("\n")).toContain("feature-config-schema-validation");
expect(scripts.join("\n")).toContain("cicd.feature_config.schema");
expect(scripts.join("\n")).toContain("export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT='http://otel-collector.platform-infra.svc.cluster.local:4318/v1/traces'");
expect(scripts.join("\n")).toContain("export OTEL_SERVICE_NAME='unidesk-cicd'");
expect(scripts.join("\n")).toContain("export OTEL_TRACES_SAMPLER_ARG='1'");
expect(scripts.join("\n")).toContain("export OTEL_EXPORTER_TIMEOUT_MS='300'");
expect(scripts.join("\n")).toContain("feature-config-validator-process-failure");
expect(scripts.join("\n").indexOf("scripts/gitops-render.mjs --use-deploy-images"))
.toBeLessThan(scripts.join("\n").indexOf("feature-config-schema-validation"));
expect(scripts.join("\n")).not.toContain("NODE_UNIDESK_RUNTIME_GITOPS_POSTPROCESS");
expect(scripts.join("\n")).not.toContain("NODE_UNIDESK_RUNTIME_GITOPS_VERIFY");
expect(scripts.join("\n")).not.toContain("UNIDESK_RUNTIME_GITOPS_OVERLAY_B64=");
expect(scripts.join("\n")).not.toContain(Buffer.from(JSON.stringify(overlay.observability), "utf8").toString("base64"));
const configMap = Bun.YAML.parse(readFileSync(configMapPath, "utf8")) as Record<string, any>;
expect(configMap.metadata.labels["app.kubernetes.io/managed-by"]).toBe("unidesk-host-gitops");
expect(JSON.parse(configMap.data["runtime-gitops-overlay.json"])).toEqual({ runtimePath: overlay.runtimePath, observability: overlay.observability });
expect(JSON.parse(configMap.data["runtime-gitops-overlay.json"])).toEqual({ runtimePath: overlay.runtimePath, observability: overlay.observability, codeAgentRuntime: overlay.codeAgentRuntime });
expect(Object.keys(configMap.data).sort()).toEqual([
"ajv2020.min.js",
"feature-config-schema-warning.mjs",
"runtime-gitops-observability.mjs",
"runtime-gitops-overlay.json",
"runtime-gitops-postprocess.mjs",
@@ -352,13 +364,18 @@ describe("HWLAB YAML-owned Pipeline provenance", () => {
const hostConfig = Bun.YAML.parse(readFileSync(rootPath("config", "unidesk-host-k8s.yaml"), "utf8")) as Record<string, any>;
const runtimePaths = new Set(hostConfig.delivery.changeDetection.runtimePaths as string[]);
for (const inputPath of [
"bun.lock",
"config/hwlab-node-lanes.yaml",
"package.json",
"scripts/native/cicd/feature-config-schema-warning.mjs",
"scripts/native/hwlab/runtime-gitops-observability.mjs",
"scripts/native/hwlab/runtime-gitops-postprocess.mjs",
"scripts/native/hwlab/runtime-gitops-scripts-configmap.mjs",
"scripts/native/hwlab/runtime-gitops-verify.mjs",
"scripts/src/config.ts",
"scripts/src/hwlab-node/render.ts",
"scripts/src/hwlab-node-lanes.ts",
"scripts/src/platform-infra-pipelines-as-code.ts",
"scripts/src/yaml-composition.ts",
]) expect(runtimePaths.has(inputPath), inputPath).toBe(true);
expect(hostConfig.delivery.gitops.resources).toContainEqual({
@@ -658,14 +675,13 @@ describe("runtime source-commit selection", () => {
mode: "remote-pipeline-annotation",
maxInlineScriptBytes: 32768,
});
expect([
spec.codeAgentRuntime?.kafkaEventBridge.features.directPublish,
spec.codeAgentRuntime?.kafkaEventBridge.features.liveKafkaSse,
spec.codeAgentRuntime?.kafkaEventBridge.features.kafkaRefreshReplay,
spec.codeAgentRuntime?.kafkaEventBridge.features.transactionalProjector,
spec.codeAgentRuntime?.kafkaEventBridge.features.projectionOutboxRelay,
spec.codeAgentRuntime?.kafkaEventBridge.features.projectionRealtime,
]).toEqual([false, false, false, true, true, true]);
expect(spec.codeAgentRuntime?.kafkaEventBridge).toMatchObject({
enabled: true,
transactionalProjectorConsumerGroupId: "hwlab-v03-agentrun-event-projector",
hwlabEventConsumerGroupId: "hwlab-v03-workbench-live-sse",
});
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");
@@ -9,7 +9,7 @@ import { AGENTRUN_CONFIG_PATH, resolveAgentRunLaneTarget } from "./agentrun-lane
import { renderAgentRunPipelineManifest } from "./agentrun-manifests";
import { hwlabRuntimeLaneSpecForNode, isHwlabRuntimeLane, type HwlabRuntimeLaneSpec } from "./hwlab-node-lanes";
import { nodeRuntimeGitopsRoot } from "./hwlab-node/cleanup";
import { nodeRuntimePipelinePostprocessScript } from "./hwlab-node/render";
import { nodeRuntimeFeatureConfigSchemaStage, nodeRuntimePipelinePostprocessScript } from "./hwlab-node/render";
import { nodeRuntimeRenderOverlay } from "./hwlab-node/web-probe";
import { hwlabRuntimePipelineProvenance } from "./hwlab-node/pipeline-provenance";
import { applyNodeRuntimeDeployYamlOverlay } from "./hwlab-node/deploy-overlay";
@@ -617,7 +617,7 @@ function renderHwlabPipelineFromOwningYaml(spec: HwlabRuntimeLaneSpec, sourceWor
"set -eu",
`render_dir=${shellQuote(output)}`,
`overlay_b64=${shellQuote(overlay)}`,
...nodeRuntimePipelinePostprocessScript(),
...nodeRuntimePipelinePostprocessScript(nodeRuntimeFeatureConfigSchemaStage(spec)),
].join("\n");
runChecked("sh", [], temporarySource, 120_000, "HWLAB UniDesk domain postprocess/verify renderer", postprocess);
const path = resolve(output, spec.tektonDir, "pipeline.yaml");
@@ -31,6 +31,7 @@ import { pacConsumerRbacDesiredIdentity, renderPacConsumerRbacDesiredFragment }
import { runPlatformInfraGiteaCommand } from "./platform-infra-gitea";
import { readGiteaConfig } from "./platform-infra-gitea-config";
import { resolvePacBootstrapMirrorRepository } from "./platform-infra-pipelines-as-code-bootstrap";
import { featureConfigSchemaHistoryFields, renderFeatureConfigSchemaLine } from "./platform-infra-pac-feature-config-projection";
const configFile = rootPath("config", "platform-infra", "pipelines-as-code.yaml");
const configLabel = "config/platform-infra/pipelines-as-code.yaml";
@@ -2307,6 +2308,7 @@ export function renderStatus(result: Record<string, unknown>): RenderedCliResult
stringValue(deliveryPlan.reusedServiceCount),
stringValue(sourceObservation.reason),
]]),
renderFeatureConfigSchemaLine(sourceObservation.featureConfigSchema),
"",
"TASKRUN DURATIONS",
...(taskRuns.length === 0 ? ["-"] : table(["TASKRUN", "STATUS", "REASON", "FAILED_STEP", "EXIT", "DURATION_S"], taskRuns.map((item) => {
@@ -2524,6 +2526,7 @@ export function renderDebugStep(result: Record<string, unknown>): RenderedCliRes
stringValue(catalog.digestCount),
short(stringValue(artifact.gitopsCommit), 16),
]]),
renderFeatureConfigSchemaLine(record(artifact.sourceObservation).featureConfigSchema),
]),
"",
"STATUS EVALUATOR FIXTURES",
@@ -2946,6 +2949,7 @@ function renderHistoryDetail(row: Record<string, unknown>): string[] {
["rolloutServices", stringValue(deliveryPlan.rolloutServiceCount)],
["buildServices", stringValue(deliveryPlan.buildServiceCount)],
["reusedServices", stringValue(deliveryPlan.reusedServiceCount)],
...featureConfigSchemaHistoryFields(sourceObservation.featureConfigSchema),
]),
"",
"TERMINAL EVIDENCE",