import { readFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { buildMetadataFromEnv, imageTagFromReference, normalizeIsoTimestamp } from "../build-metadata.mjs"; import { ENVIRONMENT_DEV, SERVICE_IDS } from "../protocol/index.mjs"; import { CLOUD_API_SERVICE_ID } from "../audit/index.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const LIVE_BUILD_TIMEOUT_MS = 900; const LIVE_BUILD_METADATA_PATHS = Object.freeze({ deployManifest: "deploy/deploy.json", artifactCatalog: "deploy/artifact-catalog.dev.json" }); const LIVE_BUILD_SERVICE_DEFAULTS = Object.freeze({ "hwlab-cloud-api": Object.freeze({ urlEnv: null, defaultUrl: "http://127.0.0.1:6667" }), "hwlab-cloud-web": Object.freeze({ urlEnv: "HWLAB_CLOUD_WEB_SERVICE_URL", defaultUrl: "http://hwlab-cloud-web.hwlab-dev.svc.cluster.local:8080" }), "hwlab-agent-mgr": Object.freeze({ urlEnv: "HWLAB_AGENT_MGR_URL", defaultUrl: "http://hwlab-agent-mgr.hwlab-dev.svc.cluster.local:7410" }), "hwlab-agent-worker": Object.freeze({ urlEnv: "HWLAB_AGENT_WORKER_URL", defaultUrl: "http://hwlab-agent-worker.hwlab-dev.svc.cluster.local:7411" }), "hwlab-device-pod": Object.freeze({ urlEnv: "HWLAB_DEVICE_POD_URL", defaultUrl: "http://hwlab-device-pod.hwlab-dev.svc.cluster.local:7601" }), "hwlab-gateway": Object.freeze({ urlEnv: "HWLAB_GATEWAY_URL", defaultUrl: "http://hwlab-gateway.hwlab-dev.svc.cluster.local:7001" }), "hwlab-edge-proxy": Object.freeze({ urlEnv: "HWLAB_EDGE_PROXY_URL", defaultUrl: "http://hwlab-edge-proxy.hwlab-dev.svc.cluster.local:6667", healthPath: "/health" }), "hwlab-agent-skills": Object.freeze({ urlEnv: "HWLAB_AGENT_SKILLS_URL", defaultUrl: "http://hwlab-agent-skills.hwlab-dev.svc.cluster.local:7430" }) }); const LIVE_BUILD_EXTERNAL_COMPONENTS = Object.freeze([ Object.freeze({ serviceId: "hwlab-frpc", serviceName: "hwlab-frpc", kind: "external", externalImage: true, defaultImage: "127.0.0.1:5000/hwlab/frpc:v0.68.1", externalReason: "外部镜像或非 HWLAB 构建产物" }) ]); const V02_REMOVED_SERVICE_IDS = Object.freeze([ "hwlab-gateway-simu", "hwlab-box-simu", "hwlab-patch-panel", "hwlab-router", "hwlab-tunnel-client" ]); function defaultRuntimeEnvironment(env = process.env) { const value = env.HWLAB_GITOPS_PROFILE || env.HWLAB_ENVIRONMENT || ENVIRONMENT_DEV; return typeof value === "string" && value.trim() ? value.trim() : ENVIRONMENT_DEV; } export async function buildLiveBuildsPayload(options = {}, dependencies = {}) { const env = options.env ?? process.env; const metadata = await loadLiveBuildMetadata(options); const inventory = liveBuildServiceInventory(metadata, env); const services = await Promise.all( inventory.map((service) => observeLiveBuildService(service, { ...options, env, buildHealthPayload: dependencies.buildHealthPayload })) ); const hwlabServices = services.filter((service) => service.kind === "hwlab"); const latest = [...hwlabServices] .filter((service) => service.build.createdAt) .sort((left, right) => Date.parse(right.build.createdAt) - Date.parse(left.build.createdAt))[0] ?? null; return { serviceId: CLOUD_API_SERVICE_ID, environment: (dependencies.runtimeEnvironment ?? defaultRuntimeEnvironment)(env), status: "ok", contractVersion: "live-builds-v1", observedAt: new Date().toISOString(), source: { kind: "live-health+deploy-artifact-catalog", route: "/v1/live-builds", healthPath: "/health/live", metadataPaths: LIVE_BUILD_METADATA_PATHS, desiredStateSource: metadata.deployManifest.status === "ok" ? LIVE_BUILD_METADATA_PATHS.deployManifest : "unavailable", artifactCatalogSource: metadata.artifactCatalog.status === "ok" ? LIVE_BUILD_METADATA_PATHS.artifactCatalog : "unavailable", note: "Each row prefers current live service health build metadata and falls back only to deploy/artifact catalog metadata when the live tag/revision matches; observedAt is not used as build time." }, latest, counts: { total: services.length, hwlab: hwlabServices.length, withBuildTime: hwlabServices.filter((service) => Boolean(service.build.createdAt)).length, unavailable: hwlabServices.filter((service) => service.build.createdAt === null).length, external: services.filter((service) => service.kind === "external").length }, services }; } export async function buildDevicePodCloudApiPayload(url, options = {}) { const upstreamPayload = await fetchDevicePodUpstreamPayload(url, options); if (upstreamPayload) { return { ...upstreamPayload, cloudApiProxy: { route: url.pathname, upstream: configuredDevicePodUrl(options.env ?? process.env), status: "proxied" } }; } return { serviceId: CLOUD_API_SERVICE_ID, contractVersion: "device-pod-authority-v1", status: "blocked", ready: false, source: { kind: "CLOUD_API_PROFILE_AUTHORITY", serviceId: CLOUD_API_SERVICE_ID, fake: false, devLiveEvidence: false, note: "Device Pod fallback is blocked instead of synthesized from fake data." }, blocker: { code: "device_pod_authority_unavailable", layer: "cloud-api", retryable: true, summary: "Device Pod authority requires access-control profile and grant data from hwlab-cloud-api.", userMessage: "Device Pod 权限和 profile 必须来自 cloud-api;不会回退到 fake 数据。" }, cloudApiProxy: { route: url.pathname, upstream: configuredDevicePodUrl(options.env ?? process.env), status: "blocked", reason: "fake fallback is disabled" } }; } async function fetchDevicePodUpstreamPayload(url, options = {}) { const baseUrl = configuredDevicePodUrl(options.env ?? process.env); if (!baseUrl) return null; const target = `${baseUrl}${url.pathname}${url.search}`; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 900); try { const response = await (options.fetchImpl ?? fetch)(target, { headers: { Accept: "application/json" }, signal: controller.signal }); if (!response.ok) return null; return await response.json(); } catch { return null; } finally { clearTimeout(timeout); } } function configuredDevicePodUrl(env = process.env) { const value = String(env.HWLAB_DEVICE_POD_URL ?? "").trim(); return value ? value.replace(/\/+$/u, "") : ""; } async function observeLiveBuildService(service, options) { if (service.externalImage) { return externalLiveBuildRecord(service, { healthUrl: null, httpStatus: null, imageReference: service.defaultImage ?? "unknown", reason: "外部镜像或非 HWLAB 构建产物" }); } const metadataRecords = liveBuildRecordsFromMetadata(service); const metadataRecord = metadataRecords[0] ?? null; if (service.serviceId === CLOUD_API_SERVICE_ID) { const health = await options.buildHealthPayload(options); return liveBuildRecordFromHealth(service, { ok: true, status: 200, url: "/health/live", payload: health }, metadataRecords); } const baseUrl = String(options.env?.[service.urlEnv] || service.defaultUrl || "").replace(/\/+$/u, ""); if (!baseUrl) { return metadataRecord ? metadataRecordWithHealthGap(metadataRecord, { service, healthUrl: null, httpStatus: null, reason: `${service.serviceId} 没有配置 live health URL` }) : unavailableLiveBuildRecord(service, { reasonCode: "service_url_unavailable", reason: `${service.serviceId} 没有配置 live health URL` }); } const healthPath = service.healthPath ?? "/health/live"; return liveBuildRecordFromHealth( service, await fetchServiceHealth(`${baseUrl}${healthPath}`, options.fetchImpl ?? fetch), metadataRecords ); } async function fetchServiceHealth(url, fetchImpl) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), LIVE_BUILD_TIMEOUT_MS); try { const response = await fetchImpl(url, { headers: { Accept: "application/json" }, signal: controller.signal }); const text = await response.text(); let payload = null; try { payload = text ? JSON.parse(text) : null; } catch { return { ok: false, status: response.status, url, reasonCode: "invalid_json", reason: `${url} returned non-JSON health payload` }; } return { ok: response.ok, status: response.status, url, payload, reasonCode: response.ok ? null : "http_error", reason: response.ok ? null : `${url} returned HTTP ${response.status}` }; } catch (error) { const timedOut = error?.name === "AbortError"; return { ok: false, status: null, url, reasonCode: timedOut ? "timeout" : "request_failed", reason: timedOut ? `${url} timed out after ${LIVE_BUILD_TIMEOUT_MS}ms` : error.message }; } finally { clearTimeout(timeout); } } function liveBuildRecordFromHealth(service, result, metadataRecords = []) { const candidates = Array.isArray(metadataRecords) ? metadataRecords : [metadataRecords].filter(Boolean); const metadataRecord = candidates[0] ?? null; if (!result.ok) { const reason = healthUnavailableReason(service, result.reason ?? `${service.serviceId} health unavailable`); return metadataRecord ? metadataRecordWithHealthGap(metadataRecord, { service, healthUrl: result.url, httpStatus: result.status, reason }) : unavailableLiveBuildRecord(service, { healthUrl: result.url, httpStatus: result.status, reasonCode: result.reasonCode ?? "health_unavailable", reason }); } const payload = result.payload ?? {}; const payloadServiceId = payload.serviceId ?? service.serviceId; if (!String(payloadServiceId).startsWith("hwlab-")) { return externalLiveBuildRecord(service, { healthUrl: result.url, httpStatus: result.status, imageReference: payload.image?.reference ?? payload.image ?? "unknown", imageTag: payload.image?.tag ?? null, imageDigest: payload.image?.digest ?? "unknown", commitId: payload.commit?.id ?? payload.revision ?? payload.version ?? "unknown", commitSource: payload.commit?.source ?? "external-health", revision: payload.revision ?? payload.commit?.id ?? payload.version ?? "unknown", reason: "外部镜像或非 HWLAB 构建产物" }); } const imageReference = payload.image?.reference ?? payload.image ?? "unknown"; const commitId = payload.commit?.id ?? payload.revision ?? "unknown"; const liveIdentity = liveBuildIdentityFromHealth({ payload, imageReference, commitId }); const createdAt = normalizeHealthBuildTime(payload); if (createdAt) { return liveBuildHealthPayloadRecord(service, result, { payload, imageReference, commitId, createdAt, metadataSource: payload.build?.metadataSource ?? "health:build.createdAt", unavailableReason: null }); } const matched = selectMatchingMetadataRecord(liveIdentity, candidates); if (matched.record?.build?.createdAt) { return mergeLiveHealthOntoMetadataRecord(matched.record, { match: matched.match, liveIdentity, service, result, payload, imageReference, commitId }); } const mismatch = matched.match ?? liveBuildMetadataMatch(liveIdentity, metadataRecord); const unavailableReason = matched.record ? "构建时间不可用:当前 live 镜像未提供 buildCreatedAt" : metadataRecord ? `构建时间不可用:live tag 与 ${controlledMetadataLabel(metadataRecord)} 不匹配` : "构建时间不可用:当前 live 镜像未提供 buildCreatedAt"; return liveBuildHealthPayloadRecord(service, result, { payload, imageReference, commitId, createdAt: null, metadataSource: matched.record ? "live-health:matched-controlled-metadata-without-build-time" : metadataRecord ? "live-health:controlled-metadata-mismatch" : "live-health:build-created-at-missing", unavailableReason, metadataMismatch: mismatch, unavailableDetail: matched.record ? `live tag ${liveIdentity.imageTag} / revision ${liveIdentity.revision} 已匹配 ${controlledMetadataLabel(matched.record)},但该 metadata 没有 buildCreatedAt(metadata tag ${matched.record.image.tag},metadata revision ${matched.record.revision},来源 ${matched.record.build.metadataSource})` : metadataRecord ? `未使用可能过期的构建时间:live tag ${liveIdentity.imageTag},live revision ${liveIdentity.revision},metadata tag ${metadataRecord.image.tag},metadata revision ${metadataRecord.revision},来源 ${metadataRecord.build.metadataSource}` : "health payload 缺少 build.createdAt / image.createdAt / buildCreatedAt,deploy/artifact catalog 也没有可匹配的 buildCreatedAt" }); } function liveBuildHealthPayloadRecord(service, result, { payload, imageReference, commitId, createdAt, metadataSource, unavailableReason, metadataMismatch = null, unavailableDetail = null }) { return { serviceId: service.serviceId, name: service.serviceName, kind: "hwlab", status: createdAt ? "ok" : "build_time_unavailable", healthUrl: result.url, httpStatus: result.status, desiredState: desiredStateSummary(service), build: { createdAt, source: payload.build?.source ?? payload.commit?.source ?? "health-payload", metadataSource, unavailableReason, unavailableDetail, metadataMismatch }, image: { reference: imageReference, tag: payload.image?.tag ?? imageTagFromReference(imageReference) ?? "unknown", digest: payload.image?.digest ?? "unknown" }, commit: { id: commitId, source: payload.commit?.source ?? "health-payload" }, runtime: liveBuildRuntimeSummary(service, payload), revision: payload.revision ?? commitId }; } function metadataRecordWithHealthGap(metadataRecord, { service, healthUrl, httpStatus, reason }) { const metadataTag = metadataRecord.image?.tag ?? "unknown"; const metadataRevision = metadataRecord.revision ?? metadataRecord.commit?.id ?? "unknown"; return { ...metadataRecord, healthUrl, httpStatus, status: "build_time_unavailable", build: { ...metadataRecord.build, createdAt: null, metadataSource: "live-health-unavailable", unavailableReason: `构建时间不可用:当前 live health 不可用,无法确认 ${service.serviceId} live tag/revision 是否匹配 deploy/artifact catalog metadata,未使用可能过期的构建时间(metadata tag ${metadataTag},metadata revision ${metadataRevision});${reason}` } }; } function mergeLiveHealthOntoMetadataRecord(metadataRecord, { match, liveIdentity, service, result, payload, imageReference, commitId }) { return { ...metadataRecord, serviceId: service.serviceId, name: service.serviceName, kind: "hwlab", status: "ok", healthUrl: result.url, httpStatus: result.status, build: { ...metadataRecord.build, metadataSource: metadataRecord.build.metadataSource, unavailableReason: null, liveMetadataMatch: match, liveHealthMissingReason: `health payload 缺少 build.createdAt / image.createdAt / buildCreatedAt,已使用与 live tag/revision 匹配的 ${controlledMetadataLabel(metadataRecord)}(live tag ${liveIdentity.imageTag})` }, image: { reference: imageReference !== "unknown" ? imageReference : metadataRecord.image.reference, tag: payload.image?.tag ?? imageTagFromReference(imageReference) ?? metadataRecord.image.tag, digest: payload.image?.digest ?? metadataRecord.image.digest }, commit: { id: commitId !== "unknown" ? commitId : metadataRecord.commit.id, source: payload.commit?.source ?? metadataRecord.commit.source }, runtime: liveBuildRuntimeSummary(service, payload), revision: payload.revision ?? (commitId !== "unknown" ? commitId : metadataRecord.revision) }; } function liveBuildRuntimeSummary(service, payload = {}) { const runtimeSource = payload?.runtimeIdentity ?? payload?.runtime; const runtime = runtimeSource && typeof runtimeSource === "object" ? runtimeSource : {}; const pod = runtime?.pod && typeof runtime.pod === "object" ? runtime.pod : {}; const podName = firstNonEmpty( payload?.podName, runtime?.podName, pod?.name, payload?.health?.podName ); const namespace = firstNonEmpty( payload?.namespace, runtime?.namespace, pod?.namespace, service.deploy?.namespace, service.artifact?.namespace ); const container = firstNonEmpty( payload?.container, payload?.containerName, runtime?.container, runtime?.containerName, pod?.container, pod?.containerName, defaultRuntimeContainer(service.serviceId) ); const unideskRoute = podName ? `G14:k3s:${namespace ?? "hwlab-v02"}:pod:${podName}${container ? `:${container}` : ""}` : null; return pruneUndefined({ pod: pruneUndefined({ name: podName, namespace, container, nodeName: firstNonEmpty(pod?.nodeName, runtime?.nodeName, payload?.nodeName) }), unideskRoute }); } function defaultRuntimeContainer(serviceId) { if (["hwlab-cloud-api", "hwlab-cloud-web", "hwlab-device-pod", "hwlab-agent-mgr", "hwlab-agent-skills"].includes(serviceId)) return serviceId; return ""; } function firstNonEmpty(...values) { for (const value of values) { if (value === null || value === undefined) continue; const textValue = String(value).trim(); if (textValue) return textValue; } return null; } function pruneUndefined(value) { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== null && item !== "")); } function liveBuildIdentityFromHealth({ payload, imageReference, commitId }) { return { imageReference, imageTag: payload.image?.tag ?? imageTagFromReference(imageReference) ?? "unknown", commitId, revision: payload.revision ?? commitId, digest: payload.image?.digest ?? "unknown" }; } function selectMatchingMetadataRecord(liveIdentity, metadataRecords) { let firstMatched = null; for (const record of metadataRecords) { const match = liveBuildMetadataMatch(liveIdentity, record); if (!match.matched) continue; if (record.build?.createdAt) return { record, match }; firstMatched ??= { record, match }; } if (firstMatched) return firstMatched; return { record: null, match: metadataRecords.length ? liveBuildMetadataMatch(liveIdentity, metadataRecords[0]) : null }; } function liveBuildMetadataMatch(liveIdentity, metadataRecord) { if (!metadataRecord) { return { matched: false, matchedValues: [], live: liveBuildIdentitySummary(liveIdentity), metadata: null }; } const liveValues = uniqueStrings([ liveIdentity.imageTag, imageTagFromReference(liveIdentity.imageReference), liveIdentity.revision, liveIdentity.commitId, liveIdentity.digest ]).filter((value) => value !== "unknown"); const metadataValues = uniqueStrings([ metadataRecord.image?.tag, imageTagFromReference(metadataRecord.image?.reference), metadataRecord.revision, metadataRecord.commit?.id, metadataRecord.image?.digest ]).filter((value) => value !== "unknown" && value !== "not_published"); const matchedValues = liveValues.filter((liveValue) => metadataValues.some((metadataValue) => liveBuildIdentityEquivalent(liveValue, metadataValue)) ); return { matched: matchedValues.length > 0, matchedValues, live: liveBuildIdentitySummary(liveIdentity), metadata: { imageTag: metadataRecord.image?.tag ?? "unknown", revision: metadataRecord.revision ?? "unknown", commitId: metadataRecord.commit?.id ?? "unknown", digest: metadataRecord.image?.digest ?? "unknown" } }; } function liveBuildIdentitySummary(identity) { return { imageTag: identity?.imageTag ?? "unknown", revision: identity?.revision ?? "unknown", commitId: identity?.commitId ?? "unknown", digest: identity?.digest ?? "unknown" }; } function liveBuildIdentityEquivalent(left, right) { const a = String(left ?? "").trim(); const b = String(right ?? "").trim(); if (!a || !b || a === "unknown" || b === "unknown") return false; if (a === b) return true; if (a.startsWith("sha256:") || b.startsWith("sha256:")) return false; return a.length >= 7 && b.length >= 7 && (a.startsWith(b) || b.startsWith(a)); } function liveBuildRecordsFromMetadata(service) { return [ liveBuildRecordFromMetadataSource(service, "artifact"), liveBuildRecordFromMetadataSource(service, "deploy") ].filter(Boolean); } function liveBuildRecordFromMetadataSource(service, sourceKind) { const source = sourceKind === "artifact" ? service.artifact : service.deploy; if (!source) return null; const createdAt = normalizeIsoTimestamp( sourceKind === "deploy" ? source.env?.HWLAB_BUILD_CREATED_AT : source.buildCreatedAt ); const imageReference = sourceKind === "deploy" ? source.env?.HWLAB_IMAGE ?? source.image ?? "unknown" : source.image ?? service.deploy?.image ?? "unknown"; const commitId = sourceKind === "artifact" ? source.commitId ?? imageTagFromReference(imageReference) ?? "unknown" : source.env?.HWLAB_COMMIT_ID ?? imageTagFromReference(imageReference) ?? "unknown"; const digest = sourceKind === "deploy" ? source.env?.HWLAB_IMAGE_DIGEST ?? "unknown" : source.digest ?? "unknown"; const buildSource = sourceKind === "deploy" ? source.env?.HWLAB_BUILD_SOURCE ?? "deploy-desired-state-env" : source.buildSource ?? "artifact-catalog-metadata"; const imageTag = sourceKind === "deploy" ? source.env?.HWLAB_IMAGE_TAG ?? imageTagFromReference(imageReference) ?? "unknown" : source.imageTag ?? imageTagFromReference(imageReference) ?? "unknown"; const revision = sourceKind === "deploy" ? source.env?.HWLAB_REVISION ?? commitId : commitId; return { serviceId: service.serviceId, name: service.serviceName, kind: "hwlab", status: createdAt ? "ok" : "build_time_unavailable", healthUrl: null, httpStatus: null, desiredState: desiredStateSummary(service), build: { createdAt, source: buildSource, metadataSource: artifactMetadataSource(sourceKind), unavailableReason: createdAt ? null : `构建时间不可用:${controlledMetadataLabel({ build: { metadataSource: artifactMetadataSource(sourceKind) } })} 缺少 buildCreatedAt` }, image: { reference: imageReference, tag: imageTag, digest }, commit: { id: commitId, source: sourceKind === "artifact" ? "artifact-catalog" : "deploy-desired-state" }, revision }; } function desiredStateSummary(service) { return { namespace: service.deploy?.namespace ?? service.artifact?.namespace ?? "hwlab-dev", profile: service.deploy?.profile ?? service.artifact?.profile ?? ENVIRONMENT_DEV, replicas: Number.isFinite(Number(service.deploy?.replicas)) ? Number(service.deploy.replicas) : null, healthPath: service.healthPath ?? service.deploy?.healthPath ?? service.artifact?.healthPath ?? "/health/live", source: service.deploy ? LIVE_BUILD_METADATA_PATHS.deployManifest : "service-defaults" }; } function artifactMetadataSource(sourceKind) { if (sourceKind === "artifact") return "artifact-catalog:buildCreatedAt"; return "deploy-env:HWLAB_BUILD_CREATED_AT"; } function controlledMetadataLabel(record) { const source = String(record?.build?.metadataSource ?? ""); if (source.includes("artifact-catalog")) return "catalog metadata"; if (source.includes("deploy-env")) return "deploy metadata"; return "controlled metadata"; } function unavailableLiveBuildRecord(service, { healthUrl = null, httpStatus = null, reasonCode, reason }) { return { serviceId: service.serviceId, name: service.serviceName, kind: "hwlab", status: "unavailable", healthUrl, httpStatus, desiredState: desiredStateSummary(service), build: { createdAt: null, metadataSource: "unavailable", unavailableReason: `构建时间不可用:${reason}` }, image: { reference: "unknown", tag: "unknown", digest: "unknown" }, commit: { id: "unknown", source: "unavailable" }, revision: "unknown", error: { code: reasonCode, message: reason } }; } function externalLiveBuildRecord(service, { healthUrl = null, httpStatus = null, imageReference = "unknown", imageTag = null, imageDigest = "unknown", commitId = "unknown", commitSource = "external-image", revision = commitId, reason }) { return { serviceId: service.serviceId, name: service.serviceName, kind: "external", status: "external", healthUrl, httpStatus, build: { createdAt: null, metadataSource: "external-image", unavailableReason: reason }, image: { reference: imageReference, tag: imageTag ?? imageTagFromReference(imageReference) ?? "unknown", digest: imageDigest }, commit: { id: commitId, source: commitSource }, revision }; } function healthUnavailableReason(service, reason) { if (Number(service.deploy?.replicas) === 0) { return `${service.serviceId} 当前 hwlab-dev desired replicas=0,无可读取的常驻 /health/live:${reason}`; } if (service.activation === "manual") { return `${service.serviceId} 是手动激活服务,当前 live health 不可用:${reason}`; } return reason; } function normalizeHealthBuildTime(payload) { return normalizeIsoTimestamp( payload?.build?.createdAt ?? payload?.image?.createdAt ?? payload?.buildCreatedAt ?? payload?.metadata?.buildCreatedAt ?? null ); } async function loadLiveBuildMetadata(options = {}) { if (options.liveBuildMetadata) return normalizeLiveBuildMetadata(options.liveBuildMetadata); const root = options.repoRoot ?? process.env.HWLAB_REPO_ROOT ?? repoRoot; const [deployManifest, artifactCatalog] = await Promise.all([ readJsonMetadata(root, LIVE_BUILD_METADATA_PATHS.deployManifest), readJsonMetadata(root, LIVE_BUILD_METADATA_PATHS.artifactCatalog) ]); return normalizeLiveBuildMetadata({ deployManifest, artifactCatalog }); } function normalizeLiveBuildMetadata(metadata) { return { deployManifest: normalizeMetadataFile(metadata.deployManifest), artifactCatalog: normalizeMetadataFile(metadata.artifactCatalog) }; } async function readJsonMetadata(root, relativePath) { try { const payload = JSON.parse(await readFile(path.join(root, relativePath), "utf8")); return { status: "ok", path: relativePath, payload }; } catch (error) { return { status: "unavailable", path: relativePath, error: error.message, payload: null }; } } function normalizeMetadataFile(file) { if (file?.status === "ok" || file?.status === "unavailable") { return { status: file.status, path: file.path ?? null, error: file.error ?? null, payload: file.payload ?? null }; } return { status: file ? "ok" : "unavailable", path: null, error: null, payload: file ?? null }; } function liveBuildServiceInventory(metadata, env = process.env) { const deployServices = Array.isArray(metadata.deployManifest.payload?.services) ? metadata.deployManifest.payload.services : []; const deployByServiceId = new Map(deployServices.map((service) => [service.serviceId, service])); const artifactByServiceId = new Map( (Array.isArray(metadata.artifactCatalog.payload?.services) ? metadata.artifactCatalog.payload.services : []) .map((service) => [service.serviceId, service]) ); const serviceIds = uniqueStrings([ ...SERVICE_IDS, ...deployServices.map((service) => service.serviceId), ...artifactByServiceId.keys() ]).filter((serviceId) => String(serviceId).startsWith("hwlab-") && !serviceRemovedForRuntimeProfile(serviceId, env)); return [ ...serviceIds.map((serviceId) => { const deploy = deployByServiceId.get(serviceId) ?? null; const defaults = LIVE_BUILD_SERVICE_DEFAULTS[serviceId] ?? {}; const healthPath = deploy?.healthPath ?? defaults.healthPath ?? "/health/live"; return Object.freeze({ serviceId, serviceName: deploy?.name ?? serviceId, urlEnv: defaults.urlEnv ?? serviceUrlEnvName(serviceId), defaultUrl: defaults.defaultUrl ?? serviceDefaultUrl(serviceId), healthPath, activation: deploy?.replicas === 0 ? "zero-replica" : defaults.activation, deploy, artifact: artifactByServiceId.get(serviceId) ?? null }); }), ...LIVE_BUILD_EXTERNAL_COMPONENTS ]; } function serviceRemovedForRuntimeProfile(serviceId, env = process.env) { const profile = String(env.HWLAB_GITOPS_PROFILE || env.HWLAB_ENVIRONMENT || "").trim().toLowerCase(); return profile === "v02" && V02_REMOVED_SERVICE_IDS.includes(serviceId); } function uniqueStrings(values) { return [...new Set(values.map((value) => String(value ?? "").trim()).filter(Boolean))]; } function serviceUrlEnvName(serviceId) { return `${serviceId.replace(/^hwlab-/u, "HWLAB_").replace(/-/gu, "_").toUpperCase()}_URL`; } function serviceDefaultUrl(serviceId) { const port = { "hwlab-cloud-api": 6667, "hwlab-cloud-web": 8080, "hwlab-agent-mgr": 7410, "hwlab-agent-worker": 7411, "hwlab-device-pod": 7601, "hwlab-gateway": 7001, "hwlab-edge-proxy": 6667, "hwlab-agent-skills": 7430 }[serviceId] ?? 8080; return `http://${serviceId}.hwlab-dev.svc.cluster.local:${port}`; }