Files
pikasTech-unidesk/scripts/src/deploy/apply-service.ts
T
2026-06-25 16:16:25 +00:00

230 lines
13 KiB
TypeScript

// 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<StepResult> {
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<Record<string, unknown>> {
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<Record<string, unknown>> {
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,
};
}