type RuntimeLaneDefaults = { defaultNodeId?: string; defaultGitopsRoot?: string; defaultBranch?: string; defaultGitopsBranch?: string; defaultCatalogPath?: string; defaultRuntimeEndpoint?: string; defaultWebEndpoint?: string; defaultV02RuntimeEndpoint?: string; defaultSourceRepo?: string; }; export type GitOpsRenderArgs = { lane: string; nodeId?: string; outDir: string; gitopsRoot?: string; sourceBranch: string; gitopsBranch: string; catalogPath: string; imageTagMode: string; runtimeEndpoint: string; webEndpoint: string; sourceRepo: string; [key: string]: unknown; }; export type RuntimeLaneConfig = { name?: string; node?: string; sourceBranch?: string; gitopsBranch?: string; gitReadUrl?: string; gitWriteUrl?: string; artifactCatalog?: string; runtimePath?: string; namespace?: string; imageTagMode?: string; endpoint?: string; externalPostgres?: { enabled?: boolean; serviceName?: string; endpointAddress?: string; port?: number; }; publicEndpoints?: { frontend?: string; api?: string; [key: string]: string | undefined; }; observability?: { traceExplorerUrlTemplate?: string; prometheusOperatorResources?: boolean; }; sourceRepo?: string; }; export type NodeConfig = { gitopsRoot?: string; sourceRepo?: string; publicHost?: string; }; export type RuntimeLaneMirrorSpec = { lane: string; sourceBranch: string; gitopsBranch: string; artifactCatalog: string; runtimePath: string; }; export type DeployConfig = { nodes?: Record; lanes?: Record; }; const defaultNodeId = "node"; const defaultGitopsRoot = "deploy/gitops/node"; const defaultPublicHost = "74.48.78.17"; export function isRuntimeLane(lane: unknown): lane is string { const value = String(lane ?? ""); return !["node", "dev", "prod"].includes(value) && /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u.test(value); } export function isVersionRuntimeLane(lane: unknown): lane is string { return /^v[0-9]{2,}$/u.test(String(lane ?? "")); } export function isNamedRuntimeLane(lane: unknown): lane is string { return isRuntimeLane(lane) && !isVersionRuntimeLane(lane); } export function runtimeLaneMinor(lane: string): number { invariant(isVersionRuntimeLane(lane), `version runtime lane must look like v02/v03, got ${lane}`); return Number.parseInt(lane.slice(1), 10); } export function versionNameForRuntimeLane(lane: string): string { return `v0.${runtimeLaneMinor(lane)}`; } export function namespaceNameForRuntimeLane(lane: string): string { invariant(isVersionRuntimeLane(lane), `version runtime lane must look like v02/v03, got ${lane}`); return `hwlab-${lane}`; } export function runtimePathForRuntimeLane(lane: string): string { invariant(isVersionRuntimeLane(lane), `version runtime lane must look like v02/v03, got ${lane}`); return `runtime-${lane}`; } export function nodeConfig(deploy: DeployConfig, nodeId: string): NodeConfig { const config = deploy.nodes?.[nodeId]; return isObject(config) ? config : {}; } export function nodeIdForRuntimeLane(deploy: DeployConfig, lane: string, defaults: RuntimeLaneDefaults = {}): string { const laneConfig = runtimeLaneConfig(deploy, lane); return laneConfig.node ?? defaults.defaultNodeId ?? defaultNodeId; } export function gitopsRootForNode(deploy: DeployConfig, nodeId: string, defaults: RuntimeLaneDefaults = {}): string { const config = nodeConfig(deploy, nodeId); return normalizeRepoPath(config.gitopsRoot ?? defaults.defaultGitopsRoot ?? defaultGitopsRoot); } export function runtimeGitopsPathForRuntimeLane(deploy: DeployConfig, lane: string, defaults: RuntimeLaneDefaults = {}): string { const laneConfig = runtimeLaneConfig(deploy, lane); const runtimePath = normalizeRepoPath(laneConfig.runtimePath ?? runtimePathForRuntimeLane(lane)); if (runtimePath.startsWith("deploy/")) return runtimePath; return joinRepoPath(gitopsRootForNode(deploy, nodeIdForRuntimeLane(deploy, lane, defaults), defaults), runtimePath); } export function defaultPortForRuntimeLane(lane: string): number { return 17666 + runtimeLaneMinor(lane) * 1000; } export function publicHostLabel(host = defaultPublicHost): string { return host.replaceAll(".", "-"); } export function defaultEndpointForRuntimeLane(lane: string, options: { host?: string; v02Endpoint?: string } = {}): string { if (lane === "v02" && options.v02Endpoint) return options.v02Endpoint; return `https://hwlab-${lane}.${publicHostLabel(options.host ?? defaultPublicHost)}.nip.io`; } export function runtimeLaneConfig(deploy: DeployConfig, lane: string): RuntimeLaneConfig { invariant(isRuntimeLane(lane), `runtime lane must be a stable lowercase id, got ${lane}`); const config = deploy.lanes?.[lane]; invariant(isObject(config), `deploy.lanes.${lane} is required`); if (isNamedRuntimeLane(lane)) validateNamedRuntimeLaneConfig(config, lane); return config; } export function applyRuntimeLaneDeployConfig(args: GitOpsRenderArgs, deploy: DeployConfig, defaults: RuntimeLaneDefaults = {}): GitOpsRenderArgs { if (!isRuntimeLane(args.lane)) return args; const result: GitOpsRenderArgs = { ...args }; const laneConfig = runtimeLaneConfig(deploy, args.lane); const configuredNodeId = nodeIdForRuntimeLane(deploy, args.lane, defaults); const rootNodeId = nodeIdFromGitopsRoot(result.gitopsRoot); const requestedNodeId = result.nodeId && result.nodeId !== defaultNodeId ? result.nodeId : rootNodeId ?? configuredNodeId; const nodeId = isNamedRuntimeLane(args.lane) ? configuredNodeId : requestedNodeId; const node = nodeConfig(deploy, nodeId); const versionName = isVersionRuntimeLane(args.lane) ? versionNameForRuntimeLane(args.lane) : null; const defaultEndpointOptions: { host?: string; v02Endpoint?: string } = {}; if (node.publicHost) defaultEndpointOptions.host = node.publicHost; if (defaults.defaultV02RuntimeEndpoint) defaultEndpointOptions.v02Endpoint = defaults.defaultV02RuntimeEndpoint; const defaultEndpoint = versionName ? defaultEndpointForRuntimeLane(args.lane, defaultEndpointOptions) : null; const configuredEndpoint = laneConfig.endpoint ?? defaultEndpoint; const configuredWebEndpoint = laneConfig.publicEndpoints?.frontend ?? configuredEndpoint; const configuredRuntimeEndpoint = laneConfig.publicEndpoints?.api ?? configuredEndpoint; const defaultBranch = defaults.defaultBranch ?? "node"; const defaultGitopsBranch = defaults.defaultGitopsBranch ?? "node-gitops"; const defaultCatalogPath = defaults.defaultCatalogPath ?? "deploy/artifact-catalog.dev.json"; const defaultRuntimeEndpoint = defaults.defaultRuntimeEndpoint ?? "http://74.48.78.17:17667"; const defaultWebEndpoint = defaults.defaultWebEndpoint ?? "http://74.48.78.17:17666"; const defaultSourceRepo = defaults.defaultSourceRepo ?? "git@github.com:pikasTech/HWLAB.git"; const defaultCatalog = `deploy/artifact-catalog.${args.lane}.json`; if (isNamedRuntimeLane(args.lane)) { const configuredSourceRepo = laneConfig.sourceRepo ?? node.sourceRepo; invariant(typeof node.gitopsRoot === "string" && node.gitopsRoot.trim().length > 0, `deploy.nodes.${nodeId}.gitopsRoot is required for named runtime lane ${args.lane}`); requiredString(configuredSourceRepo, `deploy.lanes.${args.lane}.sourceRepo or deploy.nodes.${nodeId}.sourceRepo`); result.nodeId = configuredNodeId; result.gitopsRoot = normalizeRepoPath(node.gitopsRoot); result.sourceBranch = requiredString(laneConfig.sourceBranch, `deploy.lanes.${args.lane}.sourceBranch`); result.gitopsBranch = requiredString(laneConfig.gitopsBranch, `deploy.lanes.${args.lane}.gitopsBranch`); result.catalogPath = normalizeRepoPath(requiredString(laneConfig.artifactCatalog, `deploy.lanes.${args.lane}.artifactCatalog`)); result.imageTagMode = requiredString(laneConfig.imageTagMode, `deploy.lanes.${args.lane}.imageTagMode`); result.runtimeEndpoint = requiredString(laneConfig.publicEndpoints?.api, `deploy.lanes.${args.lane}.publicEndpoints.api`); result.webEndpoint = requiredString(laneConfig.publicEndpoints?.frontend, `deploy.lanes.${args.lane}.publicEndpoints.frontend`); result.sourceRepo = configuredSourceRepo as string; result.gitReadUrl = requiredString(laneConfig.gitReadUrl, `deploy.lanes.${args.lane}.gitReadUrl`); result.gitWriteUrl = requiredString(laneConfig.gitWriteUrl, `deploy.lanes.${args.lane}.gitWriteUrl`); return result; } if (!result.nodeId || result.nodeId === defaultNodeId) result.nodeId = nodeId; if (!result.gitopsRoot || result.gitopsRoot === (defaults.defaultGitopsRoot ?? defaultGitopsRoot)) result.gitopsRoot = gitopsRootForNode(deploy, nodeId, defaults); if (result.sourceBranch === defaultBranch || result.sourceBranch === versionName) result.sourceBranch = laneConfig.sourceBranch ?? versionName ?? result.sourceBranch; if (result.gitopsBranch === defaultGitopsBranch || result.gitopsBranch === `${versionName}-gitops`) result.gitopsBranch = laneConfig.gitopsBranch ?? (versionName ? `${versionName}-gitops` : result.gitopsBranch); if (result.catalogPath === defaultCatalogPath || result.catalogPath === defaultCatalog) result.catalogPath = laneConfig.artifactCatalog ?? defaultCatalog; if (result.imageTagMode === "full") result.imageTagMode = laneConfig.imageTagMode ?? result.imageTagMode; if (result.runtimeEndpoint === defaultRuntimeEndpoint || result.runtimeEndpoint === defaultEndpoint) result.runtimeEndpoint = configuredRuntimeEndpoint ?? result.runtimeEndpoint; if (result.webEndpoint === defaultWebEndpoint || result.webEndpoint === defaultEndpoint) result.webEndpoint = configuredWebEndpoint ?? result.webEndpoint; if (result.sourceRepo === defaultSourceRepo) result.sourceRepo = laneConfig.sourceRepo ?? node.sourceRepo ?? result.sourceRepo; return result; } function nodeIdFromGitopsRoot(gitopsRoot: unknown): string | undefined { if (typeof gitopsRoot !== "string") return undefined; const normalized = normalizeRepoPath(gitopsRoot); const match = normalized.match(/(?:^|\/)deploy\/gitops\/node\/([^/]+)$/u); return match?.[1] ? match[1].toUpperCase() : undefined; } export function runtimeLaneMirrorSpecs(deploy: DeployConfig, defaults: RuntimeLaneDefaults = {}): RuntimeLaneMirrorSpec[] { return Object.entries(deploy.lanes ?? {}) .filter(([lane, config]) => isRuntimeLane(lane) && isObject(config)) .sort(([left], [right]) => left.localeCompare(right, "en")) .map(([lane, config]) => { const laneConfig = config as RuntimeLaneConfig; const versionName = isVersionRuntimeLane(lane) ? versionNameForRuntimeLane(lane) : null; return { lane, sourceBranch: laneConfig.sourceBranch ?? versionName ?? requiredString(laneConfig.sourceBranch, `deploy.lanes.${lane}.sourceBranch`), gitopsBranch: laneConfig.gitopsBranch ?? (versionName ? `${versionName}-gitops` : requiredString(laneConfig.gitopsBranch, `deploy.lanes.${lane}.gitopsBranch`)), artifactCatalog: laneConfig.artifactCatalog ?? `deploy/artifact-catalog.${lane}.json`, runtimePath: runtimeGitopsPathForRuntimeLane(deploy, lane, defaults), }; }); } export function runtimeNamespaceForRuntimeLane(deploy: DeployConfig, lane: string): string { const laneConfig = runtimeLaneConfig(deploy, lane); return laneConfig.namespace ?? namespaceNameForRuntimeLane(lane); } export function runtimeRelativePathForRuntimeLane(deploy: DeployConfig, lane: string): string { const laneConfig = runtimeLaneConfig(deploy, lane); return normalizeRepoPath(laneConfig.runtimePath ?? runtimePathForRuntimeLane(lane)); } function validateNamedRuntimeLaneConfig(config: RuntimeLaneConfig, lane: string): void { const label = `deploy.lanes.${lane}`; for (const key of ["name", "node", "sourceBranch", "gitopsBranch", "gitReadUrl", "gitWriteUrl", "artifactCatalog", "runtimePath", "namespace", "imageTagMode"] as const) { requiredString(config[key], `${label}.${key}`); } invariant(config.imageTagMode === "short" || config.imageTagMode === "full", `${label}.imageTagMode must be short or full`); invariant(isObject(config.publicEndpoints), `${label}.publicEndpoints is required`); requiredString(config.publicEndpoints.frontend, `${label}.publicEndpoints.frontend`); requiredString(config.publicEndpoints.api, `${label}.publicEndpoints.api`); invariant(Array.isArray((config as Record).envReuseServices) && ((config as Record).envReuseServices as unknown[]).length > 0, `${label}.envReuseServices must not be empty`); invariant(isObject((config as Record).serviceDeclarations), `${label}.serviceDeclarations is required`); } function requiredString(value: unknown, label: string): string { invariant(typeof value === "string" && value.trim().length > 0, `${label} must be a non-empty string`); return value.trim(); } function normalizeRepoPath(value: string): string { return value.replaceAll("\\\\", "/").replace(/^\.\//u, "").replace(/\/+$/u, ""); } function joinRepoPath(left: string, right: string): string { return `${normalizeRepoPath(left)}/${normalizeRepoPath(right).replace(/^\/+/, "")}`; } function isObject(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function invariant(condition: unknown, message: string): asserts condition { if (!condition) throw new Error(message); }