// SPEC: PJ2026-01060703 CI/CD branch follower reuse config. // Responsibility: shared parser for runtime/env reuse declarations stored in source repos. import { createHash } from "node:crypto"; export const RUNTIME_REUSE_CONFIG_PATH = "gitops/reuse.ymal"; export interface RuntimeReuseConfig { ok: boolean; present: boolean; path: string; sourceCommit: string | null; stageRef: string | null; sha256: string | null; apiVersion: string | null; kind: string | null; serviceCount: number; services: RuntimeReuseService[]; errors: string[]; valuesRedacted: true; } export interface RuntimeReuseService { id: string; runtimeReuse: { enabled: boolean | null; codeIdentityPaths: string[]; envIdentityPaths: string[]; } | null; envReuse: { enabled: boolean | null; mode: string | null; nodeDepsPath: string | null; envIdentityFiles: string[]; buildArgs: Record; } | null; } export function parseRuntimeReuseConfig(text: string, source: { sourceCommit: string | null; stageRef: string | null }): RuntimeReuseConfig { const errors: string[] = []; let parsed: unknown = null; try { parsed = Bun.YAML.parse(text) as unknown; } catch (error) { errors.push(`${RUNTIME_REUSE_CONFIG_PATH} YAML parse failed: ${error instanceof Error ? error.message : String(error)}`); } const root = asOptionalRecord(parsed); if (root === null) errors.push(`${RUNTIME_REUSE_CONFIG_PATH} must be a YAML object`); const spec = asOptionalRecord(root?.spec) ?? root ?? {}; const services = parseServices(spec, errors); if (services.length === 0) errors.push(`${RUNTIME_REUSE_CONFIG_PATH}.spec.services must declare at least one service`); const kind = stringOrNull(root?.kind); if (kind !== null && kind !== "RuntimeReuseConfig") errors.push(`${RUNTIME_REUSE_CONFIG_PATH}.kind must be RuntimeReuseConfig`); return { ok: errors.length === 0, present: true, path: RUNTIME_REUSE_CONFIG_PATH, sourceCommit: source.sourceCommit, stageRef: source.stageRef, sha256: createHash("sha256").update(text).digest("hex"), apiVersion: stringOrNull(root?.apiVersion), kind, serviceCount: services.length, services, errors, valuesRedacted: true, }; } export function missingRuntimeReuseConfig(source: { sourceCommit: string | null; stageRef: string | null }, reason: string): RuntimeReuseConfig { return { ok: false, present: false, path: RUNTIME_REUSE_CONFIG_PATH, sourceCommit: source.sourceCommit, stageRef: source.stageRef, sha256: null, apiVersion: null, kind: null, serviceCount: 0, services: [], errors: [reason], valuesRedacted: true, }; } export function invalidRuntimeReuseConfig(config: RuntimeReuseConfig, reason: string): RuntimeReuseConfig { return { ...config, ok: false, errors: [reason, ...config.errors].slice(0, 6), }; } export function summarizeRuntimeReuseConfig(config: RuntimeReuseConfig | null): Record | null { if (config === null) return null; return { ok: config.ok, present: config.present, path: config.path, sourceCommit: config.sourceCommit, stageRef: config.stageRef, sha256: config.sha256, serviceCount: config.serviceCount, serviceIds: config.services.map((service) => service.id).sort(), errors: config.errors.slice(0, 5), valuesRedacted: true, }; } export function runtimeReuseService(config: RuntimeReuseConfig | null, ids: readonly string[]): RuntimeReuseService | null { if (config === null) return null; const wanted = new Set(ids); return config.services.find((service) => wanted.has(service.id)) ?? null; } export function requiredReuseServiceError(reuseConfig: RuntimeReuseConfig, ids: readonly string[], mode: "runtimeReuse" | "envReuse"): string | null { const service = runtimeReuseService(reuseConfig, ids); if (service === null) return `${RUNTIME_REUSE_CONFIG_PATH} must declare service ${ids.join("|")}`; if (mode === "runtimeReuse") { if (service.runtimeReuse === null) return `${RUNTIME_REUSE_CONFIG_PATH} service ${service.id} must declare runtimeReuse`; if (service.runtimeReuse.enabled === false) return `${RUNTIME_REUSE_CONFIG_PATH} service ${service.id} runtimeReuse is disabled`; if (service.runtimeReuse.codeIdentityPaths.length === 0 && service.runtimeReuse.envIdentityPaths.length === 0) return `${RUNTIME_REUSE_CONFIG_PATH} service ${service.id} runtimeReuse must declare codeIdentity or envIdentity paths`; return null; } if (service.envReuse === null) return `${RUNTIME_REUSE_CONFIG_PATH} service ${service.id} must declare envReuse`; if (service.envReuse.enabled === false) return `${RUNTIME_REUSE_CONFIG_PATH} service ${service.id} envReuse is disabled`; if (service.envReuse.envIdentityFiles.length === 0 && service.envReuse.nodeDepsPath === null) return `${RUNTIME_REUSE_CONFIG_PATH} service ${service.id} envReuse must declare envIdentityFiles or nodeDepsPath`; return null; } function parseServices(spec: Record, errors: string[]): RuntimeReuseService[] { const raw = spec.services; const items = Array.isArray(raw) ? raw.map((value, index) => ({ id: stringOrNull(asOptionalRecord(value)?.id) ?? String(index), value, path: `services[${index}]` })) : Object.entries(asOptionalRecord(raw) ?? {}).map(([id, value]) => ({ id, value, path: `services.${id}` })); return items.flatMap(({ id, value, path }) => { const record = asOptionalRecord(value); if (record === null) { errors.push(`${RUNTIME_REUSE_CONFIG_PATH}.spec.${path} must be an object`); return []; } const serviceId = stringOrNull(record.id) ?? id; if (serviceId.length === 0) errors.push(`${RUNTIME_REUSE_CONFIG_PATH}.spec.${path}.id is required`); return [{ id: serviceId, runtimeReuse: parseRuntimeReuse(asOptionalRecord(record.runtimeReuse), `${path}.runtimeReuse`, errors), envReuse: parseEnvReuse(asOptionalRecord(record.envReuse), `${path}.envReuse`, errors), }]; }); } function parseRuntimeReuse(record: Record | null, path: string, errors: string[]): RuntimeReuseService["runtimeReuse"] { if (record === null) return null; return { enabled: booleanOrNull(record.enabled), codeIdentityPaths: safePathArray(asOptionalRecord(record.codeIdentity)?.paths ?? record.codeIdentityPaths, `${path}.codeIdentity.paths`, errors), envIdentityPaths: safePathArray(asOptionalRecord(record.envIdentity)?.paths ?? record.envIdentityPaths, `${path}.envIdentity.paths`, errors), }; } function parseEnvReuse(record: Record | null, path: string, errors: string[]): RuntimeReuseService["envReuse"] { if (record === null) return null; return { enabled: booleanOrNull(record.enabled), mode: stringOrNull(record.mode), nodeDepsPath: stringOrNull(record.nodeDepsPath), envIdentityFiles: safePathArray(record.envIdentityFiles ?? asOptionalRecord(record.envIdentity)?.paths, `${path}.envIdentityFiles`, errors), buildArgs: stringRecord(asOptionalRecord(record.buildArgs), `${path}.buildArgs`, errors), }; } function safePathArray(value: unknown, path: string, errors: string[]): string[] { if (value === undefined || value === null) return []; if (!Array.isArray(value)) { errors.push(`${RUNTIME_REUSE_CONFIG_PATH}.spec.${path} must be a string array`); return []; } return value.flatMap((item, index) => { if (typeof item !== "string" || item.length === 0) { errors.push(`${RUNTIME_REUSE_CONFIG_PATH}.spec.${path}[${index}] must be a non-empty string`); return []; } if (item.startsWith("/") || item.includes("..")) { errors.push(`${RUNTIME_REUSE_CONFIG_PATH}.spec.${path}[${index}] must be a relative path without ..`); return []; } return [item]; }); } function stringRecord(record: Record | null, path: string, errors: string[]): Record { if (record === null) return {}; const out: Record = {}; for (const [key, value] of Object.entries(record)) { if (typeof value !== "string") errors.push(`${RUNTIME_REUSE_CONFIG_PATH}.spec.${path}.${key} must be a string`); else out[key] = value; } return out; } function asOptionalRecord(value: unknown): Record | null { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : null; } function stringOrNull(value: unknown): string | null { return typeof value === "string" && value.length > 0 ? value : null; } function booleanOrNull(value: unknown): boolean | null { return typeof value === "boolean" ? value : null; }