feat: add frontend artifact delivery path

This commit is contained in:
Codex
2026-05-19 15:43:02 +00:00
parent 76b27c6aa9
commit a769242207
18 changed files with 277 additions and 91 deletions
+127 -17
View File
@@ -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
View File
@@ -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
View File
@@ -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);
+5
View File
@@ -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
View File
@@ -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",
],
};