feat: add baidu netdisk artifact delivery
This commit is contained in:
@@ -22,9 +22,10 @@ interface ArtifactRegistryOptions {
|
||||
dryRun: boolean;
|
||||
runNow: boolean;
|
||||
commit: string | null;
|
||||
targetImage: string;
|
||||
targetImage: string | null;
|
||||
serviceId: string | null;
|
||||
sourceRepo: string;
|
||||
deployRef: string | null;
|
||||
}
|
||||
|
||||
interface RenderedFile {
|
||||
@@ -60,11 +61,12 @@ const defaultOptions: ArtifactRegistryOptions = {
|
||||
dryRun: false,
|
||||
runNow: false,
|
||||
commit: null,
|
||||
targetImage: "unidesk-backend-core",
|
||||
targetImage: null,
|
||||
serviceId: null,
|
||||
sourceRepo: "https://github.com/pikasTech/unidesk",
|
||||
deployRef: null,
|
||||
};
|
||||
const supportedArtifactConsumerServices = ["backend-core", "decision-center"] as const;
|
||||
const supportedArtifactConsumerServices = ["backend-core", "baidu-netdisk", "decision-center"] as const;
|
||||
type SupportedArtifactConsumerService = typeof supportedArtifactConsumerServices[number];
|
||||
|
||||
interface ArtifactConsumerSpec {
|
||||
@@ -75,6 +77,13 @@ interface ArtifactConsumerSpec {
|
||||
targetImage: string;
|
||||
targetCommitImage: (commit: string) => string;
|
||||
deployRef: string;
|
||||
compose?: {
|
||||
serviceName: string;
|
||||
containerName: string;
|
||||
deployEnvPrefix: string;
|
||||
healthProbeCommand: string;
|
||||
requireHealthCommit: boolean;
|
||||
};
|
||||
k3s?: {
|
||||
namespace: string;
|
||||
manifestPath: string;
|
||||
@@ -95,6 +104,29 @@ const artifactConsumerSpecs: Record<SupportedArtifactConsumerService, ArtifactCo
|
||||
targetImage: "unidesk-backend-core",
|
||||
targetCommitImage: (commit: string) => `unidesk-backend-core:${commit}`,
|
||||
deployRef: "deploy.json#environments.prod.services.backend-core",
|
||||
compose: {
|
||||
serviceName: "backend-core",
|
||||
containerName: "unidesk-backend-core",
|
||||
deployEnvPrefix: "UNIDESK_DEPLOY",
|
||||
healthProbeCommand: "if command -v backend-core >/dev/null 2>&1; then backend-core --fetch-json http://127.0.0.1:8080/health --require-ok; elif command -v bun >/dev/null 2>&1; then 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)})\"; else exit 1; fi",
|
||||
requireHealthCommit: false,
|
||||
},
|
||||
},
|
||||
"baidu-netdisk": {
|
||||
serviceId: "baidu-netdisk",
|
||||
kind: "compose",
|
||||
registryRepository: "unidesk/baidu-netdisk",
|
||||
dockerfile: "src/components/microservices/baidu-netdisk/Dockerfile",
|
||||
targetImage: "baidu-netdisk",
|
||||
targetCommitImage: (commit: string) => `baidu-netdisk:${commit}`,
|
||||
deployRef: "deploy.json#environments.prod.services.baidu-netdisk",
|
||||
compose: {
|
||||
serviceName: "baidu-netdisk",
|
||||
containerName: "baidu-netdisk-backend",
|
||||
deployEnvPrefix: "UNIDESK_BAIDU_NETDISK_DEPLOY",
|
||||
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4244/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,
|
||||
},
|
||||
},
|
||||
"decision-center": {
|
||||
serviceId: "decision-center",
|
||||
@@ -185,6 +217,9 @@ function parseOptions(args: string[]): ArtifactRegistryOptions {
|
||||
} else if (arg === "--source-repo") {
|
||||
options.sourceRepo = requireValue(args, index, arg);
|
||||
index += 1;
|
||||
} else if (arg === "--deploy-ref") {
|
||||
options.deployRef = requireValue(args, index, arg);
|
||||
index += 1;
|
||||
} else {
|
||||
throw new Error(`unknown artifact-registry option: ${arg}`);
|
||||
}
|
||||
@@ -202,6 +237,10 @@ function shellQuote(value: string): string {
|
||||
return `'${value.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function safeName(value: string): string {
|
||||
return value.replace(/[^A-Za-z0-9_.-]/gu, "-");
|
||||
}
|
||||
|
||||
function rootExecPrelude(): string {
|
||||
return [
|
||||
"root_exec() {",
|
||||
@@ -328,16 +367,17 @@ 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 artifacts on D601",
|
||||
"CD only pulls, retags, recreates backend-core, and verifies live commit",
|
||||
"master server CD must not compile Rust or run docker compose build backend-core",
|
||||
"CI builds and publishes backend-core 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",
|
||||
],
|
||||
renderedPaths: bundle.paths,
|
||||
backendCoreArtifactFlow: [
|
||||
"D601 CI builds unidesk/backend-core:<commit>",
|
||||
"D601 CI pushes 127.0.0.1:5000/unidesk/backend-core:<commit>",
|
||||
"main server pulls via a controlled short-lived localhost relay",
|
||||
"prod CD retags, recreates backend-core, and verifies live commit metadata",
|
||||
artifactConsumerFlow: [
|
||||
"D601 CI builds unidesk/<service-id>:<commit>",
|
||||
"D601 CI pushes 127.0.0.1:5000/unidesk/<service-id>:<commit>",
|
||||
"Compose consumers pull via a controlled provider-gateway SSH image stream",
|
||||
"k3s consumers pull on D601 and import into native k3s containerd",
|
||||
"CD retags/recreates or updates Deployment images and verifies live commit metadata",
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -471,6 +511,10 @@ function artifactImageRef(options: ArtifactRegistryOptions, spec: ArtifactConsum
|
||||
return `127.0.0.1:${options.port}/${spec.registryRepository}:${commit}`;
|
||||
}
|
||||
|
||||
function deployRefFor(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec): string {
|
||||
return options.deployRef ?? spec.deployRef;
|
||||
}
|
||||
|
||||
function runRemoteScript(options: ArtifactRegistryOptions, script: string, timeoutMs = options.timeoutMs): CommandResult {
|
||||
const command = [process.execPath, "scripts/cli.ts", "ssh", options.providerId, "argv", "bash", "-lc", script];
|
||||
return runCommand(command, repoRoot, { timeoutMs });
|
||||
@@ -742,21 +786,34 @@ function verifyLocalArtifactLabels(
|
||||
};
|
||||
}
|
||||
|
||||
async function deployBackendCoreNow(options: ArtifactRegistryOptions): Promise<Record<string, unknown>> {
|
||||
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;
|
||||
return {
|
||||
[`${prefix}_SERVICE_ID`]: spec.serviceId,
|
||||
[`${prefix}_REF`]: deployRefFor(options, spec),
|
||||
[`${prefix}_REPO`]: options.sourceRepo,
|
||||
[`${prefix}_COMMIT`]: commit,
|
||||
[`${prefix}_REQUESTED_COMMIT`]: commit,
|
||||
};
|
||||
}
|
||||
|
||||
async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec): Promise<Record<string, unknown>> {
|
||||
const commit = options.commit;
|
||||
if (commit === null) throw new Error("artifact-registry deploy-backend-core requires --commit <full-sha>");
|
||||
const spec = artifactConsumerSpecs["backend-core"];
|
||||
if (commit === null) throw new Error("artifact-registry deploy-service requires --commit <full-sha>");
|
||||
if (spec.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
|
||||
const health = runReadonlyStatus(options, true);
|
||||
if (health.ok !== true) {
|
||||
return { ok: false, error: "D601 artifact registry is not healthy", health };
|
||||
return { ok: false, serviceId: spec.serviceId, error: "D601 artifact registry is not healthy", health };
|
||||
}
|
||||
const sourceImage = artifactImageRef(options, spec, commit);
|
||||
const composeImage = options.targetImage === defaultOptions.targetImage ? spec.targetImage : options.targetImage;
|
||||
const commitImage = `${composeImage}:${commit}`;
|
||||
const composeImage = options.targetImage ?? spec.targetImage;
|
||||
const commitImage = options.targetImage === null ? spec.targetCommitImage(commit) : `${options.targetImage}:${commit}`;
|
||||
const registryProbe = runRemoteScript(options, registryArtifactProbeScript(options, spec, commit), Math.max(options.timeoutMs, 120_000));
|
||||
if (registryProbe.exitCode !== 0 || registryProbe.timedOut) {
|
||||
return {
|
||||
ok: false,
|
||||
serviceId: spec.serviceId,
|
||||
step: "registry-artifact-check",
|
||||
error: registryArtifactMissingMessage(spec),
|
||||
sourceImage,
|
||||
@@ -767,6 +824,7 @@ async function deployBackendCoreNow(options: ArtifactRegistryOptions): Promise<R
|
||||
if (pull.exitCode !== 0 || pull.timedOut) {
|
||||
return {
|
||||
ok: false,
|
||||
serviceId: spec.serviceId,
|
||||
step: "docker-load",
|
||||
sourceImage,
|
||||
registryProbe: commandTail(registryProbe),
|
||||
@@ -787,41 +845,46 @@ async function deployBackendCoreNow(options: ArtifactRegistryOptions): Promise<R
|
||||
const config = readConfig();
|
||||
const runtimeEnv = writeComposeEnv(config, false);
|
||||
upsertEnvFileValues(runtimeEnv.envFile, {
|
||||
UNIDESK_DEPLOY_REF: spec.deployRef,
|
||||
UNIDESK_DEPLOY_SERVICE_ID: "backend-core",
|
||||
UNIDESK_DEPLOY_REPO: options.sourceRepo,
|
||||
UNIDESK_DEPLOY_COMMIT: commit,
|
||||
UNIDESK_DEPLOY_REQUESTED_COMMIT: commit,
|
||||
...composeArtifactEnvValues(spec, options, commit),
|
||||
});
|
||||
const compose = resolveComposeCommand(config, runtimeEnv.envFile);
|
||||
const serviceName = spec.compose.serviceName;
|
||||
const containerName = spec.compose.containerName;
|
||||
const serviceLogPrefix = spec.serviceId.replace(/-/gu, "_");
|
||||
const upScript = [
|
||||
"set -euo pipefail",
|
||||
`echo ${shellQuote(`backend_core_artifact_cd source=${sourceImage} target=${composeImage}`)}`,
|
||||
`${compose.map(shellQuote).join(" ")} up -d --no-build --no-deps --force-recreate backend-core`,
|
||||
`echo ${shellQuote(`${serviceLogPrefix}_artifact_cd source=${sourceImage} target=${composeImage}`)}`,
|
||||
`${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=backend-core --filter label=com.docker.compose.oneoff=False | head -1 || true)",
|
||||
` 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)`,
|
||||
" if [ -n \"$cid\" ]; then",
|
||||
" health=$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}' \"$cid\" 2>/dev/null || true)",
|
||||
" echo \"backend_core_container_probe attempt=$attempt cid=$cid health=$health\"",
|
||||
` echo "${serviceLogPrefix}_container_probe attempt=$attempt cid=$cid health=$health"`,
|
||||
" if [ \"$health\" = \"healthy\" ] || [ \"$health\" = \"running\" ]; then ready=1; break; fi",
|
||||
" else",
|
||||
" echo \"backend_core_container_probe attempt=$attempt cid=missing\"",
|
||||
` echo "${serviceLogPrefix}_container_probe attempt=$attempt cid=missing"`,
|
||||
" fi",
|
||||
" sleep 1",
|
||||
"done",
|
||||
"test \"$ready\" = \"1\"",
|
||||
"cid=$(docker ps -q --filter label=com.docker.compose.project=unidesk --filter label=com.docker.compose.service=backend-core --filter label=com.docker.compose.oneoff=False | head -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)`,
|
||||
"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)}`,
|
||||
"docker exec \"$cid\" 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)})\" >/tmp/unidesk-backend-core-health.json",
|
||||
"cat /tmp/unidesk-backend-core-health.json",
|
||||
`health_json=/tmp/unidesk-${safeName(spec.serviceId)}-health.json`,
|
||||
`docker exec "$cid" sh -lc ${shellQuote(spec.compose.healthProbeCommand)} > "$health_json"`,
|
||||
"cat \"$health_json\"",
|
||||
...(spec.compose.requireHealthCommit ? [
|
||||
"health_commit=$(python3 -c 'import json,sys; print(((json.load(open(sys.argv[1])).get(\"deploy\") or {}).get(\"commit\") or \"\"))' \"$health_json\")",
|
||||
`test "$health_commit" = ${shellQuote(commit)}`,
|
||||
] : []),
|
||||
].join("\n");
|
||||
const deploy = runCommand(["bash", "-lc", composeLockScript(upScript)], repoRoot, { timeoutMs: Math.max(options.timeoutMs, 300_000) });
|
||||
if (deploy.exitCode !== 0 || deploy.timedOut) {
|
||||
return {
|
||||
ok: false,
|
||||
serviceId: spec.serviceId,
|
||||
step: "compose-recreate",
|
||||
sourceImage,
|
||||
targetImage: composeImage,
|
||||
@@ -829,9 +892,11 @@ async function deployBackendCoreNow(options: ArtifactRegistryOptions): Promise<R
|
||||
deploy: commandTail(deploy),
|
||||
};
|
||||
}
|
||||
const running = runCommand(["docker", "inspect", "unidesk-backend-core", "--format", "image={{.Config.Image}} imageId={{.Image}} status={{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}"], repoRoot);
|
||||
const running = runCommand(["docker", "inspect", containerName, "--format", "image={{.Config.Image}} imageId={{.Image}} status={{.State.Status}} health={{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}"], repoRoot);
|
||||
return {
|
||||
ok: true,
|
||||
supported: true,
|
||||
serviceId: spec.serviceId,
|
||||
commit,
|
||||
providerId: options.providerId,
|
||||
sourceImage,
|
||||
@@ -842,13 +907,24 @@ async function deployBackendCoreNow(options: ArtifactRegistryOptions): Promise<R
|
||||
pull: commandTail(pull),
|
||||
deploy: commandTail(deploy),
|
||||
running: running.stdout.trim(),
|
||||
validation: {
|
||||
liveCommit: commit,
|
||||
imageLabelCommit: commit,
|
||||
serviceHealthCommit: spec.compose.requireHealthCommit ? commit : "not-required",
|
||||
healthyOldVersionAccepted: false,
|
||||
},
|
||||
rollback: {
|
||||
previousImageHint: "Use docker image ls and docker inspect logs for the previous backend-core image id; Compose named volumes were not changed.",
|
||||
commandShape: `${compose.map(shellQuote).join(" ")} up -d --no-build --no-deps --force-recreate backend-core`,
|
||||
previousImageHint: `Use docker image ls and docker inspect logs for the previous ${spec.serviceId} image id; Compose named volumes were not changed.`,
|
||||
commandShape: `${compose.map(shellQuote).join(" ")} up -d --no-build --no-deps --force-recreate ${serviceName}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function deployBackendCoreNow(options: ArtifactRegistryOptions): Promise<Record<string, unknown>> {
|
||||
if (options.commit === null) throw new Error("artifact-registry deploy-backend-core requires --commit <full-sha>");
|
||||
return deployComposeArtifactNow(options, artifactConsumerSpecs["backend-core"]);
|
||||
}
|
||||
|
||||
function deployBackendCoreJob(args: string[], options: ArtifactRegistryOptions): Record<string, unknown> {
|
||||
if (options.commit === null) throw new Error("artifact-registry deploy-backend-core requires --commit <full-sha>");
|
||||
const runArgs = args.includes("--run-now") ? args : [...args, "--run-now"];
|
||||
@@ -875,6 +951,7 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
|
||||
serviceId: spec.serviceId,
|
||||
commit,
|
||||
sourceRepo: options.sourceRepo,
|
||||
deployRef: deployRefFor(options, spec),
|
||||
sourceImage,
|
||||
requiredLabels: {
|
||||
"unidesk.ai/service-id": spec.serviceId,
|
||||
@@ -885,23 +962,30 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
|
||||
method: "HEAD",
|
||||
url: `http://127.0.0.1:${options.port}/v2/${spec.registryRepository}/manifests/${commit}`,
|
||||
},
|
||||
boundary: "prod CD is artifact-consumer only: verify commit-pinned registry image, pull/import, deploy, then verify live commit/image/health; it never builds source on prod",
|
||||
boundary: "artifact CD is artifact-consumer only: verify commit-pinned registry image, pull/import, deploy, then verify live commit/image/health; it never builds source on the runtime target",
|
||||
};
|
||||
if (spec.kind === "compose") {
|
||||
if (spec.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
|
||||
return {
|
||||
...common,
|
||||
target: {
|
||||
kind: "compose",
|
||||
composeService: "backend-core",
|
||||
targetImage: options.targetImage === defaultOptions.targetImage ? spec.targetImage : options.targetImage,
|
||||
deployCommandShape: "docker compose up -d --no-build --no-deps --force-recreate backend-core",
|
||||
composeService: spec.compose.serviceName,
|
||||
containerName: spec.compose.containerName,
|
||||
targetImage: options.targetImage ?? spec.targetImage,
|
||||
runtimeImage: spec.targetCommitImage(commit),
|
||||
deployEnvPrefix: spec.compose.deployEnvPrefix,
|
||||
deployCommandShape: `docker compose up -d --no-build --no-deps --force-recreate ${spec.compose.serviceName}`,
|
||||
},
|
||||
validation: [
|
||||
"D601 registry /v2 manifest exists for the commit tag",
|
||||
"loaded image labels match service id, source commit, and Dockerfile",
|
||||
"running Compose container image label matches the requested commit",
|
||||
"backend-core /health succeeds for the recreated container",
|
||||
spec.compose.requireHealthCommit
|
||||
? `${spec.serviceId} /health succeeds and reports deploy.commit matching the artifact commit`
|
||||
: `${spec.serviceId} /health succeeds for the recreated container`,
|
||||
],
|
||||
rollback: rollbackInfo(spec, commit),
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -929,9 +1013,12 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
|
||||
|
||||
function rollbackInfo(spec: ArtifactConsumerSpec, commit: string): Record<string, unknown> {
|
||||
if (spec.kind === "compose") {
|
||||
const compose = spec.compose;
|
||||
return {
|
||||
type: "compose-retag-recreate",
|
||||
previousImageHint: "Use docker image ls / docker inspect to find the previous labeled backend-core image id; Compose volumes are unchanged.",
|
||||
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>`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -951,6 +1038,7 @@ function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: Art
|
||||
const k3s = spec.k3s;
|
||||
const labels = [
|
||||
`unidesk.ai/deploy-service-id=${spec.serviceId}`,
|
||||
`unidesk.ai/deploy-ref=${deployRefFor(options, spec)}`,
|
||||
`unidesk.ai/deploy-repo=${options.sourceRepo}`,
|
||||
`unidesk.ai/deploy-commit=${commit}`,
|
||||
`unidesk.ai/deploy-requested-commit=${commit}`,
|
||||
@@ -964,6 +1052,7 @@ function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: Art
|
||||
`commit_image=${shellQuote(commitImage)}`,
|
||||
`service_id=${shellQuote(spec.serviceId)}`,
|
||||
`source_repo=${shellQuote(options.sourceRepo)}`,
|
||||
`deploy_ref=${shellQuote(deployRefFor(options, spec))}`,
|
||||
`commit=${shellQuote(commit)}`,
|
||||
`dockerfile=${shellQuote(spec.dockerfile)}`,
|
||||
`namespace=${shellQuote(k3s.namespace)}`,
|
||||
@@ -997,7 +1086,7 @@ function d601K3sArtifactDeployScript(options: ArtifactRegistryOptions, spec: Art
|
||||
"if [ -f \"$manifest\" ]; then kubectl apply -f \"$manifest\"; else echo artifact_cd_manifest_missing=$manifest; fi",
|
||||
"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_REPO=$source_repo\" \"UNIDESK_DEPLOY_COMMIT=$commit\" \"UNIDESK_DEPLOY_REQUESTED_COMMIT=$commit\"",
|
||||
"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\"",
|
||||
`kubectl -n "$namespace" annotate "deployment/$deployment" ${labels.map(shellQuote).join(" ")} --overwrite`,
|
||||
"if [ -n \"$previous_commit\" ] && [ \"$previous_commit\" != \"$commit\" ]; then kubectl -n \"$namespace\" annotate \"deployment/$deployment\" \"unidesk.ai/deploy-previous-commit=$previous_commit\" --overwrite; fi",
|
||||
"kubectl -n \"$namespace\" rollout status \"deployment/$deployment\" --timeout=180s",
|
||||
@@ -1109,6 +1198,7 @@ async function deployD601K3sArtifactNow(options: ArtifactRegistryOptions, spec:
|
||||
commit,
|
||||
providerId: options.providerId,
|
||||
sourceRepo: options.sourceRepo,
|
||||
deployRef: deployRefFor(options, spec),
|
||||
sourceImage,
|
||||
stableImage: spec.targetImage,
|
||||
runtimeImage: spec.targetCommitImage(commit),
|
||||
@@ -1130,7 +1220,7 @@ async function deployServiceNow(options: ArtifactRegistryOptions): Promise<Recor
|
||||
const spec = artifactConsumerSpec(options.serviceId);
|
||||
if (spec === null) return unsupportedService(options.serviceId, options);
|
||||
if (options.dryRun) return dryRunArtifactConsumerPlan(options, spec, options.commit);
|
||||
if (spec.kind === "compose") return deployBackendCoreNow({ ...options, targetImage: options.targetImage || spec.targetImage });
|
||||
if (spec.kind === "compose") return deployComposeArtifactNow(options, spec);
|
||||
return deployD601K3sArtifactNow(options, spec);
|
||||
}
|
||||
|
||||
@@ -1171,13 +1261,18 @@ function localHelp(): Record<string, unknown> {
|
||||
"bun scripts/cli.ts artifact-registry health [--provider-id D601]",
|
||||
"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 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,
|
||||
unsupportedPolicy: "return structured unsupported; never fall back to legacy maintenance-channel source builds",
|
||||
prodCommand: "bun scripts/cli.ts deploy apply --env prod --service decision-center",
|
||||
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 decision-center",
|
||||
],
|
||||
rollbackShape: "rerun the same artifact consumer with a previous commit-pinned image",
|
||||
},
|
||||
defaults: defaultOptions,
|
||||
|
||||
@@ -28,6 +28,7 @@ const syntaxFiles = [
|
||||
"src/components/frontend/src/decision-center.tsx",
|
||||
"src/components/provider-gateway/src/index.ts",
|
||||
"src/components/microservices/oa-event-flow/src/index.ts",
|
||||
"src/components/microservices/baidu-netdisk/src/index.ts",
|
||||
"src/components/microservices/k3sctl-adapter/src/index.ts",
|
||||
"src/components/microservices/mdtodo/src/index.ts",
|
||||
"src/components/microservices/decision-center/src/index.ts",
|
||||
|
||||
+12
-8
@@ -196,11 +196,14 @@ function requireSupportedUserService(config: UniDeskConfig, serviceId: string):
|
||||
}
|
||||
const service = config.microservices.find((item) => item.id === serviceId);
|
||||
if (service === undefined) throw new Error(`unknown user service: ${serviceId}`);
|
||||
if (service.providerId !== d601ProviderId || service.development.providerId !== d601ProviderId) {
|
||||
throw new Error(`ci publish-user-service currently supports only D601 user services; ${serviceId} is assigned to ${service.providerId}`);
|
||||
}
|
||||
if (service.deployment.mode !== "k3sctl-managed") {
|
||||
throw new Error(`ci publish-user-service currently supports k3sctl-managed D601 user services; ${serviceId} uses ${service.deployment.mode}`);
|
||||
const isD601K3sService = service.providerId === d601ProviderId
|
||||
&& service.development.providerId === d601ProviderId
|
||||
&& service.deployment.mode === "k3sctl-managed";
|
||||
const isMainServerDirectService = service.providerId === "main-server"
|
||||
&& service.development.providerId === "main-server"
|
||||
&& service.deployment.mode === "unidesk-direct";
|
||||
if (!isD601K3sService && !isMainServerDirectService) {
|
||||
throw new Error(`ci publish-user-service supports only reviewed k3sctl-managed D601 services or main-server unidesk-direct services; ${serviceId} is ${service.providerId}/${service.deployment.mode}`);
|
||||
}
|
||||
return service;
|
||||
}
|
||||
@@ -1307,6 +1310,7 @@ export function ciHelp(): Record<string, unknown> {
|
||||
"bun scripts/cli.ts ci install",
|
||||
"bun scripts/cli.ts ci run --revision <commit>",
|
||||
"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 run-dev-e2e --wait-ms 600000",
|
||||
"bun scripts/cli.ts ci logs <runId>",
|
||||
@@ -1328,7 +1332,7 @@ 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: ["decision-center"],
|
||||
initiallySupportedServices: ["baidu-netdisk", "decision-center"],
|
||||
registry: "127.0.0.1:5000/unidesk/<service-id>:<commit>",
|
||||
outputFields: ["imageRef", "tag", "digest", "sourceCommit", "serviceId"],
|
||||
boundary: "artifact producer only; no prod deploy and no production namespace mutation",
|
||||
@@ -1373,8 +1377,8 @@ export async function runCiCommand(config: UniDeskConfig, args: string[]): Promi
|
||||
const waitMs = numberOption(args, "--wait-ms", 0);
|
||||
const dryRun = boolFlag(args, "--dry-run");
|
||||
const dockerfile = requireRepoRelativePath(service.repository.dockerfile, `microservices.${serviceId}.repository.dockerfile`);
|
||||
if (serviceId !== "decision-center") {
|
||||
throw new Error("ci publish-user-service currently allows only decision-center until each user-service Dockerfile contract is reviewed");
|
||||
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");
|
||||
}
|
||||
return publishUserServiceArtifact(config, {
|
||||
repoUrl,
|
||||
|
||||
+67
-13
@@ -135,7 +135,8 @@ 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 prodArtifactConsumerServiceIds = new Set<string>(["backend-core", "decision-center"]);
|
||||
const devArtifactConsumerServiceIds = new Set<string>(["baidu-netdisk"]);
|
||||
const prodArtifactConsumerServiceIds = new Set<string>(["backend-core", "baidu-netdisk", "decision-center"]);
|
||||
const deployEnvironmentTargets: Record<DeployEnvironment, DeployEnvironmentTarget> = {
|
||||
dev: {
|
||||
environment: "dev",
|
||||
@@ -198,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 is enabled only for backend-core and frontend in D601 unidesk-dev; prod apply uses the D601 registry artifact consumer for backend-core and decision-center." },
|
||||
{ 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: "--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." },
|
||||
@@ -2476,12 +2477,17 @@ function environmentDryRunPlan(
|
||||
? prodArtifactConsumerServiceIds.has(service.id)
|
||||
? "d601-registry-artifact-consumer"
|
||||
: "unsupported"
|
||||
: "d601-dev-target-side-build",
|
||||
unsupported: environment === "prod" && !prodArtifactConsumerServiceIds.has(service.id)
|
||||
: devArtifactConsumerServiceIds.has(service.id)
|
||||
? "d601-registry-artifact-consumer-dev-validation"
|
||||
: "d601-dev-target-side-build",
|
||||
unsupported: (environment === "prod" && !prodArtifactConsumerServiceIds.has(service.id))
|
||||
|| (environment === "dev" && !devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id))
|
||||
? {
|
||||
ok: false,
|
||||
supported: false,
|
||||
reason: "No standardized prod D601 registry artifact consumer is implemented for this service; legacy maintenance-channel deployment is not allowed.",
|
||||
reason: environment === "prod"
|
||||
? "No standardized prod D601 registry artifact consumer is implemented for this service; legacy maintenance-channel deployment is not allowed."
|
||||
: "No standardized dev deployment or artifact validation path is implemented for this service.",
|
||||
}
|
||||
: undefined,
|
||||
})),
|
||||
@@ -2491,7 +2497,7 @@ 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));
|
||||
return services.map((service) => service.id).filter((id) => !devApplySupportedServiceIds.has(id) && !devArtifactConsumerServiceIds.has(id));
|
||||
}
|
||||
|
||||
function blockedD601MaintenanceDeployServices(config: UniDeskConfig, manifest: DeployManifest, serviceId: string | null): string[] {
|
||||
@@ -2505,6 +2511,16 @@ 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.`;
|
||||
}
|
||||
|
||||
function selectedDevArtifactServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
|
||||
if (manifest.environment !== "dev") return [];
|
||||
return selectedEnvironmentServices(manifest, serviceId).filter((service) => devArtifactConsumerServiceIds.has(service.id));
|
||||
}
|
||||
|
||||
function selectedDevTargetServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
|
||||
if (manifest.environment !== "dev") return [];
|
||||
return selectedEnvironmentServices(manifest, serviceId).filter((service) => devApplySupportedServiceIds.has(service.id));
|
||||
}
|
||||
|
||||
function selectedEnvironmentServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
|
||||
const services = serviceId === null ? manifest.services : manifest.services.filter((service) => service.id === serviceId);
|
||||
if (serviceId !== null && services.length === 0) throw new Error(`deploy manifest does not contain service: ${serviceId}`);
|
||||
@@ -2532,10 +2548,12 @@ function prodArtifactUnsupportedResult(services: DeployManifestService[]): Recor
|
||||
};
|
||||
}
|
||||
|
||||
async function runProdArtifactApplyNow(manifest: DeployManifest, options: DeployOptions): Promise<Record<string, unknown>> {
|
||||
const selected = selectedEnvironmentServices(manifest, options.serviceId);
|
||||
const unsupported = selected.filter((service) => !prodArtifactConsumerServiceIds.has(service.id));
|
||||
if (unsupported.length > 0) return prodArtifactUnsupportedResult(unsupported);
|
||||
async function runArtifactConsumerApplyNow(
|
||||
manifest: DeployManifest,
|
||||
options: DeployOptions,
|
||||
environment: "dev" | "prod",
|
||||
selected: DeployManifestService[],
|
||||
): Promise<Record<string, unknown>> {
|
||||
const startedAt = nowIso();
|
||||
const results = [];
|
||||
for (const service of selected) {
|
||||
@@ -2545,11 +2563,12 @@ async function runProdArtifactApplyNow(manifest: DeployManifest, options: Deploy
|
||||
"--service", service.id,
|
||||
"--commit", commit,
|
||||
"--source-repo", service.repo,
|
||||
"--deploy-ref", `deploy.json#environments.${environment}.services.${service.id}`,
|
||||
"--timeout-ms", String(options.timeoutMs),
|
||||
"--run-now",
|
||||
...(options.dryRun ? ["--dry-run"] : []),
|
||||
];
|
||||
progressLine("prod-artifact-cd", options.dryRun ? "dry-run" : "reconcile", { serviceId: service.id, commit });
|
||||
progressLine(`${environment}-artifact-cd`, options.dryRun ? "dry-run" : "reconcile", { serviceId: service.id, commit });
|
||||
results.push(await runArtifactRegistryCommand(artifactArgs));
|
||||
const latest = results.at(-1) as Record<string, unknown>;
|
||||
if (latest.ok !== true) break;
|
||||
@@ -2557,7 +2576,7 @@ async function runProdArtifactApplyNow(manifest: DeployManifest, options: Deploy
|
||||
return {
|
||||
ok: results.every((result) => asRecord(result)?.ok === true),
|
||||
action: "apply",
|
||||
environment: "prod",
|
||||
environment,
|
||||
executor: "d601-registry-artifact-consumer",
|
||||
dryRun: options.dryRun,
|
||||
startedAt,
|
||||
@@ -2566,6 +2585,13 @@ async function runProdArtifactApplyNow(manifest: DeployManifest, options: Deploy
|
||||
};
|
||||
}
|
||||
|
||||
async function runProdArtifactApplyNow(manifest: DeployManifest, options: DeployOptions): Promise<Record<string, unknown>> {
|
||||
const selected = selectedEnvironmentServices(manifest, options.serviceId);
|
||||
const unsupported = selected.filter((service) => !prodArtifactConsumerServiceIds.has(service.id));
|
||||
if (unsupported.length > 0) return prodArtifactUnsupportedResult(unsupported);
|
||||
return runArtifactConsumerApplyNow(manifest, options, "prod", selected);
|
||||
}
|
||||
|
||||
function prodArtifactApplyJob(args: string[], options: DeployOptions): Record<string, unknown> {
|
||||
if (!options.runNow && options.dryRun) {
|
||||
throw new Error("deploy apply --env prod --dry-run should run in the foreground so the plan is visible immediately");
|
||||
@@ -2585,6 +2611,25 @@ function prodArtifactApplyJob(args: string[], options: DeployOptions): Record<st
|
||||
};
|
||||
}
|
||||
|
||||
function devArtifactApplyJob(args: string[], options: DeployOptions): Record<string, unknown> {
|
||||
if (!options.runNow && options.dryRun) {
|
||||
throw new Error("deploy apply --env dev --dry-run should run in the foreground so the plan is visible immediately");
|
||||
}
|
||||
const runArgs = args.includes("--run-now") ? args : [...args, "--run-now"];
|
||||
const command = [process.execPath, rootPath("scripts", "cli.ts"), "deploy", ...runArgs];
|
||||
const source = `${deployEnvironmentTargets.dev.gitRef}:deploy.json#environments.dev`;
|
||||
const job = startJob("deploy_dev_artifact_apply", command, `Validate dev artifact consumer from ${source}${options.serviceId === null ? "" : ` service=${options.serviceId}`}`);
|
||||
return {
|
||||
ok: true,
|
||||
mode: "async-job",
|
||||
executor: "d601-registry-artifact-consumer-dev-validation",
|
||||
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.",
|
||||
};
|
||||
}
|
||||
|
||||
async function runApplyNow(config: UniDeskConfig, manifest: DeployManifest, options: DeployOptions): Promise<Record<string, unknown>> {
|
||||
const selected = selectServices(config, manifest, options.serviceId);
|
||||
const startedAt = nowIso();
|
||||
@@ -2639,7 +2684,16 @@ 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 and frontend; unsupported selected services: ${unsupported.join(", ")}. Use ci run-dev-e2e for smoke verification.`);
|
||||
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.`);
|
||||
}
|
||||
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");
|
||||
}
|
||||
if (devArtifactServices.length > 0) {
|
||||
if (options.dryRun || options.runNow) return await runArtifactConsumerApplyNow(manifest, options, "dev", devArtifactServices);
|
||||
return devArtifactApplyJob(args, options);
|
||||
}
|
||||
if (config === null) throw new Error("deploy apply --env dev requires config.json");
|
||||
if (!options.dryRun) {
|
||||
|
||||
+3
-2
@@ -54,7 +54,7 @@ export function rootHelp(): unknown {
|
||||
{ command: "debug dispatch [providerId] [docker.ps|provider.upgrade|host.ssh|microservice.http|echo] [--wait-ms N]", description: "Submit a real internal-core dispatch request for CLI debugging." },
|
||||
{ command: "debug task <taskId|latest>", description: "Read a dispatched task record from internal core for CLI debugging." },
|
||||
{ command: "network perf [--service code-queue --path /api/tasks/overview?limit=30 --count N --concurrency N --label before|after]", description: "Benchmark frontend -> backend-core -> provider/adapter user-service networking and report latency/proxy-mode distributions." },
|
||||
{ command: "ci install|status|run|publish-backend-core|run-dev-e2e|logs", description: "Manage D601 k3s Tekton CI; publish-backend-core builds commit-pinned images in CI without deploying CD." },
|
||||
{ command: "ci install|status|run|publish-backend-core|publish-user-service|run-dev-e2e|logs", description: "Manage D601 k3s Tekton CI; artifact publish commands build commit-pinned images in CI without deploying CD." },
|
||||
{ command: "e2e run [--only pattern[,pattern...]] [--skip pattern[,pattern...]]", description: "Run selected public/internal/Playwright E2E checks; use --only for focused iteration and rerun without filters for final regression." },
|
||||
],
|
||||
};
|
||||
@@ -259,6 +259,7 @@ function artifactRegistryHelp(): unknown {
|
||||
"bun scripts/cli.ts artifact-registry health [--provider-id D601]",
|
||||
"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 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.",
|
||||
@@ -267,7 +268,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 and decision-center as standardized consumers",
|
||||
"deploy-service currently supports backend-core, baidu-netdisk, and decision-center as standardized consumers",
|
||||
"status and health use provider-gateway Host SSH readonly checks",
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user