// SPEC: PJ2026-01060703 CI/CD branch follower reuse config. // Responsibility: native target-side parser for runtime/env reuse declarations. import { createHash } from "node:crypto"; export const RUNTIME_REUSE_CONFIG_PATH = "gitops/reuse.ymal"; export function parseRuntimeReuseConfig(text, source) { const errors = []; let parsed = null; try { parsed = Bun.YAML.parse(text); } 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 summarizeRuntimeReuseConfig(config) { 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, }; } function parseServices(spec, errors) { 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, path, errors) { 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, path, errors) { 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, path, errors) { 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, path, errors) { if (record === null) return {}; const out = {}; 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) { return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null; } function stringOrNull(value) { return typeof value === "string" && value.length > 0 ? value : null; } function booleanOrNull(value) { return typeof value === "boolean" ? value : null; }