feat: enable dev core deploy apply

This commit is contained in:
Codex
2026-05-18 00:53:11 +00:00
parent bfaea963ad
commit b605c78875
7 changed files with 312 additions and 38 deletions
+257 -20
View File
@@ -131,6 +131,7 @@ const nativeK3sInstallVersion = "v1.34.1+k3s1";
const nativeK3sImage = "rancher/k3s:v1.34.1-k3s1";
const nativeK3sCtrAddress = "/run/k3s/containerd/containerd.sock";
const unideskRepoUrl = "https://github.com/pikasTech/unidesk";
const devApplySupportedServiceIds = new Set(["backend-core", "frontend"]);
const deployEnvironmentTargets: Record<DeployEnvironment, DeployEnvironmentTarget> = {
dev: {
environment: "dev",
@@ -184,7 +185,7 @@ function deployHelp(action: string | undefined = undefined): Record<string, unkn
usage: {
check: "bun scripts/cli.ts deploy check [--file deploy.json | --env dev|prod] [--service id]",
plan: "bun scripts/cli.ts deploy plan [--file deploy.json | --env dev|prod] [--service id]",
apply: "bun scripts/cli.ts deploy apply [--file deploy.json] [--service id] [--dry-run] [--force] [--timeout-ms N] [--run-now]",
apply: "bun scripts/cli.ts deploy apply [--file deploy.json | --env dev] [--service id] [--dry-run] [--force] [--timeout-ms N] [--run-now]",
},
actions: {
check: "Validate desired repo+commit state against live service health and commit markers.",
@@ -193,7 +194,7 @@ function deployHelp(action: string | undefined = undefined): Record<string, unkn
},
options: [
{ name: "--file <path>", default: defaultDeployFile, description: "Desired-state manifest path relative to the repo root. JSON and ESM JS manifests are supported, for example deploy.json or develop.js." },
{ name: "--env <dev|prod>", description: "Read deploy.json from the fixed environment ref: dev=origin/deploy/dev, prod=origin/deploy/prod. Phase 0 supports check/plan only." },
{ name: "--env <dev|prod>", description: "Read deploy.json from the fixed environment ref: dev=origin/deploy/dev, prod=origin/deploy/prod. Apply is currently enabled for supported dev services only." },
{ name: "--service <id>", description: "Limit reconcile to one service from the manifest." },
{ name: "--dry-run", description: "Prepare and validate without mutating the target service." },
{ name: "--force", description: "Redeploy even when the live commit appears up to date." },
@@ -567,7 +568,96 @@ function frontendCoreDeployService(config: UniDeskConfig): UniDeskMicroserviceCo
};
}
function coreDeployService(config: UniDeskConfig, id: string): UniDeskMicroserviceConfig | undefined {
function devCoreDeployService(id: string): UniDeskMicroserviceConfig | undefined {
const specs: Record<string, {
name: string;
description: string;
dockerfile: string;
composeService: string;
containerName: string;
nodeBaseUrl: string;
nodePort: number;
healthPath: string;
route: string;
allowedMethods: string[];
allowedPathPrefixes: string[];
}> = {
"backend-core": {
name: "UniDesk Dev Backend Core",
description: "Isolated dev backend-core deployed into D601 native k3s namespace unidesk-dev.",
dockerfile: "src/components/backend-core/Dockerfile",
composeService: "backend-core-dev",
containerName: "k3s:backend-core-dev",
nodeBaseUrl: "k3s://backend-core-dev",
nodePort: 8080,
healthPath: "/health",
route: "/dev/backend-core",
allowedMethods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"],
allowedPathPrefixes: ["/", "/api/", "/logs"],
},
frontend: {
name: "UniDesk Dev Frontend",
description: "Isolated dev frontend deployed into D601 native k3s namespace unidesk-dev.",
dockerfile: "src/components/frontend/Dockerfile",
composeService: "frontend-dev",
containerName: "k3s:frontend-dev",
nodeBaseUrl: "k3s://frontend-dev",
nodePort: 8080,
healthPath: "/health",
route: "/dev/frontend",
allowedMethods: ["GET", "HEAD"],
allowedPathPrefixes: ["/"],
},
};
const spec = specs[id];
if (spec === undefined) return undefined;
return {
id,
name: spec.name,
providerId: "D601",
description: spec.description,
repository: {
url: unideskRepoUrl,
commitId: "deploy-dev",
dockerfile: spec.dockerfile,
composeFile: "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml",
composeService: spec.composeService,
containerName: spec.containerName,
},
backend: {
nodeBaseUrl: spec.nodeBaseUrl,
nodeBindHost: `k3s://unidesk-dev/${spec.composeService}`,
nodePort: spec.nodePort,
proxyMode: "dev-k3s-direct",
frontendOnly: true,
public: false,
allowedMethods: spec.allowedMethods,
allowedPathPrefixes: spec.allowedPathPrefixes,
healthPath: spec.healthPath,
timeoutMs: 30_000,
},
deployment: {
mode: "k3sctl-managed",
adapterServiceId: "k3sctl-adapter",
k3sServiceId: spec.composeService,
namespace: "unidesk-dev",
expectedNodeIds: ["D601"],
activeNodeId: "D601",
},
development: {
providerId: "D601",
sshPassthrough: true,
worktreePath: `/home/ubuntu/unidesk-dev-core-deploy/${id}`,
},
frontend: {
route: spec.route,
integrated: false,
},
};
}
function coreDeployService(config: UniDeskConfig, id: string, environment: DeployEnvironment | null): UniDeskMicroserviceConfig | undefined {
if (environment === "dev") return devCoreDeployService(id);
if (id === "frontend") return frontendCoreDeployService(config);
return undefined;
}
@@ -576,6 +666,12 @@ function isCoreDeployService(service: UniDeskMicroserviceConfig): boolean {
return service.id === "frontend" && service.providerId === "main-server" && service.backend.proxyMode === "core-direct";
}
function isDevK3sCoreService(service: UniDeskMicroserviceConfig): boolean {
return service.deployment.mode === "k3sctl-managed"
&& service.deployment.namespace === deployEnvironmentTargets.dev.namespace
&& devApplySupportedServiceIds.has(service.id);
}
function isDirectComposeDeployMode(service: UniDeskMicroserviceConfig): boolean {
return service.deployment.mode === "unidesk-direct" || service.deployment.mode === "internal-sidecar";
}
@@ -588,7 +684,14 @@ function selectServices(config: UniDeskConfig, manifest: DeployManifest, service
const selected = serviceId === null ? manifest.services : manifest.services.filter((service) => service.id === serviceId);
if (serviceId !== null && selected.length === 0) throw new Error(`deploy manifest does not contain service: ${serviceId}`);
return selected.map((desired) => {
const service = configById.get(desired.id) ?? coreDeployService(config, desired.id);
if (manifest.environment === "dev") {
const service = devCoreDeployService(desired.id);
if (service === undefined) {
throw new Error(`deploy --env dev service ${desired.id} is not enabled in this executor yet; currently supported: ${[...devApplySupportedServiceIds].join(", ")}`);
}
return { desired, config: service };
}
const service = configById.get(desired.id) ?? coreDeployService(config, desired.id, manifest.environment);
if (service === undefined) throw new Error(`deploy manifest service ${desired.id} is not present in config.json microservices or supported core deploy services`);
return { desired, config: service };
});
@@ -643,6 +746,7 @@ function targetExportDir(service: UniDeskMicroserviceConfig, runId: string): str
}
function targetWorkDir(service: UniDeskMicroserviceConfig): string {
if (isDevK3sCoreService(service)) return service.development.worktreePath;
if (service.deployment.mode === "k3sctl-managed") return k3sDeployDir;
if (targetIsMain(service) && isUnideskRepo(service.repository.url)) {
return rootPath(".state", "deploy", "work", safeId(service.id));
@@ -676,6 +780,7 @@ function sourceBuildContext(service: UniDeskMicroserviceConfig): string {
}
function buildImageTag(service: UniDeskMicroserviceConfig): string {
if (isDevK3sCoreService(service)) return `unidesk-${service.id}:dev`;
if (service.deployment.mode === "k3sctl-managed") return `unidesk-${service.id}:d601`;
if (targetIsMain(service)) {
if (["project-manager", "baidu-netdisk", "oa-event-flow"].includes(service.repository.composeService)) return service.repository.composeService;
@@ -706,6 +811,7 @@ function directDockerfileOverride(service: UniDeskMicroserviceConfig): string {
function k8sManifestPath(service: UniDeskMicroserviceConfig): string {
const composeFile = service.repository.composeFile;
if (composeFile.endsWith(".k8s.yaml")) return composeFile;
if (!composeFile.endsWith(".k3s.json")) throw new Error(`${service.id} k3s service composeFile must point to *.k3s.json`);
return composeFile.replace(/\.k3s\.json$/u, ".k8s.yaml");
}
@@ -916,8 +1022,24 @@ function claudeqqDeployAssetOverlayCommands(): string[] {
}
function syncK8sControlManifestsScript(service: UniDeskMicroserviceConfig): string {
if (isDevK3sCoreService(service)) {
const manifest = k8sManifestPath(service);
if (!existsSync(rootPath(manifest))) throw new Error(`${service.id} dev k3s control manifest missing: ${manifest}`);
const encoded = Buffer.from(readFileSync(rootPath(manifest), "utf8"), "utf8").toString("base64");
return [
"set -euo pipefail",
`target_root=${shellQuote(targetWorkDir(service))}`,
`relative_path=${shellQuote(manifest)}`,
"target_file=\"$target_root/$relative_path\"",
"mkdir -p \"$(dirname \"$target_file\")\"",
`printf %s ${shellQuote(encoded)} | base64 -d > "$target_file"`,
"printf 'synced_k3s_control_manifest=%s\\n' \"$target_file\"",
].join("\n");
}
if (service.deployment.mode !== "k3sctl-managed" || isUnideskRepo(service.repository.url)) return "";
const manifests = [service.repository.composeFile, k8sManifestPath(service)];
const manifests = service.repository.composeFile.endsWith(".k8s.yaml")
? [service.repository.composeFile]
: [service.repository.composeFile, k8sManifestPath(service)];
const commands = [
"set -euo pipefail",
`target_root=${shellQuote(targetWorkDir(service))}`,
@@ -970,7 +1092,14 @@ function buildImageScript(service: UniDeskMicroserviceConfig, desired: DeployMan
sourceProxyPrelude(service),
`image=${shellQuote(image)}`,
`dockerfile=${shellQuote(dockerfile)}`,
"docker buildx version >/dev/null",
"if ! docker buildx version >/dev/null 2>&1; then",
" if ! docker buildx inspect default >/dev/null 2>&1; then echo target_build_builder=missing >&2; exit 1; fi",
"fi",
...(isDevK3sCoreService(service) ? [
"if ! docker image inspect oven/bun:1-alpine >/dev/null 2>&1; then",
" docker pull oven/bun:1-alpine",
"fi",
] : []),
"builder_args=()",
"if docker buildx inspect --builder default >/dev/null 2>&1; then builder_args=(--builder default); echo target_build_builder=default; else echo target_build_builder=implicit; fi",
"docker buildx inspect \"${builder_args[@]}\" --bootstrap || true",
@@ -981,6 +1110,54 @@ function buildImageScript(service: UniDeskMicroserviceConfig, desired: DeployMan
].filter((line) => line.length > 0).join("\n");
}
function patchDevK3sCoreManifestScript(service: UniDeskMicroserviceConfig, desired: DeployManifestService, resolvedCommit: string): string {
const manifest = `${targetWorkDir(service)}/${k8sManifestPath(service)}`;
const image = buildImageTag(service);
const serviceId = service.id;
const deploymentName = service.repository.composeService;
const manifestServiceId = deploymentName;
return [
"set -euo pipefail",
`manifest=${shellQuote(manifest)}`,
`service_id=${shellQuote(serviceId)}`,
`deployment_name=${shellQuote(deploymentName)}`,
`image=${shellQuote(image)}`,
`repo=${shellQuote(desired.repo)}`,
`commit=${shellQuote(resolvedCommit)}`,
`requested_commit=${shellQuote(desired.commitId)}`,
`manifest_service_id=${shellQuote(manifestServiceId)}`,
`python3 - "$manifest" "$service_id" "$deployment_name" "$image" "$repo" "$commit" "$requested_commit" "$manifest_service_id" <<'PY'`,
"import re",
"import sys",
"path, service_id, deployment_name, image, repo, commit, requested_commit, manifest_service_id = sys.argv[1:]",
"text = open(path, encoding='utf-8').read()",
"segments = re.split(r'(?m)^---\\s*$', text)",
"kept = []",
"for segment in segments:",
" if not segment.strip():",
" continue",
" if f'\\n name: {deployment_name}\\n' in ('\\n' + segment + '\\n'):",
" kept.append(segment)",
"if not kept:",
" raise SystemExit(f'deployment/service {deployment_name} not found in {path}')",
"patched = []",
"for segment in kept:",
" segment = segment.replace('unidesk.ai/image-source: deploy-dev-commit', 'unidesk.ai/image-source: deploy-env-commit')",
" segment = re.sub(r'image: unidesk-[^\\n]+:dev-placeholder', f'image: {image}', segment)",
" segment = re.sub(r'value: replace-with-deploy-dev-commit', f'value: {commit}', segment)",
" segment = segment.replace(f'value: {manifest_service_id}', f'value: {service_id}')",
" patched.append(segment.strip() + '\\n')",
"out = '---\\n'.join(patched)",
"open(path, 'w', encoding='utf-8').write(out)",
"print(f'dev_k3s_manifest_patched={path}')",
"print(f'dev_k3s_manifest_service={service_id}')",
"print(f'dev_k3s_manifest_deployment={deployment_name}')",
"print(f'dev_k3s_manifest_image={image}')",
"print(f'dev_k3s_manifest_commit={commit}')",
"PY",
].join("\n");
}
function directComposeResolveScript(service: UniDeskMicroserviceConfig): string {
const projectHint = targetIsMain(service) ? "unidesk" : "";
return [
@@ -1322,6 +1499,10 @@ function cleanupLegacyDirectCodeQueueScript(service: UniDeskMicroserviceConfig):
].join("\n");
}
function k8sNamespaceForService(service: UniDeskMicroserviceConfig): string {
return service.deployment.namespace ?? k8sNamespace;
}
function k8sDeploymentsForService(service: UniDeskMicroserviceConfig): string[] {
if (service.id === "code-queue") return ["d601-provider-egress-proxy", "d601-tcp-egress-gateway", "code-queue", "code-queue-read", "code-queue-write"];
return [service.repository.composeService];
@@ -1329,11 +1510,12 @@ function k8sDeploymentsForService(service: UniDeskMicroserviceConfig): string[]
function applyK8sScript(service: UniDeskMicroserviceConfig): string {
const manifest = `${targetWorkDir(service)}/${k8sManifestPath(service)}`;
const namespace = k8sNamespaceForService(service);
const cleanup = service.id === "code-queue"
? [
`KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n ${shellQuote(k8sNamespace)} delete endpointslice d601-provider-egress-proxy --ignore-not-found`,
`KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n ${shellQuote(k8sNamespace)} delete deployment code-queue-d518 --ignore-not-found`,
`KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n ${shellQuote(k8sNamespace)} delete service code-queue-d518 --ignore-not-found`,
`KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n ${shellQuote(namespace)} delete endpointslice d601-provider-egress-proxy --ignore-not-found`,
`KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n ${shellQuote(namespace)} delete deployment code-queue-d518 --ignore-not-found`,
`KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n ${shellQuote(namespace)} delete service code-queue-d518 --ignore-not-found`,
].join("\n")
: "";
return [
@@ -1344,6 +1526,7 @@ function applyK8sScript(service: UniDeskMicroserviceConfig): string {
function stampK8sScript(service: UniDeskMicroserviceConfig, desired: DeployManifestService, resolvedCommit: string): string {
const deployments = k8sDeploymentsForService(service).map((name) => `deployment/${name}`);
const namespace = k8sNamespaceForService(service);
const envPairs = [
`UNIDESK_DEPLOY_SERVICE_ID=${service.id}`,
`UNIDESK_DEPLOY_REPO=${desired.repo}`,
@@ -1362,9 +1545,9 @@ function stampK8sScript(service: UniDeskMicroserviceConfig, desired: DeployManif
];
return [
"set -euo pipefail",
`KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n ${shellQuote(k8sNamespace)} set env ${deployments.map(shellQuote).join(" ")} ${envPairs.map(shellQuote).join(" ")}`,
`KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n ${shellQuote(k8sNamespace)} annotate ${deployments.map(shellQuote).join(" ")} ${annotatePairs.map(shellQuote).join(" ")} --overwrite`,
`actual=$(KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n ${shellQuote(k8sNamespace)} get deployment ${shellQuote(service.repository.composeService)} -o jsonpath='{.metadata.annotations.unidesk\\.ai/deploy-commit}')`,
`KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n ${shellQuote(namespace)} set env ${deployments.map(shellQuote).join(" ")} ${envPairs.map(shellQuote).join(" ")}`,
`KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n ${shellQuote(namespace)} annotate ${deployments.map(shellQuote).join(" ")} ${annotatePairs.map(shellQuote).join(" ")} --overwrite`,
`actual=$(KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n ${shellQuote(namespace)} get deployment ${shellQuote(service.repository.composeService)} -o jsonpath='{.metadata.annotations.unidesk\\.ai/deploy-commit}')`,
`test "$actual" = ${shellQuote(resolvedCommit)}`,
"printf 'k8s_deploy_commit=%s\\n' \"$actual\"",
].join("\n");
@@ -1372,18 +1555,20 @@ function stampK8sScript(service: UniDeskMicroserviceConfig, desired: DeployManif
function rolloutK8sScript(service: UniDeskMicroserviceConfig): string {
const deployments = k8sDeploymentsForService(service);
const namespace = k8sNamespaceForService(service);
return [
"set -euo pipefail",
`KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n ${shellQuote(k8sNamespace)} rollout restart ${deployments.map((name) => shellQuote(`deployment/${name}`)).join(" ")}`,
...deployments.map((name) => `KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n ${shellQuote(k8sNamespace)} rollout status ${shellQuote(`deployment/${name}`)} --timeout=180s`),
`KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n ${shellQuote(k8sNamespace)} get deploy ${deployments.map(shellQuote).join(" ")} -o wide`,
`KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n ${shellQuote(namespace)} rollout restart ${deployments.map((name) => shellQuote(`deployment/${name}`)).join(" ")}`,
...deployments.map((name) => `KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n ${shellQuote(namespace)} rollout status ${shellQuote(`deployment/${name}`)} --timeout=180s`),
`KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n ${shellQuote(namespace)} get deploy ${deployments.map(shellQuote).join(" ")} -o wide`,
].join("\n");
}
function k8sCommitProbeScript(service: UniDeskMicroserviceConfig): string {
const namespace = k8sNamespaceForService(service);
return [
"set -euo pipefail",
`commit=$(KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n ${shellQuote(k8sNamespace)} get deployment ${shellQuote(service.repository.composeService)} -o jsonpath='{.metadata.annotations.unidesk\\.ai/deploy-commit}' 2>/dev/null || true)`,
`commit=$(KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n ${shellQuote(namespace)} get deployment ${shellQuote(service.repository.composeService)} -o jsonpath='{.metadata.annotations.unidesk\\.ai/deploy-commit}' 2>/dev/null || true)`,
"printf '%s\\n' \"$commit\"",
].join("\n");
}
@@ -1444,10 +1629,41 @@ async function directHttpJson(url: string, timeoutMs: number): Promise<unknown>
}
}
function devK3sServiceHealthScript(service: UniDeskMicroserviceConfig): string {
const namespace = k8sNamespaceForService(service);
const deployment = service.repository.composeService;
const path = service.backend.healthPath;
return [
"set -euo pipefail",
`namespace=${shellQuote(namespace)}`,
`deployment=${shellQuote(deployment)}`,
`path=${shellQuote(path)}`,
`pod=$(KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n "$namespace" get pod -l app.kubernetes.io/name=${shellQuote(service.id)},unidesk.ai/environment=dev -o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)`,
"if [ -z \"$pod\" ]; then echo '{\"ok\":false,\"error\":\"dev deployment pod not found\"}'; exit 0; fi",
`KUBECONFIG=${shellQuote(k8sKubeconfig)} kubectl -n "$namespace" exec "$pod" -- wget -q -T 5 -O - "http://127.0.0.1:8080$path"`,
].join("\n");
}
async function serviceHealth(config: UniDeskConfig, service: UniDeskMicroserviceConfig): Promise<unknown> {
if (isCoreDeployService(service)) {
return await directHttpJson(`${service.backend.nodeBaseUrl}${service.backend.healthPath}`, service.backend.timeoutMs);
}
if (isDevK3sCoreService(service)) {
const result = await runTargetCommand(config, service, devK3sServiceHealthScript(service), "/home/ubuntu", 30_000, 20_000);
let body: unknown = null;
try {
body = result.stdout.trim().length > 0 ? JSON.parse(result.stdout.trim()) : null;
} catch {
body = { text: result.stdout.trim() };
}
return {
ok: result.ok,
status: result.ok ? 200 : 502,
body,
raw: result.raw,
error: result.ok ? undefined : result.stderr || result.stdout || "dev k3s service health failed",
};
}
return coreInternalFetch(`/api/microservices/${encodeURIComponent(service.id)}/health`);
}
@@ -1699,6 +1915,7 @@ async function step(
}
async function readDockerImageCommit(config: UniDeskConfig, service: UniDeskMicroserviceConfig): Promise<string | null> {
if (isDevK3sCoreService(service)) return null;
const result = await runTargetCommand(config, service, dockerCommitProbeScript(service), targetIsMain(service) ? repoRoot : "/home/ubuntu", 30_000, 20_000);
const commit = parseFullCommit(result.stdout);
return commit.length > 0 ? commit : null;
@@ -1824,7 +2041,7 @@ async function applyOneService(config: UniDeskConfig, service: UniDeskMicroservi
const runId = `${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
const exportDir = targetExportDir(service, runId);
if (service.id === "code-queue" && !targetIsMain(service)) {
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 };
}
@@ -1846,6 +2063,11 @@ async function applyOneService(config: UniDeskConfig, service: UniDeskMicroservi
if (!pushStep(steps, controlManifestSync)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
}
if (isDevK3sCoreService(service)) {
const patchManifest = await step(config, service, "patch-dev-k3s-manifest", patchDevK3sCoreManifestScript(service, desired, resolvedCommit), targetWorkDir(service), 60_000, false);
if (!pushStep(steps, patchManifest)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
}
const buildScript = isDirectComposeDeployMode(service)
? buildDirectImageScript(service, desired, resolvedCommit)
: buildImageScript(service, desired, resolvedCommit);
@@ -1965,6 +2187,12 @@ function environmentDryRunPlan(
};
}
function unsupportedDevApplyServices(manifest: DeployManifest, serviceId: string | null): string[] {
if (manifest.environment !== "dev") return [];
const services = serviceId === null ? manifest.services : manifest.services.filter((service) => service.id === serviceId);
return services.map((service) => service.id).filter((id) => !devApplySupportedServiceIds.has(id));
}
async function runApplyNow(config: UniDeskConfig, manifest: DeployManifest, options: DeployOptions): Promise<Record<string, unknown>> {
const selected = selectServices(config, manifest, options.serviceId);
const startedAt = nowIso();
@@ -1989,7 +2217,8 @@ async function runApplyNow(config: UniDeskConfig, manifest: DeployManifest, opti
function applyJob(config: UniDeskConfig, args: string[], options: DeployOptions): Record<string, unknown> {
const runArgs = args.includes("--run-now") ? args : [...args, "--run-now"];
const command = [process.execPath, rootPath("scripts", "cli.ts"), "deploy", ...runArgs];
const job = startJob("deploy_apply", command, `Reconcile services from ${options.file}${options.serviceId === null ? "" : ` service=${options.serviceId}`}`);
const source = options.environment === null ? options.file : `${deployEnvironmentTargets[options.environment].gitRef}:deploy.json`;
const job = startJob("deploy_apply", command, `Reconcile services from ${source}${options.serviceId === null ? "" : ` service=${options.serviceId}`}`);
return {
ok: true,
mode: "async-job",
@@ -2008,9 +2237,17 @@ export async function runDeployCommand(config: UniDeskConfig | null, args: strin
const action = actionRaw as DeployAction;
const options = parseOptions(args.slice(1));
if (options.environment !== null) {
if (action === "apply") throw new Error("deploy apply --env is not enabled in Phase 0; use deploy plan --env dev|prod for dry-run only");
const { manifest, source } = readEnvironmentDeployManifest(options.environment);
return environmentDryRunPlan(manifest, source, options, action);
if (action === "check" || action === "plan") return environmentDryRunPlan(manifest, source, options, action);
if (options.environment !== "dev") throw new Error("deploy apply --env prod is not enabled yet");
const unsupported = unsupportedDevApplyServices(manifest, options.serviceId);
if (unsupported.length > 0) {
throw new Error(`deploy apply --env dev currently supports only ${[...devApplySupportedServiceIds].join(", ")}; unsupported selected services: ${unsupported.join(", ")}`);
}
if (config === null) throw new Error("deploy apply --env dev requires config.json");
const resolved = resolveManifestCommits(manifest, options.serviceId);
if (!options.runNow) return applyJob(config, args, options);
return await runApplyNow(config, resolved, options);
}
if (config === null) throw new Error("deploy local manifest mode requires config.json");
const manifest = resolveManifestCommits(await readDeployManifest(options.file), options.serviceId);