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"; import { parseStructuredConfig } from "../../scripts/src/structured-config.mjs"; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const LIVE_BUILD_TIMEOUT_MS = 900; const LIVE_BUILD_DEPLOY_MANIFEST_PATH = "deploy/deploy.yaml"; 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" }), "hwlab-gateway": Object.freeze({ urlEnv: "HWLAB_GATEWAY_URL" }), "hwlab-edge-proxy": Object.freeze({ urlEnv: "HWLAB_EDGE_PROXY_URL", healthPath: "/health" }), "hwlab-agent-skills": Object.freeze({ urlEnv: "HWLAB_AGENT_SKILLS_URL" }) }); 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 构建产物" }) ]); 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; } function liveBuildProfile(env = process.env) { return defaultRuntimeEnvironment(env); } export async function buildLiveBuildsPayload(options = {}, dependencies = {}) { const env = options.env ?? process.env; const metadata = await loadLiveBuildMetadata(options, env); 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: metadata.paths, desiredStateSource: metadata.deployManifest.status === "ok" ? metadata.paths.deployManifest : "unavailable", artifactCatalogSource: metadata.artifactCatalog.status === "ok" ? 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 }; } 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.runtimeNamespace, 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 ? namespace ? `G14:k3s:${namespace}:pod:${podName}${container ? `:${container}` : ""}` : null : 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-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.runtimeNamespace ?? service.deploy?.namespace ?? service.artifact?.namespace ?? null, profile: service.runtimeProfile ?? 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 ? service.metadataPaths?.deployManifest ?? LIVE_BUILD_DEPLOY_MANIFEST_PATH : "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) { const namespace = desiredStateSummary(service).namespace; return `${service.serviceId} 当前 ${namespace} 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 = {}, env = process.env) { if (options.liveBuildMetadata) return normalizeLiveBuildMetadata(options.liveBuildMetadata, env); const root = options.repoRoot ?? process.env.HWLAB_REPO_ROOT ?? repoRoot; const deployManifest = await readJsonMetadata(root, LIVE_BUILD_DEPLOY_MANIFEST_PATH); const paths = metadataPathsFromDeployManifest(deployManifest.payload, liveBuildProfile(env)); const artifactCatalog = await readJsonMetadata(root, paths.artifactCatalog); return normalizeLiveBuildMetadata({ deployManifest, artifactCatalog }, paths); } function normalizeLiveBuildMetadata(metadata, envOrPaths = process.env) { const deployManifest = normalizeMetadataFile(metadata.deployManifest); const paths = isMetadataPaths(envOrPaths) ? envOrPaths : metadataPathsFromDeployManifest(deployManifest.payload, liveBuildProfile(envOrPaths)); return { paths, deployManifest, artifactCatalog: normalizeMetadataFile(metadata.artifactCatalog) }; } function isMetadataPaths(value) { return Boolean(value?.deployManifest && value?.artifactCatalog); } function metadataPathsFromDeployManifest(deployManifest, profile) { const lane = deployLaneForProfile(deployManifest, profile); const artifactCatalog = firstNonEmpty( lane?.artifactCatalog, deployManifest?.artifactCatalog ); return { deployManifest: LIVE_BUILD_DEPLOY_MANIFEST_PATH, artifactCatalog }; } async function readJsonMetadata(root, relativePath) { try { const raw = await readFile(path.join(root, relativePath), "utf8"); const payload = parseStructuredConfig(raw, relativePath); 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 profile = liveBuildProfile(env); const lane = deployLaneForProfile(metadata.deployManifest.payload, profile); const runtimeNamespace = firstNonEmpty(lane?.namespace, metadata.deployManifest.payload?.namespace); const deployServices = deployServicesForProfile(metadata.deployManifest.payload, profile, lane, runtimeNamespace); 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) => normalizeServiceRuntimeTarget(service, profile, runtimeNamespace)) .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, lane)); 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"; const defaultUrl = serviceId === CLOUD_API_SERVICE_ID ? defaults.defaultUrl ?? serviceDefaultUrl(serviceId, runtimeNamespace) : serviceDefaultUrl(serviceId, runtimeNamespace); return Object.freeze({ serviceId, serviceName: deploy?.name ?? serviceId, urlEnv: defaults.urlEnv ?? serviceUrlEnvName(serviceId), defaultUrl, healthPath, activation: deploy?.replicas === 0 ? "zero-replica" : defaults.activation, runtimeProfile: profile, runtimeNamespace, metadataPaths: metadata.paths, deploy, artifact: artifactByServiceId.get(serviceId) ?? null }); }), ...LIVE_BUILD_EXTERNAL_COMPONENTS ]; } function deployLaneForProfile(deployManifest, profile) { const lanes = deployManifest?.lanes && typeof deployManifest.lanes === "object" ? deployManifest.lanes : {}; return lanes[profile] ?? null; } function deployServicesForProfile(deployManifest, profile, lane, runtimeNamespace) { const baseServices = Array.isArray(deployManifest?.services) ? deployManifest.services : []; if (!lane) return baseServices; const servicesById = new Map(baseServices.map((service) => [service.serviceId, { ...service, env: { ...(service.env ?? {}) } }])); const laneServices = Array.isArray(lane.services) ? lane.services : []; for (const service of laneServices) { const current = servicesById.get(service.serviceId) ?? { serviceId: service.serviceId }; servicesById.set(service.serviceId, { ...current, ...service, env: { ...(current.env ?? {}), ...(service.env ?? {}) } }); } return [...servicesById.values()].map((service) => normalizeServiceRuntimeTarget(service, profile, runtimeNamespace)); } function normalizeServiceRuntimeTarget(service, profile, runtimeNamespace) { if (!service) return service; return { ...service, namespace: runtimeNamespace, profile }; } function serviceRemovedForRuntimeProfile(serviceId, lane) { const removed = uniqueStrings([ ...(Array.isArray(lane?.removedServices) ? lane.removedServices : []), ...(Array.isArray(lane?.disabledServices) ? lane.disabledServices : []), ...(Array.isArray(lane?.omitServices) ? lane.omitServices : []) ]); return removed.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, namespace = null) { if (!namespace) return null; const port = { "hwlab-cloud-api": 6667, "hwlab-cloud-web": 8080, "hwlab-gateway": 7001, "hwlab-edge-proxy": 6667, "hwlab-agent-skills": 7430 }[serviceId] ?? 8080; return `http://${serviceId}.${namespace}.svc.cluster.local:${port}`; }