// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. manifest module for scripts/src/deploy.ts. // Moved mechanically from scripts/src/deploy.ts:536-633 for #903. import { createHash } from "node:crypto"; import { existsSync, mkdirSync, readFileSync } from "node:fs"; import { join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { runCommand } from "../command"; import { type UniDeskConfig, type UniDeskMicroserviceConfig, repoRoot, rootPath } from "../config"; import { ensureGithubSshIdentityForProvider } from "../deploy-ssh-identity"; import { baiduNetdiskRuntimeSecretRequirements, runtimeSecretContractFromEnvText, type RuntimeSecretContract, runArtifactRegistryCommand } from "../artifact-registry"; import { startJob } from "../jobs"; import { coreInternalFetch } from "../microservices"; import { codeQueueSourceImportPreflight, codeQueueSourceSubdir } from "../code-queue-source-guard"; import { k3sGuardShellLines, defaultNativeKubeconfig } from "../k3s-target-guard"; import { composeRuntimeEnvValue } from "../runtime-env"; import { compareDeployJsonExecutorMirrors, deployJsonCommitImage, deployJsonDriftResult, deployJsonSourceOfTruth, encodeDeployJsonServiceContract, hasDeployJsonExecutorContract, k3sManifestExecutorMirror, parseDeployJsonServiceContract, type DeployJsonExecutorMirror, type DeployJsonServiceContract, } from "../deploy-json-contract"; import type { DeployEnvironment, DeployEnvironmentManifestSource, DeployManifest, DeployManifestService } from "./types"; import { asRecord, commandFailure, isDeployEnvironment, nowIso, parseFullCommit, runGitOrThrow } from "./options"; import { deployEnvironmentTargets } from "./types"; export function parseDeployManifestService(item: unknown, index: number, seen: Set, path: string): DeployManifestService { const service = parseDeployJsonServiceContract(item, `deploy manifest ${path}[${index}]`); const id = service.id; const repo = service.repo; const commitId = service.commitId; if (id.length === 0) throw new Error(`deploy manifest ${path}[${index}].id must be a non-empty string`); if (repo.length === 0) throw new Error(`deploy manifest ${path}[${index}].repo must be a non-empty string`); if (!/^[0-9a-f]{7,40}$/iu.test(commitId)) throw new Error(`deploy manifest ${path}[${index}].commitId must be a 7-40 char git SHA`); if (seen.has(id)) throw new Error(`duplicate deploy manifest service id: ${id}`); seen.add(id); return service; } export function parseDeployManifestServices(value: unknown, path: string): DeployManifestService[] { if (!Array.isArray(value)) throw new Error(`deploy manifest ${path} must be an array`); const seen = new Set(); return value.map((item, index) => parseDeployManifestService(item, index, seen, path)); } export function parseDeployManifest(parsed: unknown, source: string, expectedEnvironment: DeployEnvironment | null): DeployManifest { const record = asRecord(parsed); if (record === null) throw new Error(`deploy manifest ${source} must be an object`); if (record.schemaVersion !== 1 && record.schemaVersion !== 2) throw new Error("deploy manifest schemaVersion must be 1 or 2"); if (record.schemaVersion === 2) { const environments = asRecord(record.environments); if (environments === null) throw new Error("deploy manifest schemaVersion=2 requires environments"); if (expectedEnvironment === null) { const prod = asRecord(environments.prod); if (prod === null) throw new Error("deploy manifest schemaVersion=2 requires environments.prod for local compatibility mode"); return { schemaVersion: 2, environment: "prod", services: parseDeployManifestServices(prod.services, "environments.prod.services") }; } const envRecord = asRecord(environments[expectedEnvironment]); if (envRecord === null) throw new Error(`deploy manifest ${source} does not contain environments.${expectedEnvironment}`); return { schemaVersion: 2, environment: expectedEnvironment, services: parseDeployManifestServices(envRecord.services, `environments.${expectedEnvironment}.services`) }; } const rawEnvironment = record.environment; let environment: DeployEnvironment | null = null; if (rawEnvironment !== undefined) { if (typeof rawEnvironment !== "string" || !isDeployEnvironment(rawEnvironment)) { throw new Error("deploy manifest environment must be dev or prod when present"); } environment = rawEnvironment; } if (expectedEnvironment !== null) { if (environment === null) throw new Error(`deploy manifest ${source} must declare environment=${expectedEnvironment} when --env ${expectedEnvironment} is used`); if (environment !== expectedEnvironment) { throw new Error(`deploy manifest ${source} declares environment=${environment}, refusing --env ${expectedEnvironment}`); } } return { schemaVersion: 1, environment, services: parseDeployManifestServices(record.services, "services") }; } export function readEnvironmentDeployManifest(environment: DeployEnvironment): { manifest: DeployManifest; source: DeployEnvironmentManifestSource } { const target = deployEnvironmentTargets[environment]; const remoteRef = `refs/remotes/${target.remote}/${target.branch}`; const fetch = runCommand(["git", "fetch", "--no-tags", target.remote, `+refs/heads/${target.branch}:${remoteRef}`], repoRoot, { timeoutMs: 60_000 }); if (fetch.exitCode !== 0) { throw new Error(`failed to fetch ${target.gitRef} for deploy --env ${environment}: ${commandFailure(fetch)}`); } const commit = parseFullCommit(runGitOrThrow(["rev-parse", "--verify", `${target.gitRef}^{commit}`], repoRoot, `failed to resolve ${target.gitRef}`).stdout); if (commit.length !== 40) throw new Error(`failed to resolve a full commit for ${target.gitRef}`); const blob = runGitOrThrow(["rev-parse", `${target.gitRef}:deploy.json`], repoRoot, `failed to resolve ${target.gitRef}:deploy.json`).stdout.trim().toLowerCase(); if (!/^[0-9a-f]{40}$/iu.test(blob)) throw new Error(`failed to resolve a deploy.json blob for ${target.gitRef}`); const raw = runGitOrThrow(["show", `${target.gitRef}:deploy.json`], repoRoot, `failed to read ${target.gitRef}:deploy.json`).stdout; const manifest = parseDeployManifest(JSON.parse(raw) as unknown, `${target.gitRef}:deploy.json#environments.${environment}`, environment); return { manifest, source: { source: "git-ref", path: "deploy.json", ref: target.gitRef, remote: target.remote, branch: target.branch, commit, blob, fetchedAt: nowIso(), }, }; } export function readProdDeployManifestSnapshot(): DeployManifest { const raw = runGitOrThrow(["show", `${deployEnvironmentTargets.prod.gitRef}:deploy.json`], repoRoot, `failed to read ${deployEnvironmentTargets.prod.gitRef}:deploy.json`).stdout; return parseDeployManifest(JSON.parse(raw) as unknown, `${deployEnvironmentTargets.prod.gitRef}:deploy.json#environments.prod`, "prod"); } export async function readDeployManifest(file: string): Promise { const path = resolve(repoRoot, file); if (!existsSync(path)) throw new Error(`deploy manifest not found: ${path}`); if (/\.(?:mjs|js|cjs)$/iu.test(path)) { const moduleValue = await import(`${pathToFileURL(path).href}?unideskDeployManifest=${Date.now()}`); const moduleRecord = asRecord(moduleValue); const parsed = moduleRecord?.default ?? moduleRecord?.manifest; if (parsed === undefined) throw new Error(`deploy JS manifest must export default or named manifest: ${path}`); return parseDeployManifest(parsed, path, null); } return parseDeployManifest(JSON.parse(readFileSync(path, "utf8")) as unknown, path, null); }