feat: add frontend artifact delivery path
This commit is contained in:
@@ -1,13 +1,15 @@
|
||||
import { createHash } from "node:crypto";
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { runCommand, type CommandResult } from "./command";
|
||||
import { readConfig, repoRoot, rootPath } from "./config";
|
||||
import { readConfig, type UniDeskConfig, repoRoot, rootPath } from "./config";
|
||||
import { resolveComposeCommand, writeComposeEnv } from "./docker";
|
||||
import { startJob } from "./jobs";
|
||||
|
||||
type ArtifactRegistryAction = "plan" | "render" | "status" | "health" | "install" | "deploy-backend-core" | "deploy-service";
|
||||
type ArtifactDeployEnvironment = "prod" | "dev";
|
||||
|
||||
interface ArtifactRegistryOptions {
|
||||
environment: ArtifactDeployEnvironment | null;
|
||||
providerId: string;
|
||||
host: string;
|
||||
port: number;
|
||||
@@ -47,6 +49,7 @@ interface RenderedBundle {
|
||||
}
|
||||
|
||||
const defaultOptions: ArtifactRegistryOptions = {
|
||||
environment: null,
|
||||
providerId: "D601",
|
||||
host: "127.0.0.1",
|
||||
port: 5000,
|
||||
@@ -66,11 +69,12 @@ const defaultOptions: ArtifactRegistryOptions = {
|
||||
sourceRepo: "https://github.com/pikasTech/unidesk",
|
||||
deployRef: null,
|
||||
};
|
||||
const supportedArtifactConsumerServices = ["backend-core", "baidu-netdisk", "decision-center"] as const;
|
||||
const supportedArtifactConsumerServices = ["backend-core", "baidu-netdisk", "decision-center", "frontend"] as const;
|
||||
type SupportedArtifactConsumerService = typeof supportedArtifactConsumerServices[number];
|
||||
|
||||
interface ArtifactConsumerSpec {
|
||||
serviceId: SupportedArtifactConsumerService;
|
||||
environment?: ArtifactDeployEnvironment;
|
||||
kind: "compose" | "d601-k3s";
|
||||
registryRepository: string;
|
||||
dockerfile: string;
|
||||
@@ -92,12 +96,15 @@ interface ArtifactConsumerSpec {
|
||||
servicePort: number;
|
||||
containerName: string;
|
||||
healthPath: string;
|
||||
applySelector?: string;
|
||||
podLabelSelector?: string;
|
||||
};
|
||||
}
|
||||
|
||||
const artifactConsumerSpecs: Record<SupportedArtifactConsumerService, ArtifactConsumerSpec> = {
|
||||
const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
|
||||
"backend-core": {
|
||||
serviceId: "backend-core",
|
||||
environment: "prod",
|
||||
kind: "compose",
|
||||
registryRepository: "unidesk/backend-core",
|
||||
dockerfile: "src/components/backend-core/Dockerfile",
|
||||
@@ -114,6 +121,7 @@ const artifactConsumerSpecs: Record<SupportedArtifactConsumerService, ArtifactCo
|
||||
},
|
||||
"baidu-netdisk": {
|
||||
serviceId: "baidu-netdisk",
|
||||
environment: "prod",
|
||||
kind: "compose",
|
||||
registryRepository: "unidesk/baidu-netdisk",
|
||||
dockerfile: "src/components/microservices/baidu-netdisk/Dockerfile",
|
||||
@@ -130,6 +138,7 @@ const artifactConsumerSpecs: Record<SupportedArtifactConsumerService, ArtifactCo
|
||||
},
|
||||
"decision-center": {
|
||||
serviceId: "decision-center",
|
||||
environment: "prod",
|
||||
kind: "d601-k3s",
|
||||
registryRepository: "unidesk/decision-center",
|
||||
dockerfile: "src/components/microservices/decision-center/Dockerfile",
|
||||
@@ -146,6 +155,44 @@ const artifactConsumerSpecs: Record<SupportedArtifactConsumerService, ArtifactCo
|
||||
healthPath: "/health",
|
||||
},
|
||||
},
|
||||
"frontend": {
|
||||
serviceId: "frontend",
|
||||
environment: "prod",
|
||||
kind: "compose",
|
||||
registryRepository: "unidesk/frontend",
|
||||
dockerfile: "src/components/frontend/Dockerfile",
|
||||
targetImage: "unidesk-frontend",
|
||||
targetCommitImage: (commit: string) => `unidesk-frontend:${commit}`,
|
||||
deployRef: "deploy.json#environments.prod.services.frontend",
|
||||
compose: {
|
||||
serviceName: "frontend",
|
||||
containerName: "unidesk-frontend",
|
||||
deployEnvPrefix: "UNIDESK_FRONTEND_DEPLOY",
|
||||
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:8080/health').then(async r=>{const text=await r.text(); console.log(text); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"",
|
||||
requireHealthCommit: true,
|
||||
},
|
||||
},
|
||||
"dev:frontend": {
|
||||
serviceId: "frontend",
|
||||
environment: "dev",
|
||||
kind: "d601-k3s",
|
||||
registryRepository: "unidesk/frontend",
|
||||
dockerfile: "src/components/frontend/Dockerfile",
|
||||
targetImage: "unidesk-frontend:dev",
|
||||
targetCommitImage: (commit: string) => `unidesk-frontend:${commit}`,
|
||||
deployRef: "origin/master:deploy.json#environments.dev.services.frontend",
|
||||
k3s: {
|
||||
namespace: "unidesk-dev",
|
||||
manifestPath: "/home/ubuntu/cq-deploy/src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml",
|
||||
deploymentName: "frontend-dev",
|
||||
serviceName: "frontend-dev",
|
||||
servicePort: 8080,
|
||||
containerName: "frontend",
|
||||
healthPath: "/health",
|
||||
applySelector: "app.kubernetes.io/name=frontend",
|
||||
podLabelSelector: "app.kubernetes.io/name=frontend,app.kubernetes.io/component=web,unidesk.ai/environment=dev",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
function isHelpArg(value: string | undefined): boolean {
|
||||
@@ -176,6 +223,11 @@ function commitValue(value: string, option: string): string {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function environmentValue(value: string, option: string): ArtifactDeployEnvironment {
|
||||
if (value === "prod" || value === "dev") return value;
|
||||
throw new Error(`${option} must be one of: prod, dev`);
|
||||
}
|
||||
|
||||
function parseOptions(args: string[]): ArtifactRegistryOptions {
|
||||
const options = { ...defaultOptions };
|
||||
for (let index = 0; index < args.length; index += 1) {
|
||||
@@ -220,6 +272,9 @@ function parseOptions(args: string[]): ArtifactRegistryOptions {
|
||||
} else if (arg === "--deploy-ref") {
|
||||
options.deployRef = requireValue(args, index, arg);
|
||||
index += 1;
|
||||
} else if (arg === "--env" || arg === "--environment") {
|
||||
options.environment = environmentValue(requireValue(args, index, arg), arg);
|
||||
index += 1;
|
||||
} else {
|
||||
throw new Error(`unknown artifact-registry option: ${arg}`);
|
||||
}
|
||||
@@ -237,6 +292,10 @@ function shellQuote(value: string): string {
|
||||
return `'${value.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function base64(value: string): string {
|
||||
return Buffer.from(value, "utf8").toString("base64");
|
||||
}
|
||||
|
||||
function safeName(value: string): string {
|
||||
return value.replace(/[^A-Za-z0-9_.-]/gu, "-");
|
||||
}
|
||||
@@ -367,7 +426,7 @@ function plan(options: ArtifactRegistryOptions): Record<string, unknown> {
|
||||
"listen only on D601 host loopback 127.0.0.1:5000",
|
||||
"do not expose a public port, NodePort, hostPort, or third-party registry",
|
||||
"do not run inside k3s; keep the registry outside the native k3s failure domain",
|
||||
"CI builds and publishes backend-core and reviewed user-service artifacts on D601",
|
||||
"CI builds and publishes backend-core, frontend, and reviewed user-service artifacts on D601",
|
||||
"CD only pulls, retags, recreates/imports artifacts, and verifies live commit",
|
||||
"master server CD must not compile Rust or run docker compose build for artifact consumers",
|
||||
],
|
||||
@@ -379,6 +438,11 @@ function plan(options: ArtifactRegistryOptions): Record<string, unknown> {
|
||||
"k3s consumers pull on D601 and import into native k3s containerd",
|
||||
"CD retags/recreates or updates Deployment images and verifies live commit metadata",
|
||||
],
|
||||
frontendArtifactFlow: [
|
||||
"D601 CI builds and pushes 127.0.0.1:5000/unidesk/frontend:<commit>",
|
||||
"dev CD imports the image into D601 native k3s and verifies frontend-dev /health deploy.commit",
|
||||
"prod CD pulls via provider-gateway image stream, recreates Compose frontend with no build, and verifies /health deploy.commit",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -490,8 +554,17 @@ function commandTail(result: CommandResult): Record<string, unknown> {
|
||||
};
|
||||
}
|
||||
|
||||
function artifactConsumerSpec(serviceId: string): ArtifactConsumerSpec | null {
|
||||
return (artifactConsumerSpecs as Record<string, ArtifactConsumerSpec | undefined>)[serviceId] ?? null;
|
||||
function artifactConsumerSpec(serviceId: string, environment: ArtifactDeployEnvironment | null): ArtifactConsumerSpec | null {
|
||||
const key = environment === null || environment === "prod" ? serviceId : `${environment}:${serviceId}`;
|
||||
return artifactConsumerSpecs[key] ?? null;
|
||||
}
|
||||
|
||||
function supportedArtifactConsumers(): Array<{ environment: ArtifactDeployEnvironment; serviceId: SupportedArtifactConsumerService; kind: ArtifactConsumerSpec["kind"] }> {
|
||||
return Object.values(artifactConsumerSpecs).map((spec) => ({
|
||||
environment: spec.environment ?? "prod",
|
||||
serviceId: spec.serviceId,
|
||||
kind: spec.kind,
|
||||
}));
|
||||
}
|
||||
|
||||
function unsupportedService(serviceId: string, options: ArtifactRegistryOptions): Record<string, unknown> {
|
||||
@@ -500,9 +573,11 @@ function unsupportedService(serviceId: string, options: ArtifactRegistryOptions)
|
||||
supported: false,
|
||||
error: "unsupported",
|
||||
serviceId,
|
||||
environment: options.environment ?? "prod",
|
||||
providerId: options.providerId,
|
||||
reason: "No standardized D601 registry CD consumer is implemented for this service.",
|
||||
supportedServices: supportedArtifactConsumerServices,
|
||||
supportedConsumers: supportedArtifactConsumers(),
|
||||
policy: "unsupported services must not silently fall back to maintenance-channel source builds or legacy direct deployment",
|
||||
};
|
||||
}
|
||||
@@ -786,6 +861,24 @@ function verifyLocalArtifactLabels(
|
||||
};
|
||||
}
|
||||
|
||||
function d601DevFrontendAuthPatchScript(config: UniDeskConfig): string {
|
||||
const secretData = {
|
||||
AUTH_USERNAME: base64(config.auth.username),
|
||||
AUTH_PASSWORD: base64(config.auth.password),
|
||||
SESSION_SECRET: base64(config.auth.sessionSecret),
|
||||
};
|
||||
const configData = {
|
||||
SESSION_TTL_SECONDS: String(config.auth.sessionTtlSeconds),
|
||||
};
|
||||
return [
|
||||
`secret_patch=${shellQuote(JSON.stringify({ data: secretData }))}`,
|
||||
`config_patch=${shellQuote(JSON.stringify({ data: configData }))}`,
|
||||
"kubectl -n unidesk-dev patch secret unidesk-dev-runtime-secrets --type merge -p \"$secret_patch\"",
|
||||
"kubectl -n unidesk-dev patch configmap unidesk-dev-runtime-config --type merge -p \"$config_patch\"",
|
||||
"echo artifact_cd_dev_frontend_auth_synced=ok",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function composeArtifactEnvValues(spec: ArtifactConsumerSpec, options: ArtifactRegistryOptions, commit: string): Record<string, string> {
|
||||
if (spec.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
|
||||
const prefix = spec.compose.deployEnvPrefix;
|
||||
@@ -848,6 +941,8 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
|
||||
...composeArtifactEnvValues(spec, options, commit),
|
||||
});
|
||||
const compose = resolveComposeCommand(config, runtimeEnv.envFile);
|
||||
const projectIndex = compose.indexOf("-p");
|
||||
const composeProject = projectIndex >= 0 && compose[projectIndex + 1] !== undefined ? compose[projectIndex + 1] : "unidesk";
|
||||
const serviceName = spec.compose.serviceName;
|
||||
const containerName = spec.compose.containerName;
|
||||
const serviceLogPrefix = spec.serviceId.replace(/-/gu, "_");
|
||||
@@ -857,7 +952,7 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
|
||||
`${compose.map(shellQuote).join(" ")} up -d --no-build --no-deps --force-recreate ${shellQuote(serviceName)}`,
|
||||
"ready=0",
|
||||
"for attempt in $(seq 1 60); do",
|
||||
` cid=$(docker ps -q --filter label=com.docker.compose.project=unidesk --filter label=com.docker.compose.service=${shellQuote(serviceName)} --filter label=com.docker.compose.oneoff=False | head -1 || true)`,
|
||||
` cid=$(docker ps -q --filter label=com.docker.compose.project=${shellQuote(composeProject)} --filter label=com.docker.compose.service=${shellQuote(serviceName)} --filter label=com.docker.compose.oneoff=False | head -1 || true)`,
|
||||
" if [ -n \"$cid\" ]; then",
|
||||
" health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' \"$cid\" 2>/dev/null || true)",
|
||||
` echo "${serviceLogPrefix}_container_probe attempt=$attempt cid=$cid health=$health"`,
|
||||
@@ -868,7 +963,7 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
|
||||
" sleep 1",
|
||||
"done",
|
||||
"test \"$ready\" = \"1\"",
|
||||
`cid=$(docker ps -q --filter label=com.docker.compose.project=unidesk --filter label=com.docker.compose.service=${shellQuote(serviceName)} --filter label=com.docker.compose.oneoff=False | head -1)`,
|
||||
`cid=$(docker ps -q --filter label=com.docker.compose.project=${shellQuote(composeProject)} --filter label=com.docker.compose.service=${shellQuote(serviceName)} --filter label=com.docker.compose.oneoff=False | head -1)`,
|
||||
"image_id=$(docker inspect -f '{{.Image}}' \"$cid\")",
|
||||
"actual_commit=$(docker image inspect -f '{{ index .Config.Labels \"unidesk.ai/source-commit\" }}' \"$image_id\")",
|
||||
`test "$actual_commit" = ${shellQuote(commit)}`,
|
||||
@@ -1006,19 +1101,21 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
|
||||
"native k3s containerd has the commit image and stable runtime image tag",
|
||||
"Deployment annotation and pod image id label match the requested commit",
|
||||
"service health via Kubernetes API service proxy returns the same deploy.commit",
|
||||
...(spec.environment === "dev" && spec.serviceId === "frontend" ? ["dev frontend auth/session config is synced from main-server config before rollout"] : []),
|
||||
],
|
||||
rollback: rollbackInfo(spec, commit),
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackInfo(spec: ArtifactConsumerSpec, commit: string): Record<string, unknown> {
|
||||
const environment = spec.environment ?? "prod";
|
||||
if (spec.kind === "compose") {
|
||||
const compose = spec.compose;
|
||||
return {
|
||||
type: "compose-retag-recreate",
|
||||
composeService: compose?.serviceName,
|
||||
previousImageHint: `Use docker image ls / docker inspect to find the previous labeled ${spec.serviceId} image id; Compose volumes are unchanged.`,
|
||||
commandShape: `bun scripts/cli.ts deploy apply --env prod --service ${spec.serviceId} --commit <previous-full-sha>`,
|
||||
commandShape: `bun scripts/cli.ts deploy apply --env ${environment} --service ${spec.serviceId}${environment === "prod" ? " --commit <previous-full-sha>" : ""}`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -1026,12 +1123,12 @@ function rollbackInfo(spec: ArtifactConsumerSpec, commit: string): Record<string
|
||||
serviceId: spec.serviceId,
|
||||
currentCommit: commit,
|
||||
discovery: `kubectl -n ${spec.k3s?.namespace} rollout history deployment/${spec.k3s?.deploymentName} && kubectl -n ${spec.k3s?.namespace} get deployment ${spec.k3s?.deploymentName} -o jsonpath='{.metadata.annotations.unidesk\\.ai/deploy-previous-commit}'`,
|
||||
commandShape: `bun scripts/cli.ts deploy apply --env prod --service ${spec.serviceId} --commit <previous-full-sha>`,
|
||||
commandShape: `bun scripts/cli.ts deploy apply --env ${environment} --service ${spec.serviceId}${environment === "prod" ? " --commit <previous-full-sha>" : ""}`,
|
||||
note: "Rollback is exposed as the same artifact consumer pointed at a previous commit-pinned image that still exists in D601 registry.",
|
||||
};
|
||||
}
|
||||
|
||||
function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, commit: string): string {
|
||||
function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, commit: string, config: UniDeskConfig): string {
|
||||
if (spec.k3s === undefined) throw new Error(`${spec.serviceId} missing k3s artifact consumer config`);
|
||||
const sourceImage = artifactImageRef(options, spec, commit);
|
||||
const commitImage = spec.targetCommitImage(commit);
|
||||
@@ -1062,6 +1159,9 @@ function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: Art
|
||||
`service_port=${shellQuote(String(k3s.servicePort))}`,
|
||||
`health_path=${shellQuote(k3s.healthPath)}`,
|
||||
`manifest=${shellQuote(k3s.manifestPath)}`,
|
||||
`apply_selector=${shellQuote(k3s.applySelector ?? "")}`,
|
||||
`pod_selector=${shellQuote(k3s.podLabelSelector ?? `app.kubernetes.io/name=${k3s.deploymentName}`)}`,
|
||||
"health_tmp=",
|
||||
"export KUBECONFIG=/etc/rancher/k3s/k3s.yaml",
|
||||
"command -v docker >/dev/null",
|
||||
"command -v kubectl >/dev/null",
|
||||
@@ -1083,7 +1183,8 @@ function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: Art
|
||||
"root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images import \"$archive\" >/dev/null",
|
||||
"root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F \"$stable_image\" >/dev/null",
|
||||
"root_exec ctr --address /run/k3s/containerd/containerd.sock -n k8s.io images ls | grep -F \"$commit_image\" >/dev/null",
|
||||
"if [ -f \"$manifest\" ]; then kubectl apply -f \"$manifest\"; else echo artifact_cd_manifest_missing=$manifest; fi",
|
||||
"if [ -f \"$manifest\" ]; then if [ -n \"$apply_selector\" ]; then kubectl apply -f \"$manifest\" -l \"$apply_selector\"; else kubectl apply -f \"$manifest\"; fi; else echo artifact_cd_manifest_missing=$manifest; fi",
|
||||
...(spec.environment === "dev" && spec.serviceId === "frontend" ? [d601DevFrontendAuthPatchScript(config)] : []),
|
||||
"previous_commit=$(kubectl -n \"$namespace\" get deployment \"$deployment\" -o jsonpath='{.metadata.annotations.unidesk\\.ai/deploy-commit}' 2>/dev/null || true)",
|
||||
"kubectl -n \"$namespace\" set image \"deployment/$deployment\" \"$container_name=$commit_image\"",
|
||||
"kubectl -n \"$namespace\" set env \"deployment/$deployment\" \"UNIDESK_DEPLOY_SERVICE_ID=$service_id\" \"UNIDESK_DEPLOY_REF=$deploy_ref\" \"UNIDESK_DEPLOY_REPO=$source_repo\" \"UNIDESK_DEPLOY_COMMIT=$commit\" \"UNIDESK_DEPLOY_REQUESTED_COMMIT=$commit\"",
|
||||
@@ -1134,7 +1235,7 @@ function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: Art
|
||||
"image_label_service=$(containerd_config_label \"$commit_image\" 'unidesk.ai/service-id')",
|
||||
"test \"$image_label_commit\" = \"$commit\"",
|
||||
"test \"$image_label_service\" = \"$service_id\"",
|
||||
"pod=$(kubectl -n \"$namespace\" get pod -l \"app.kubernetes.io/name=$deployment\" -o jsonpath='{.items[0].metadata.name}')",
|
||||
"pod=$(kubectl -n \"$namespace\" get pod -l \"$pod_selector\" -o jsonpath='{.items[0].metadata.name}')",
|
||||
"test -n \"$pod\"",
|
||||
"pod_image=$(kubectl -n \"$namespace\" get pod \"$pod\" -o jsonpath='{.spec.containers[?(@.name==\"'\"$container_name\"'\")].image}')",
|
||||
"test \"$pod_image\" = \"$commit_image\"",
|
||||
@@ -1177,7 +1278,8 @@ async function deployD601K3sArtifactNow(options: ArtifactRegistryOptions, spec:
|
||||
registryProbe: commandTail(registryProbe),
|
||||
};
|
||||
}
|
||||
const deployScript = d601K3sArtifactDeployScript(options, spec, commit);
|
||||
const config = readConfig();
|
||||
const deployScript = d601K3sArtifactDeployScript(options, spec, commit, config);
|
||||
const deploy = runRemoteScript(options, deployScript, Math.max(options.timeoutMs, 420_000));
|
||||
if (deploy.exitCode !== 0 || deploy.timedOut) {
|
||||
return {
|
||||
@@ -1217,7 +1319,7 @@ async function deployD601K3sArtifactNow(options: ArtifactRegistryOptions, spec:
|
||||
async function deployServiceNow(options: ArtifactRegistryOptions): Promise<Record<string, unknown>> {
|
||||
if (options.serviceId === null) throw new Error("artifact-registry deploy-service requires --service <id>");
|
||||
if (options.commit === null) throw new Error("artifact-registry deploy-service requires --commit <full-sha>");
|
||||
const spec = artifactConsumerSpec(options.serviceId);
|
||||
const spec = artifactConsumerSpec(options.serviceId, options.environment);
|
||||
if (spec === null) return unsupportedService(options.serviceId, options);
|
||||
if (options.dryRun) return dryRunArtifactConsumerPlan(options, spec, options.commit);
|
||||
if (spec.kind === "compose") return deployComposeArtifactNow(options, spec);
|
||||
@@ -1227,16 +1329,17 @@ async function deployServiceNow(options: ArtifactRegistryOptions): Promise<Recor
|
||||
function deployServiceJob(args: string[], options: ArtifactRegistryOptions): Record<string, unknown> {
|
||||
if (options.serviceId === null) throw new Error("artifact-registry deploy-service requires --service <id>");
|
||||
if (options.commit === null) throw new Error("artifact-registry deploy-service requires --commit <full-sha>");
|
||||
const spec = artifactConsumerSpec(options.serviceId);
|
||||
const spec = artifactConsumerSpec(options.serviceId, options.environment);
|
||||
if (spec === null) return unsupportedService(options.serviceId, options);
|
||||
if (options.dryRun) return dryRunArtifactConsumerPlan(options, spec, options.commit);
|
||||
const runArgs = args.includes("--run-now") ? args : [...args, "--run-now"];
|
||||
const command = [process.execPath, rootPath("scripts", "cli.ts"), "artifact-registry", ...runArgs];
|
||||
const job = startJob("artifact_registry_service_cd", command, `Pull and deploy ${options.serviceId} artifact ${options.commit} from D601 registry`);
|
||||
const job = startJob("artifact_registry_service_cd", command, `Pull and deploy ${options.environment ?? "prod"} ${options.serviceId} artifact ${options.commit} from D601 registry`);
|
||||
return {
|
||||
ok: true,
|
||||
mode: "async-job",
|
||||
serviceId: options.serviceId,
|
||||
environment: options.environment ?? "prod",
|
||||
job,
|
||||
statusCommand: `bun scripts/cli.ts job status ${job.id}`,
|
||||
tailCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 30000`,
|
||||
@@ -1262,17 +1365,24 @@ function localHelp(): Record<string, unknown> {
|
||||
"bun scripts/cli.ts artifact-registry install [--provider-id D601]",
|
||||
"bun scripts/cli.ts artifact-registry deploy-backend-core --commit <full-sha> [--run-now] [--provider-id D601]",
|
||||
"bun scripts/cli.ts artifact-registry deploy-service --service baidu-netdisk --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
|
||||
"bun scripts/cli.ts artifact-registry deploy-service --service frontend --env prod --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
|
||||
"bun scripts/cli.ts artifact-registry deploy-service --service frontend --env dev --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
|
||||
"bun scripts/cli.ts artifact-registry deploy-service --service decision-center --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
|
||||
],
|
||||
firstStage: "install now writes the rendered systemd/Compose/config files and starts the registry",
|
||||
artifactConsumers: {
|
||||
supportedServices: supportedArtifactConsumerServices,
|
||||
supportedConsumers: supportedArtifactConsumers(),
|
||||
unsupportedPolicy: "return structured unsupported; never fall back to legacy maintenance-channel source builds",
|
||||
prodCommands: [
|
||||
"bun scripts/cli.ts deploy apply --env prod --service backend-core",
|
||||
"bun scripts/cli.ts deploy apply --env prod --service baidu-netdisk",
|
||||
"bun scripts/cli.ts deploy apply --env prod --service frontend",
|
||||
"bun scripts/cli.ts deploy apply --env prod --service decision-center",
|
||||
],
|
||||
devCommands: [
|
||||
"bun scripts/cli.ts deploy apply --env dev --service frontend",
|
||||
],
|
||||
rollbackShape: "rerun the same artifact consumer with a previous commit-pinned image",
|
||||
},
|
||||
defaults: defaultOptions,
|
||||
|
||||
+27
-7
@@ -216,6 +216,13 @@ function requireSupportedUserService(config: UniDeskConfig, serviceId: string):
|
||||
return service;
|
||||
}
|
||||
|
||||
function frontendArtifactTarget(repoOverride: string | null): { repoUrl: string; dockerfile: string } {
|
||||
return {
|
||||
repoUrl: repoOverride ?? "https://github.com/pikasTech/unidesk",
|
||||
dockerfile: "src/components/frontend/Dockerfile",
|
||||
};
|
||||
}
|
||||
|
||||
function chunks(value: string, size: number): string[] {
|
||||
const result: string[] = [];
|
||||
for (let index = 0; index < value.length; index += size) {
|
||||
@@ -1415,6 +1422,7 @@ export function ciHelp(): Record<string, unknown> {
|
||||
"bun scripts/cli.ts ci publish-backend-core --commit <full-sha>",
|
||||
"bun scripts/cli.ts ci publish-user-service --service baidu-netdisk --commit <full-sha>",
|
||||
"bun scripts/cli.ts ci publish-user-service --service decision-center --commit <full-sha>",
|
||||
"bun scripts/cli.ts ci publish-user-service --service frontend --commit <full-sha>",
|
||||
"bun scripts/cli.ts ci run-dev-e2e --wait-ms 600000",
|
||||
"bun scripts/cli.ts ci logs <runId>",
|
||||
],
|
||||
@@ -1435,10 +1443,14 @@ export function ciHelp(): Record<string, unknown> {
|
||||
userServiceArtifact: {
|
||||
producer: "D601 CI",
|
||||
command: "bun scripts/cli.ts ci publish-user-service --service <service-id> --commit <full-sha>",
|
||||
initiallySupportedServices: ["baidu-netdisk", "decision-center"],
|
||||
initiallySupportedServices: ["baidu-netdisk", "decision-center", "frontend"],
|
||||
registry: "127.0.0.1:5000/unidesk/<service-id>:<commit>",
|
||||
outputFields: ["serviceId", "sourceCommit", "sourceRepo", "dockerfile", "imageRef", "tag", "digest", "digestRef"],
|
||||
boundary: "artifact producer only; no prod deploy and no production namespace mutation",
|
||||
frontendNext: [
|
||||
"bun scripts/cli.ts deploy apply --env dev --service frontend",
|
||||
"bun scripts/cli.ts deploy apply --env prod --service frontend",
|
||||
],
|
||||
},
|
||||
runDevE2E: {
|
||||
defaultTriggerMode: "commit-pinned-ssh-launcher",
|
||||
@@ -1475,17 +1487,25 @@ export async function runCiCommand(config: UniDeskConfig, args: string[]): Promi
|
||||
}
|
||||
if (action === "publish-user-service") {
|
||||
const serviceId = requireServiceId(stringOption(args, "--service") ?? stringOption(args, "--service-id"));
|
||||
const service = requireSupportedUserService(config, serviceId);
|
||||
const repoUrl = stringOption(args, "--repo") ?? stringOption(args, "--repo-url") ?? service.repository.url;
|
||||
const repoOverride = stringOption(args, "--repo") ?? stringOption(args, "--repo-url");
|
||||
const target = serviceId === "frontend"
|
||||
? frontendArtifactTarget(repoOverride)
|
||||
: (() => {
|
||||
const service = requireSupportedUserService(config, serviceId);
|
||||
return {
|
||||
repoUrl: repoOverride ?? service.repository.url,
|
||||
dockerfile: service.repository.dockerfile,
|
||||
};
|
||||
})();
|
||||
const commit = requireFullCommit(stringOption(args, "--commit") ?? stringOption(args, "--revision"));
|
||||
const waitMs = numberOption(args, "--wait-ms", 0);
|
||||
const dryRun = boolFlag(args, "--dry-run");
|
||||
const dockerfile = requireRepoRelativePath(service.repository.dockerfile, `microservices.${serviceId}.repository.dockerfile`);
|
||||
if (!["baidu-netdisk", "decision-center"].includes(serviceId)) {
|
||||
throw new Error("ci publish-user-service currently allows only baidu-netdisk and decision-center until each user-service Dockerfile contract is reviewed");
|
||||
const dockerfile = requireRepoRelativePath(target.dockerfile, serviceId === "frontend" ? "frontend.dockerfile" : `microservices.${serviceId}.repository.dockerfile`);
|
||||
if (!["baidu-netdisk", "decision-center", "frontend"].includes(serviceId)) {
|
||||
throw new Error("ci publish-user-service currently allows only baidu-netdisk, decision-center, and frontend until each user-service Dockerfile contract is reviewed");
|
||||
}
|
||||
return publishUserServiceArtifact(config, {
|
||||
repoUrl,
|
||||
repoUrl: target.repoUrl,
|
||||
commit,
|
||||
waitMs,
|
||||
serviceId,
|
||||
|
||||
+22
-16
@@ -133,10 +133,10 @@ 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 d601MaintenanceDeployAllowedServiceIds = new Set<string>(["backend-core", "frontend", "k3sctl-adapter", "code-queue"]);
|
||||
const devApplySupportedServiceIds = new Set<string>(["backend-core", "frontend"]);
|
||||
const devArtifactConsumerServiceIds = new Set<string>(["baidu-netdisk"]);
|
||||
const prodArtifactConsumerServiceIds = new Set<string>(["backend-core", "baidu-netdisk", "decision-center"]);
|
||||
const d601MaintenanceDeployAllowedServiceIds = new Set<string>(["backend-core", "k3sctl-adapter", "code-queue"]);
|
||||
const devApplySupportedServiceIds = new Set<string>(["backend-core"]);
|
||||
const devArtifactConsumerServiceIds = new Set<string>(["baidu-netdisk", "frontend"]);
|
||||
const prodArtifactConsumerServiceIds = new Set<string>(["backend-core", "baidu-netdisk", "decision-center", "frontend"]);
|
||||
const deployEnvironmentTargets: Record<DeployEnvironment, DeployEnvironmentTarget> = {
|
||||
dev: {
|
||||
environment: "dev",
|
||||
@@ -199,7 +199,7 @@ export function deployHelp(action: string | undefined = undefined): Record<strin
|
||||
},
|
||||
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. Local manifest apply allows k3sctl-adapter and explicit production code-queue controlled rollout on D601." },
|
||||
{ name: "--env <dev|prod>", description: "Read the named environment from origin/master:deploy.json. Dev apply supports backend-core/frontend target-side rollout plus baidu-netdisk artifact validation; prod apply uses the D601 registry artifact consumer for backend-core, baidu-netdisk, and decision-center." },
|
||||
{ name: "--env <dev|prod>", description: "Read the named environment from origin/master:deploy.json. Dev apply supports backend-core target-side rollout plus frontend/baidu-netdisk artifact consumers; prod apply uses the D601 registry artifact consumer for backend-core, frontend, baidu-netdisk, and decision-center." },
|
||||
{ name: "--service <id>", description: "Limit reconcile to one service from the manifest." },
|
||||
{ name: "--commit <full-sha>", description: "Prod artifact rollback/apply override for a selected service; the image must already exist in D601 registry." },
|
||||
{ name: "--dry-run", description: "Prepare and validate without mutating the target service." },
|
||||
@@ -2305,7 +2305,7 @@ async function applyOneService(config: UniDeskConfig, service: UniDeskMicroservi
|
||||
ok: false,
|
||||
serviceId: service.id,
|
||||
skipped: true,
|
||||
reason: `D601 target-side deployment is allowed only for k3sctl-adapter and dev backend-core/frontend; ${service.id} is not enabled. Use ci run-dev-e2e for smoke verification.`,
|
||||
reason: `D601 target-side deployment is allowed only for k3sctl-adapter and dev backend-core; ${service.id} is not enabled. Use ci run-dev-e2e for smoke verification.`,
|
||||
steps,
|
||||
};
|
||||
}
|
||||
@@ -2439,7 +2439,9 @@ function environmentDryRunPlan(
|
||||
}));
|
||||
const fingerprint = databaseFingerprint(target);
|
||||
return {
|
||||
ok: environment === "prod" ? services.every((service) => prodArtifactConsumerServiceIds.has(service.id)) : true,
|
||||
ok: environment === "prod"
|
||||
? services.every((service) => prodArtifactConsumerServiceIds.has(service.id))
|
||||
: services.every((service) => devApplySupportedServiceIds.has(service.id) || devArtifactConsumerServiceIds.has(service.id)),
|
||||
action,
|
||||
mode: "environment-ref-dry-run",
|
||||
dryRun: true,
|
||||
@@ -2478,8 +2480,10 @@ function environmentDryRunPlan(
|
||||
? "d601-registry-artifact-consumer"
|
||||
: "unsupported"
|
||||
: devArtifactConsumerServiceIds.has(service.id)
|
||||
? "d601-registry-artifact-consumer-dev-validation"
|
||||
: "d601-dev-target-side-build",
|
||||
? service.id === "frontend" ? "d601-registry-artifact-consumer" : "d601-registry-artifact-consumer-dev-validation"
|
||||
: devApplySupportedServiceIds.has(service.id)
|
||||
? "d601-dev-target-side-build"
|
||||
: "unsupported",
|
||||
unsupported: (environment === "prod" && !prodArtifactConsumerServiceIds.has(service.id))
|
||||
|| (environment === "dev" && !devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id))
|
||||
? {
|
||||
@@ -2508,7 +2512,7 @@ function blockedD601MaintenanceDeployServices(config: UniDeskConfig, manifest: D
|
||||
}
|
||||
|
||||
function d601MaintenanceDeployBlockMessage(blocked: string[]): string {
|
||||
return `D601 target-side deployment is enabled only for k3sctl-adapter and dev backend-core/frontend; blocked services: ${blocked.join(", ")}. Use ci run-dev-e2e for dev smoke verification.`;
|
||||
return `D601 target-side deployment is enabled only for k3sctl-adapter and dev backend-core; blocked services: ${blocked.join(", ")}. Use ci run-dev-e2e for dev smoke verification.`;
|
||||
}
|
||||
|
||||
function selectedDevArtifactServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
|
||||
@@ -2518,7 +2522,8 @@ function selectedDevArtifactServices(manifest: DeployManifest, serviceId: string
|
||||
|
||||
function selectedDevTargetServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
|
||||
if (manifest.environment !== "dev") return [];
|
||||
return selectedEnvironmentServices(manifest, serviceId).filter((service) => devApplySupportedServiceIds.has(service.id));
|
||||
return selectedEnvironmentServices(manifest, serviceId)
|
||||
.filter((service) => devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id));
|
||||
}
|
||||
|
||||
function selectedEnvironmentServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
|
||||
@@ -2563,7 +2568,8 @@ async function runArtifactConsumerApplyNow(
|
||||
"--service", service.id,
|
||||
"--commit", commit,
|
||||
"--source-repo", service.repo,
|
||||
"--deploy-ref", `deploy.json#environments.${environment}.services.${service.id}`,
|
||||
"--deploy-ref", `${deployEnvironmentTargets[environment].gitRef}:deploy.json#environments.${environment}.services.${service.id}`,
|
||||
...(environment === "prod" || service.id === "frontend" ? ["--env", environment] : []),
|
||||
"--timeout-ms", String(options.timeoutMs),
|
||||
"--run-now",
|
||||
...(options.dryRun ? ["--dry-run"] : []),
|
||||
@@ -2622,11 +2628,11 @@ function devArtifactApplyJob(args: string[], options: DeployOptions): Record<str
|
||||
return {
|
||||
ok: true,
|
||||
mode: "async-job",
|
||||
executor: "d601-registry-artifact-consumer-dev-validation",
|
||||
executor: "d601-registry-artifact-consumer",
|
||||
job,
|
||||
statusCommand: `bun scripts/cli.ts job status ${job.id}`,
|
||||
tailCommand: `bun scripts/cli.ts job status ${job.id} --tail-bytes 30000`,
|
||||
note: "Dev artifact validation continues in the background: D601 registry commit-pinned image check, pull-only Compose recreate, image-label and live health commit verification.",
|
||||
note: "Dev artifact consumer continues in the background: D601 registry commit-pinned image check, pull/import, rollout or validation, image-label and live health commit verification.",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2684,12 +2690,12 @@ export async function runDeployCommand(config: UniDeskConfig | null, args: strin
|
||||
}
|
||||
const unsupported = unsupportedDevApplyServices(manifest, options.serviceId);
|
||||
if (unsupported.length > 0) {
|
||||
throw new Error(`deploy apply --env dev currently supports only backend-core/frontend target-side rollout and baidu-netdisk artifact validation; unsupported selected services: ${unsupported.join(", ")}. Use ci run-dev-e2e for smoke verification.`);
|
||||
throw new Error(`deploy apply --env dev currently supports backend-core target-side rollout plus frontend/baidu-netdisk artifact consumers; unsupported selected services: ${unsupported.join(", ")}. Use ci run-dev-e2e for smoke verification.`);
|
||||
}
|
||||
const devArtifactServices = selectedDevArtifactServices(manifest, options.serviceId);
|
||||
const devTargetServices = selectedDevTargetServices(manifest, options.serviceId);
|
||||
if (devArtifactServices.length > 0 && devTargetServices.length > 0) {
|
||||
throw new Error("deploy apply --env dev cannot mix artifact validation services with target-side rollout services in one invocation; pass --service");
|
||||
throw new Error("deploy apply --env dev cannot mix artifact consumer services with target-side rollout services in one invocation; pass --service");
|
||||
}
|
||||
if (devArtifactServices.length > 0) {
|
||||
if (options.dryRun || options.runNow) return await runArtifactConsumerApplyNow(manifest, options, "dev", devArtifactServices);
|
||||
|
||||
@@ -111,6 +111,11 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
|
||||
UNIDESK_DEPLOY_REPO: runtimeSecret("UNIDESK_DEPLOY_REPO"),
|
||||
UNIDESK_DEPLOY_COMMIT: runtimeSecret("UNIDESK_DEPLOY_COMMIT"),
|
||||
UNIDESK_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_DEPLOY_REQUESTED_COMMIT"),
|
||||
UNIDESK_FRONTEND_DEPLOY_REF: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_REF"),
|
||||
UNIDESK_FRONTEND_DEPLOY_SERVICE_ID: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_SERVICE_ID") || "frontend",
|
||||
UNIDESK_FRONTEND_DEPLOY_REPO: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_REPO"),
|
||||
UNIDESK_FRONTEND_DEPLOY_COMMIT: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_COMMIT"),
|
||||
UNIDESK_FRONTEND_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_FRONTEND_DEPLOY_REQUESTED_COMMIT"),
|
||||
UNIDESK_AUTH_USERNAME: config.auth.username,
|
||||
UNIDESK_AUTH_PASSWORD: config.auth.password,
|
||||
UNIDESK_SESSION_SECRET: config.auth.sessionSecret,
|
||||
|
||||
+3
-1
@@ -260,6 +260,8 @@ function artifactRegistryHelp(): unknown {
|
||||
"bun scripts/cli.ts artifact-registry install [--provider-id D601]",
|
||||
"bun scripts/cli.ts artifact-registry deploy-backend-core --commit <full-sha> [--run-now] [--provider-id D601]",
|
||||
"bun scripts/cli.ts artifact-registry deploy-service --service baidu-netdisk --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
|
||||
"bun scripts/cli.ts artifact-registry deploy-service --service frontend --env prod --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
|
||||
"bun scripts/cli.ts artifact-registry deploy-service --service frontend --env dev --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
|
||||
"bun scripts/cli.ts artifact-registry deploy-service --service decision-center --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
|
||||
],
|
||||
description: "Manage the declaration, rendered files and readonly checks for the D601 host-managed CNCF Distribution artifact registry.",
|
||||
@@ -268,7 +270,7 @@ function artifactRegistryHelp(): unknown {
|
||||
"service is host-managed by systemd + Docker Compose, not k3s-managed",
|
||||
"install writes the rendered host unit/config and starts the registry",
|
||||
"deploy-backend-core only pulls commit-pinned backend-core artifacts and does not build backend-core on the master server",
|
||||
"deploy-service currently supports backend-core, baidu-netdisk, and decision-center as standardized consumers",
|
||||
"deploy-service currently supports backend-core, baidu-netdisk, prod/dev frontend, and decision-center as standardized consumers",
|
||||
"status and health use provider-gateway Host SSH readonly checks",
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user