// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. apply-service module for scripts/src/deploy.ts. // Moved mechanically from scripts/src/deploy.ts:2840-3031 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 { d601K3sGuardShellLines, d601NativeKubeconfig } from "../d601-k3s-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 { DeployManifestService, DeployOptions, ServiceRuntimeState, StepResult } from "./types"; import { applyK8sScript, cleanupLegacyDirectCodeQueueScript, rolloutK8sScript, stampK8sScript, verifyK8sImagesScript } from "./health"; import { asRecord, compactTail, elapsedMs, isUnideskRepo, nowIso, parseFullCommit, progressLine } from "./options"; import { dockerBuildTimeoutMs, targetExportDir, targetIsMain, targetWorkDir } from "./paths"; import { healthVerify, manifestEnvironmentForService, pushStep, readRuntimeState, step } from "./remote"; import { buildDirectImageScript, buildImageScript, codeQueueSourcePreflightScript, composeDeployScript, ensureNativeK3sScript, imageLabelVerifyScript, importK3sImageScript, patchDevK3sManifestScript, prepareSourceScript, syncDevFrontendAuthScript, syncK8sControlManifestsScript, syncSourceScript } from "./scripts"; import { isD601MaintenanceDeployBlocked, isDevArtifactConsumerService, isDevK3sDeployService, isDirectComposeDeployMode, unsupportedReason } from "./service-plan"; export async function ensureGithubSshIdentityStep(config: UniDeskConfig, service: UniDeskMicroserviceConfig): Promise { const startedAt = nowIso(); const startedMs = Date.now(); progressLine("github-ssh-identity", "start", { serviceId: service.id, providerId: service.providerId }); try { const result = await ensureGithubSshIdentityForProvider(config, service.providerId); progressLine("github-ssh-identity", result.ok ? "succeeded" : "failed", { serviceId: service.id, providerId: service.providerId, elapsedMs: elapsedMs(startedMs), fingerprint: result.fingerprint, seededFromLocal: result.seededFromLocal, detail: result.ok ? result.detail : compactTail(result.detail, 1200), }); return { step: "github-ssh-identity", ok: result.ok, detail: result.detail, startedAt, finishedAt: nowIso(), raw: result.raw, }; } catch (error) { const detail = error instanceof Error ? error.message : String(error); progressLine("github-ssh-identity", "failed", { serviceId: service.id, providerId: service.providerId, elapsedMs: elapsedMs(startedMs), detail: compactTail(detail, 1200), }); return { step: "github-ssh-identity", ok: false, detail, startedAt, finishedAt: nowIso(), raw: null }; } } export async function runDevArtifactConsumerService( service: UniDeskMicroserviceConfig, desired: DeployManifestService, options: DeployOptions, before: ServiceRuntimeState, ): Promise> { const commit = options.commitOverride ?? desired.commitId; const artifactArgs = [ "deploy-service", "--env", "dev", "--service", service.id, "--commit", commit, "--source-repo", desired.repo, "--timeout-ms", String(options.timeoutMs), "--provider-id", options.providerId, "--run-now", ...(options.dryRun ? ["--dry-run"] : []), ]; progressLine("dev-artifact-cd", options.dryRun ? "dry-run" : "reconcile", { serviceId: service.id, commit }); const result = await runArtifactRegistryCommand(artifactArgs); const ok = asRecord(result)?.ok === true; return { ok, action: ok ? "deployed" : "failed", environment: "dev", executor: "d601-registry-artifact-consumer", resolvedCommit: commit, before, after: result, steps: [{ step: "artifact-registry-consumer", ok, detail: JSON.stringify(result), startedAt: nowIso(), finishedAt: nowIso(), raw: result, }], }; } export async function applyOneService(config: UniDeskConfig, service: UniDeskMicroserviceConfig, desired: DeployManifestService, options: DeployOptions): Promise> { const steps: StepResult[] = []; const startedAt = nowIso(); const reason = unsupportedReason(service); if (reason !== null) return { ok: false, serviceId: service.id, skipped: true, reason, steps }; const before = await readRuntimeState(config, service, desired); if (!options.force && before.upToDate) return { ok: true, serviceId: service.id, action: "noop", before, steps }; if (manifestEnvironmentForService(service) === "dev" && isDevArtifactConsumerService(service)) { const artifact = await runDevArtifactConsumerService(service, desired, options, before); return { ...artifact, before, serviceId: service.id, startedAt, finishedAt: nowIso(), }; } if (options.dryRun) return { ok: true, serviceId: service.id, action: "would-deploy", before, steps }; if (!options.dryRun && isD601MaintenanceDeployBlocked(service)) { return { ok: false, serviceId: service.id, skipped: true, reason: `D601 target-side deployment is allowed only for k3sctl-adapter as a control-bridge maintenance path; artifact consumers must use registry CD. ${service.id} is not enabled. Use ci run-dev-e2e for smoke verification.`, steps, }; } const runId = `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`; const exportDir = targetExportDir(service, runId); if (!targetIsMain(service) && isUnideskRepo(desired.repo)) { const identity = await ensureGithubSshIdentityStep(config, service); if (!pushStep(steps, identity)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), before, steps }; } const prepare = await step(config, service, "prepare-source", prepareSourceScript(service, desired, exportDir), targetIsMain(service) ? repoRoot : "/home/ubuntu", Math.min(options.timeoutMs, 180_000), !targetIsMain(service)); if (!pushStep(steps, prepare)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), before, steps }; const resolvedCommit = parseFullCommit(prepare.detail) || parseFullCommit(JSON.stringify(prepare.raw)); if (resolvedCommit.length !== 40) { const failure = { step: "resolve-commit", ok: false, detail: "prepare-source did not expose a full 40-char commit", startedAt: nowIso(), finishedAt: nowIso(), raw: prepare }; steps.push(failure); return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), before, steps }; } const sync = await step(config, service, "sync-source", syncSourceScript(service, exportDir), targetIsMain(service) ? repoRoot : "/home/ubuntu", 90_000, !targetIsMain(service)); if (!pushStep(steps, sync)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps }; const codeQueueSourcePreflight = codeQueueSourcePreflightScript(service); if (codeQueueSourcePreflight.length > 0) { const sourcePreflight = await step(config, service, "code-queue-hostpath-source-preflight", codeQueueSourcePreflight, targetWorkDir(service), 60_000, !targetIsMain(service)); if (!pushStep(steps, sourcePreflight)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps }; } const controlManifestSyncScript = syncK8sControlManifestsScript(service); if (controlManifestSyncScript.length > 0) { const controlManifestStep = isDevK3sDeployService(service) ? "verify-target-k3s-manifest" : "sync-k3s-control-manifests"; const controlManifestSync = await step(config, service, controlManifestStep, controlManifestSyncScript, targetIsMain(service) ? repoRoot : "/home/ubuntu", 90_000, !targetIsMain(service)); if (!pushStep(steps, controlManifestSync)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps }; } if (isDevK3sDeployService(service)) { const patchManifest = await step(config, service, "patch-dev-k3s-manifest", patchDevK3sManifestScript(service, desired, resolvedCommit), targetWorkDir(service), 60_000, false); if (!pushStep(steps, patchManifest)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps }; } if (isDevK3sDeployService(service) && service.id === "frontend") { const authSync = await step(config, service, "sync-dev-frontend-auth", syncDevFrontendAuthScript(config), "/home/ubuntu", 60_000, false); if (!pushStep(steps, authSync)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps }; } const buildScript = isDirectComposeDeployMode(service) ? buildDirectImageScript(service, desired, resolvedCommit) : buildImageScript(service, desired, resolvedCommit); const build = await step(config, service, "docker-build", buildScript, targetWorkDir(service), dockerBuildTimeoutMs(service, options), !targetIsMain(service)); if (!pushStep(steps, build)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps }; if (service.deployment.mode === "k3sctl-managed") { const nativeK3s = await step(config, service, "ensure-native-k3s", ensureNativeK3sScript(), "/home/ubuntu", 600_000, true); if (!pushStep(steps, nativeK3s)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps }; const imageImport = await step(config, service, "import-k3s-image", importK3sImageScript(service), targetWorkDir(service), Math.min(options.timeoutMs, 900_000), true); if (!pushStep(steps, imageImport)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps }; const apply = await step(config, service, "kubectl-apply", applyK8sScript(service), targetWorkDir(service), 60_000, false); if (!pushStep(steps, apply)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps }; const k8sImages = await step(config, service, "k8s-image-preflight", verifyK8sImagesScript(service), targetWorkDir(service), 60_000, false); if (!pushStep(steps, k8sImages)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps }; const stamp = await step(config, service, "stamp-deploy-commit", stampK8sScript(service, desired, resolvedCommit), targetWorkDir(service), 60_000, false); if (!pushStep(steps, stamp)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps }; const rollout = await step(config, service, "rollout", rolloutK8sScript(service), targetWorkDir(service), 240_000, true); if (!pushStep(steps, rollout)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps }; const legacyCleanupScript = cleanupLegacyDirectCodeQueueScript(service); if (legacyCleanupScript.length > 0) { const cleanup = await step(config, service, "legacy-direct-cleanup", legacyCleanupScript, targetWorkDir(service), 90_000, false); if (!pushStep(steps, cleanup)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps }; } } else { const deploy = await step(config, service, "compose-up", composeDeployScript(service, desired, resolvedCommit), targetIsMain(service) ? repoRoot : targetWorkDir(service), 180_000, !targetIsMain(service)); if (!pushStep(steps, deploy)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps }; const imageVerify = await step(config, service, "image-label-verify", imageLabelVerifyScript(service, resolvedCommit), targetIsMain(service) ? repoRoot : targetWorkDir(service), 60_000, false); if (!pushStep(steps, imageVerify)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps }; } const health = await healthVerify(config, service, desired, resolvedCommit, 90_000); steps.push(health); return { ok: health.ok, serviceId: service.id, action: "deployed", startedAt, finishedAt: nowIso(), resolvedCommit, before, after: health.raw, steps, }; }