feat: extend main-server artifact consumers

This commit is contained in:
Codex
2026-05-20 01:07:52 +00:00
parent b28f693fda
commit 93d9488d36
17 changed files with 519 additions and 34 deletions
+245 -4
View File
@@ -69,7 +69,7 @@ const defaultOptions: ArtifactRegistryOptions = {
sourceRepo: "https://github.com/pikasTech/unidesk",
deployRef: null,
};
const supportedArtifactConsumerServices = ["backend-core", "baidu-netdisk", "decision-center", "frontend"] as const;
const supportedArtifactConsumerServices = ["backend-core", "baidu-netdisk", "code-queue-mgr", "decision-center", "frontend", "oa-event-flow", "project-manager", "todo-note"] as const;
type SupportedArtifactConsumerService = typeof supportedArtifactConsumerServices[number];
const legacyDeployBackendCoreDisabled = true;
@@ -80,6 +80,10 @@ interface ArtifactConsumerSpec {
registryRepository: string;
dockerfile: string;
targets: Partial<Record<ArtifactDeployEnvironment, ArtifactConsumerTarget>>;
prodLiveApply: "enabled" | "supervisor-only" | "unsupported";
prodLiveBlockReason?: string;
runtimeVerification?: "strict" | "blocked";
runtimeVerificationBlockReason?: string;
}
interface ArtifactConsumerTarget {
@@ -113,6 +117,7 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
kind: "compose",
registryRepository: "unidesk/backend-core",
dockerfile: "src/components/backend-core/Dockerfile",
prodLiveApply: "enabled",
targets: {
prod: {
targetImage: "unidesk-backend-core",
@@ -134,7 +139,20 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
kind: "compose",
registryRepository: "unidesk/baidu-netdisk",
dockerfile: "src/components/microservices/baidu-netdisk/Dockerfile",
prodLiveApply: "enabled",
targets: {
dev: {
targetImage: "baidu-netdisk",
targetCommitImage: (commit: string) => `baidu-netdisk:${commit}`,
deployRef: "deploy.json#environments.dev.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,
},
},
prod: {
targetImage: "baidu-netdisk",
targetCommitImage: (commit: string) => `baidu-netdisk:${commit}`,
@@ -149,12 +167,48 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
},
},
},
"code-queue-mgr": {
serviceId: "code-queue-mgr",
environment: "prod",
kind: "compose",
registryRepository: "unidesk/code-queue-mgr",
dockerfile: "src/components/microservices/code-queue-mgr/Dockerfile",
prodLiveApply: "supervisor-only",
prodLiveBlockReason: "code-queue-mgr is the main-server Code Queue control-plane sidecar; live production apply requires explicit supervisor confirmation.",
targets: {
dev: {
targetImage: "code-queue-mgr",
targetCommitImage: (commit: string) => `code-queue-mgr:${commit}`,
deployRef: "deploy.json#environments.dev.services.code-queue-mgr",
compose: {
serviceName: "code-queue-mgr",
containerName: "code-queue-mgr-backend",
deployEnvPrefix: "UNIDESK_CODE_QUEUE_MGR_DEPLOY",
healthProbeCommand: "code-queue-mgr --print-health",
requireHealthCommit: true,
},
},
prod: {
targetImage: "code-queue-mgr",
targetCommitImage: (commit: string) => `code-queue-mgr:${commit}`,
deployRef: "deploy.json#environments.prod.services.code-queue-mgr",
compose: {
serviceName: "code-queue-mgr",
containerName: "code-queue-mgr-backend",
deployEnvPrefix: "UNIDESK_CODE_QUEUE_MGR_DEPLOY",
healthProbeCommand: "code-queue-mgr --print-health",
requireHealthCommit: true,
},
},
},
},
"decision-center": {
serviceId: "decision-center",
environment: "prod",
kind: "d601-k3s",
registryRepository: "unidesk/decision-center",
dockerfile: "src/components/microservices/decision-center/Dockerfile",
prodLiveApply: "enabled",
targets: {
dev: {
targetImage: "unidesk-decision-center:dev",
@@ -193,6 +247,7 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
kind: "compose",
registryRepository: "unidesk/frontend",
dockerfile: "src/components/frontend/Dockerfile",
prodLiveApply: "enabled",
targets: {
prod: {
targetImage: "unidesk-frontend",
@@ -208,12 +263,118 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
},
},
},
"oa-event-flow": {
serviceId: "oa-event-flow",
environment: "prod",
kind: "compose",
registryRepository: "unidesk/oa-event-flow",
dockerfile: "src/components/microservices/oa-event-flow/Dockerfile",
prodLiveApply: "enabled",
targets: {
dev: {
targetImage: "oa-event-flow",
targetCommitImage: (commit: string) => `oa-event-flow:${commit}`,
deployRef: "deploy.json#environments.dev.services.oa-event-flow",
compose: {
serviceName: "oa-event-flow",
containerName: "oa-event-flow-backend",
deployEnvPrefix: "UNIDESK_OA_EVENT_FLOW_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4255/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,
},
},
prod: {
targetImage: "oa-event-flow",
targetCommitImage: (commit: string) => `oa-event-flow:${commit}`,
deployRef: "deploy.json#environments.prod.services.oa-event-flow",
compose: {
serviceName: "oa-event-flow",
containerName: "oa-event-flow-backend",
deployEnvPrefix: "UNIDESK_OA_EVENT_FLOW_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4255/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,
},
},
},
},
"project-manager": {
serviceId: "project-manager",
environment: "prod",
kind: "compose",
registryRepository: "unidesk/project-manager",
dockerfile: "src/components/microservices/project-manager/Dockerfile",
prodLiveApply: "enabled",
targets: {
dev: {
targetImage: "project-manager",
targetCommitImage: (commit: string) => `project-manager:${commit}`,
deployRef: "deploy.json#environments.dev.services.project-manager",
compose: {
serviceName: "project-manager",
containerName: "project-manager-backend",
deployEnvPrefix: "UNIDESK_PROJECT_MANAGER_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4233/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,
},
},
prod: {
targetImage: "project-manager",
targetCommitImage: (commit: string) => `project-manager:${commit}`,
deployRef: "deploy.json#environments.prod.services.project-manager",
compose: {
serviceName: "project-manager",
containerName: "project-manager-backend",
deployEnvPrefix: "UNIDESK_PROJECT_MANAGER_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4233/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,
},
},
},
},
"todo-note": {
serviceId: "todo-note",
environment: "prod",
kind: "compose",
registryRepository: "unidesk/todo-note",
dockerfile: "Dockerfile",
prodLiveApply: "unsupported",
prodLiveBlockReason: "todo-note source is external to this repository and the current checked-in contract cannot prove /api/health deploy.commit/deploy.requestedCommit support.",
runtimeVerification: "blocked",
runtimeVerificationBlockReason: "todo-note source is external to this repository and the current checked-in contract cannot prove /api/health deploy.commit/deploy.requestedCommit support.",
targets: {
dev: {
targetImage: "todo-note",
targetCommitImage: (commit: string) => `todo-note:${commit}`,
deployRef: "deploy.json#environments.dev.services.todo-note",
compose: {
serviceName: "todo-note",
containerName: "todo-note-backend",
deployEnvPrefix: "UNIDESK_TODO_NOTE_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4211/api/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,
},
},
prod: {
targetImage: "todo-note",
targetCommitImage: (commit: string) => `todo-note:${commit}`,
deployRef: "deploy.json#environments.prod.services.todo-note",
compose: {
serviceName: "todo-note",
containerName: "todo-note-backend",
deployEnvPrefix: "UNIDESK_TODO_NOTE_DEPLOY",
healthProbeCommand: "bun -e \"fetch('http://127.0.0.1:4211/api/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",
prodLiveApply: "enabled",
targets: {
dev: {
targetImage: "unidesk-frontend:dev",
@@ -649,6 +810,58 @@ function unsupportedEnvironment(spec: ArtifactConsumerSpec, options: ArtifactReg
};
}
function artifactConsumerLiveBlock(spec: ArtifactConsumerSpec, options: ArtifactRegistryOptions): Record<string, unknown> | null {
const environment = options.environment ?? "prod";
if (spec.runtimeVerification === "blocked") {
return {
ok: false,
supported: false,
liveApplyAllowed: false,
error: "runtime-verification-blocked",
serviceId: spec.serviceId,
environment,
providerId: options.providerId,
reason: spec.runtimeVerificationBlockReason ?? `${spec.serviceId} does not yet satisfy strict runtime deploy commit verification.`,
requiredBeforeLiveApply: [
"runtime Compose env injects deploy commit/requestedCommit metadata",
"service health reports deploy.commit and deploy.requestedCommit for strict verification",
],
policy: "artifact CD must not accept a healthy old service or silently fall back to legacy rebuild paths",
};
}
if (environment !== "prod" || spec.prodLiveApply === "enabled") return null;
if (spec.prodLiveApply === "supervisor-only") {
return {
ok: false,
supported: true,
liveApplyAllowed: false,
error: "supervisor-confirmation-required",
serviceId: spec.serviceId,
environment,
providerId: options.providerId,
reason: spec.prodLiveBlockReason ?? `${spec.serviceId} production artifact apply requires supervisor confirmation.`,
dryRunCommandShape: `bun scripts/cli.ts artifact-registry deploy-service --env prod --service ${spec.serviceId} --commit <full-sha> --dry-run`,
policy: "worker automation must not perform live production apply for this infrastructure control-plane service",
};
}
return {
ok: false,
supported: false,
liveApplyAllowed: false,
error: "artifact-consumer-blocked",
serviceId: spec.serviceId,
environment,
providerId: options.providerId,
reason: spec.prodLiveBlockReason ?? `${spec.serviceId} does not yet satisfy the artifact consumer runtime verification contract.`,
requiredBeforeLiveApply: [
"CI can publish a commit-pinned image with matching service id, source commit, and Dockerfile labels",
"runtime Compose env injects deploy commit/requestedCommit metadata",
"service health reports deploy.commit and deploy.requestedCommit for strict verification",
],
policy: "do not silently fall back to server rebuild, dirty worktrees, mutable tags, or source builds on the runtime target",
};
}
function artifactImageRef(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, commit: string): string {
return `127.0.0.1:${options.port}/${spec.registryRepository}:${commit}`;
}
@@ -1040,7 +1253,9 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
"cat \"$health_json\"",
...(target.compose.requireHealthCommit ? [
"health_commit=$(python3 -c 'import json,sys; print(((json.load(open(sys.argv[1])).get(\"deploy\") or {}).get(\"commit\") or \"\"))' \"$health_json\")",
"health_requested_commit=$(python3 -c 'import json,sys; print(((json.load(open(sys.argv[1])).get(\"deploy\") or {}).get(\"requestedCommit\") or \"\"))' \"$health_json\")",
`test "$health_commit" = ${shellQuote(commit)}`,
`test "$health_requested_commit" = ${shellQuote(commit)}`,
] : []),
].join("\n");
const deploy = runCommand(["bash", "-lc", composeLockScript(upScript)], repoRoot, { timeoutMs: Math.max(options.timeoutMs, 300_000) });
@@ -1072,8 +1287,10 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
running: running.stdout.trim(),
validation: {
liveCommit: commit,
liveRequestedCommit: target.compose.requireHealthCommit ? commit : "not-required",
imageLabelCommit: commit,
serviceHealthCommit: target.compose.requireHealthCommit ? commit : "not-required",
serviceHealthRequestedCommit: target.compose.requireHealthCommit ? commit : "not-required",
healthyOldVersionAccepted: false,
},
rollback: {
@@ -1125,12 +1342,14 @@ function legacyDeployBackendCoreResult(options: ArtifactRegistryOptions): Record
function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget, commit: string): Record<string, unknown> {
const environment = options.environment ?? "prod";
const verificationBlocked = spec.runtimeVerification === "blocked";
const sourceImage = artifactImageRef(options, spec, commit);
const common = {
ok: true,
supported: true,
ok: !verificationBlocked,
supported: !verificationBlocked,
dryRun: true,
mutation: false,
error: verificationBlocked ? "runtime-verification-blocked" : undefined,
environment,
providerId: options.providerId,
serviceId: spec.serviceId,
@@ -1148,6 +1367,11 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
url: `http://127.0.0.1:${options.port}/v2/${spec.registryRepository}/manifests/${commit}`,
},
boundary: `${environment} 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`,
liveApply: {
policy: spec.prodLiveApply,
allowed: !verificationBlocked && (environment !== "prod" || spec.prodLiveApply === "enabled"),
reason: spec.runtimeVerificationBlockReason ?? spec.prodLiveBlockReason ?? null,
},
};
if (spec.kind === "compose") {
if (target.compose === undefined) throw new Error(`${spec.serviceId} missing compose artifact consumer config`);
@@ -1166,7 +1390,9 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
"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",
target.compose.requireHealthCommit
verificationBlocked
? `blocked: ${spec.runtimeVerificationBlockReason}`
: target.compose.requireHealthCommit
? `${spec.serviceId} /health succeeds and reports deploy.commit matching the artifact commit`
: `${spec.serviceId} /health succeeds for the recreated container`,
],
@@ -1430,6 +1656,8 @@ async function deployServiceNow(options: ArtifactRegistryOptions): Promise<Recor
const target = artifactConsumerTarget(spec, options.environment);
if (target === null) return unsupportedEnvironment(spec, options);
if (options.dryRun) return dryRunArtifactConsumerPlan(options, spec, target, options.commit);
const liveBlock = artifactConsumerLiveBlock(spec, options);
if (liveBlock !== null) return liveBlock;
if (spec.kind === "compose") return deployComposeArtifactNow(options, spec, target);
return deployD601K3sArtifactNow(options, spec, target);
}
@@ -1442,6 +1670,8 @@ function deployServiceJob(args: string[], options: ArtifactRegistryOptions): Rec
const target = artifactConsumerTarget(spec, options.environment);
if (target === null) return unsupportedEnvironment(spec, options);
if (options.dryRun) return dryRunArtifactConsumerPlan(options, spec, target, options.commit);
const liveBlock = artifactConsumerLiveBlock(spec, options);
if (liveBlock !== null) return liveBlock;
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.environment ?? "prod"} ${options.serviceId} artifact ${options.commit} from D601 registry`);
@@ -1478,6 +1708,10 @@ function localHelp(): Record<string, unknown> {
"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 --env dev --service decision-center --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service decision-center --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service project-manager --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service oa-event-flow --commit <full-sha> [--dry-run] [--run-now] [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service code-queue-mgr --commit <full-sha> --dry-run [--provider-id D601]",
"bun scripts/cli.ts artifact-registry deploy-service --env prod --service todo-note --commit <full-sha> --dry-run [--provider-id D601]",
],
firstStage: "install now writes the rendered systemd/Compose/config files and starts the registry",
artifactConsumers: {
@@ -1489,10 +1723,17 @@ function localHelp(): Record<string, unknown> {
"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",
"bun scripts/cli.ts deploy apply --env prod --service project-manager",
"bun scripts/cli.ts deploy apply --env prod --service oa-event-flow",
"bun scripts/cli.ts deploy apply --env prod --service code-queue-mgr --dry-run",
],
devCommands: [
"bun scripts/cli.ts deploy apply --env dev --service frontend",
"bun scripts/cli.ts deploy apply --env dev --service decision-center",
"bun scripts/cli.ts deploy apply --env dev --service project-manager --dry-run",
"bun scripts/cli.ts deploy apply --env dev --service oa-event-flow --dry-run",
"bun scripts/cli.ts deploy apply --env dev --service code-queue-mgr --dry-run",
"bun scripts/cli.ts deploy apply --env dev --service todo-note --dry-run",
],
rollbackShape: "rerun the same artifact consumer with a previous commit-pinned image",
},
+14 -3
View File
@@ -29,6 +29,14 @@ const ciRuntimeImages = [
"alpine/git:2.45.2",
ciCodeQueueImage,
];
const publishUserServiceArtifactAllowedServiceIds = new Set([
"baidu-netdisk",
"code-queue-mgr",
"decision-center",
"frontend",
"oa-event-flow",
"project-manager",
]);
interface CiOptions {
repoUrl: string;
@@ -213,7 +221,10 @@ function requireSupportedUserService(config: UniDeskConfig, serviceId: string):
const isMainServerDirectService = service.providerId === "main-server"
&& service.development.providerId === "main-server"
&& service.deployment.mode === "unidesk-direct";
if (!isD601K3sService && !isMainServerDirectService) {
const isMainServerInternalSidecar = service.providerId === "main-server"
&& service.development.providerId === "main-server"
&& service.deployment.mode === "internal-sidecar";
if (!isD601K3sService && !isMainServerDirectService && !isMainServerInternalSidecar) {
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;
@@ -1539,8 +1550,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(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");
if (!publishUserServiceArtifactAllowedServiceIds.has(serviceId)) {
throw new Error(`ci publish-user-service currently allows only ${Array.from(publishUserServiceArtifactAllowedServiceIds).join(", ")} until each user-service Dockerfile contract is reviewed`);
}
return publishUserServiceArtifact(config, {
repoUrl: target.repoUrl,
+56 -13
View File
@@ -136,9 +136,13 @@ const nativeK3sCtrAddress = "/run/k3s/containerd/containerd.sock";
const unideskRepoUrl = "https://github.com/pikasTech/unidesk";
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", "decision-center", "frontend"]);
const prodArtifactConsumerServiceIds = new Set<string>(["backend-core", "baidu-netdisk", "decision-center", "frontend"]);
const prodForbiddenTargetSideBuildServiceIds = prodArtifactConsumerServiceIds;
const devArtifactConsumerServiceIds = new Set<string>(["baidu-netdisk", "code-queue-mgr", "decision-center", "frontend", "oa-event-flow", "project-manager", "todo-note"]);
const devArtifactConsumerProdDesiredFallbackServiceIds = new Set<string>(["code-queue-mgr", "oa-event-flow", "project-manager", "todo-note"]);
const prodArtifactConsumerServiceIds = new Set<string>(["backend-core", "baidu-netdisk", "code-queue-mgr", "decision-center", "frontend", "oa-event-flow", "project-manager", "todo-note"]);
const prodArtifactLiveApplyBlockedServiceIds = new Map<string, string>([
["code-queue-mgr", "code-queue-mgr is the main-server Code Queue control-plane sidecar; live production apply requires explicit supervisor confirmation."],
["todo-note", "todo-note source is external to this repository and the current checked-in contract cannot prove /api/health deploy.commit/deploy.requestedCommit support."],
]);
const deployEnvironmentTargets: Record<DeployEnvironment, DeployEnvironmentTarget> = {
dev: {
environment: "dev",
@@ -201,7 +205,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 target-side rollout plus reviewed artifact consumers for frontend, baidu-netdisk, and decision-center; prod apply uses the D601 registry artifact consumer for backend-core, frontend, 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 reviewed artifact consumers for frontend, baidu-netdisk, decision-center, project-manager, oa-event-flow, code-queue-mgr validation, while todo-note remains dry-run only. Prod apply uses the D601 registry artifact consumer for reviewed services; code-queue-mgr live apply is supervisor-gated and todo-note remains blocked." },
{ 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." },
@@ -569,6 +573,11 @@ function readEnvironmentDeployManifest(environment: DeployEnvironment): { manife
};
}
function readProdDeployManifestSnapshot(): DeployManifest {
const raw = runGitOrThrow(["show", `${deployEnvironmentTargets.prod.gitRef}:deploy.json`], repoRoot, `failed to read ${deployEnvironmentTargets.prod.gitRef}:deploy.json`).stdout;
return parseDeployManifest(JSON.parse(raw) as unknown, `${deployEnvironmentTargets.prod.gitRef}:deploy.json#environments.prod`, "prod");
}
async function readDeployManifest(file: string): Promise<DeployManifest> {
const path = resolve(repoRoot, file);
if (!existsSync(path)) throw new Error(`deploy manifest not found: ${path}`);
@@ -2607,6 +2616,15 @@ function environmentDryRunPlan(
reason: "No standardized dev D601 registry artifact consumer is implemented for this service; legacy maintenance-channel deployment is not allowed.",
}
: undefined,
liveApply: environment === "prod" && prodArtifactLiveApplyBlockedServiceIds.has(service.id)
? {
allowed: false,
reason: prodArtifactLiveApplyBlockedServiceIds.get(service.id),
dryRunOnly: true,
}
: environment === "prod"
? { allowed: prodArtifactConsumerServiceIds.has(service.id) }
: undefined,
})),
unsupported: environment === "prod" ? prodArtifactUnsupported : devUnsupported,
};
@@ -2615,6 +2633,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);
if (serviceId !== null && services.length === 0 && devArtifactConsumerProdDesiredFallbackServiceIds.has(serviceId)) return [];
return services.map((service) => service.id).filter((id) => !devApplySupportedServiceIds.has(id) && !devArtifactConsumerServiceIds.has(id));
}
@@ -2636,6 +2655,7 @@ function selectedDevArtifactServices(manifest: DeployManifest, serviceId: string
function selectedDevTargetServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
if (manifest.environment !== "dev") return [];
if (serviceId !== null && devArtifactConsumerProdDesiredFallbackServiceIds.has(serviceId) && !manifest.services.some((service) => service.id === serviceId)) return [];
return selectedEnvironmentServices(manifest, serviceId)
.filter((service) => devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id));
}
@@ -2646,6 +2666,14 @@ function selectedEnvironmentServices(manifest: DeployManifest, serviceId: string
return services;
}
function selectedDevArtifactServicesWithProdFallback(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
if (manifest.environment !== "dev" || serviceId === null) return selectedDevArtifactServices(manifest, serviceId);
const selected = manifest.services.filter((service) => service.id === serviceId && devArtifactConsumerServiceIds.has(service.id));
if (selected.length > 0 || !devArtifactConsumerProdDesiredFallbackServiceIds.has(serviceId)) return selected;
const prodManifest = readProdDeployManifestSnapshot();
return prodManifest.services.filter((service) => service.id === serviceId);
}
function devUnsupportedServices(manifest: DeployManifest, serviceId: string | null): DeployManifestService[] {
if (manifest.environment !== "dev") return [];
return selectedEnvironmentServices(manifest, serviceId).filter((service) => !devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id));
@@ -2672,20 +2700,21 @@ function prodArtifactUnsupportedResult(services: DeployManifestService[]): Recor
};
}
function prodArtifactConsumerLocalManifestResult(services: DeployManifestService[]): Record<string, unknown> {
function prodArtifactLiveApplyBlockedResult(services: DeployManifestService[]): Record<string, unknown> {
return {
ok: false,
supported: false,
error: "prod-artifact-consumer-local-manifest-blocked",
supported: true,
error: "live-prod-apply-blocked",
services: services.map((service) => ({
id: service.id,
repo: service.repo,
commitId: service.commitId,
supported: true,
reason: "This service has a reviewed artifact consumer; production source-build deploy from a local manifest is blocked.",
liveApplyAllowed: false,
reason: prodArtifactLiveApplyBlockedServiceIds.get(service.id) ?? "live production artifact apply is blocked by policy",
})),
policy: "prod artifact consumers must enter through deploy apply --env prod so CD consumes an existing commit-pinned registry image and never falls back to source build or a dirty worktree",
commandShape: "bun scripts/cli.ts deploy apply --env prod --service <service-id> --commit <full-sha>",
policy: "prod dry-run/plan is available, but worker automation must not run live production apply for these services",
dryRunCommandShape: "bun scripts/cli.ts deploy apply --env prod --service <id> --dry-run",
};
}
@@ -2705,7 +2734,7 @@ async function runArtifactConsumerApplyNow(
"--commit", commit,
"--source-repo", service.repo,
"--deploy-ref", `${deployEnvironmentTargets[environment].gitRef}:deploy.json#environments.${environment}.services.${service.id}`,
...(environment === "prod" || service.id === "frontend" || service.id === "decision-center" ? ["--env", environment] : []),
"--env", environment,
"--timeout-ms", String(options.timeoutMs),
"--run-now",
...(options.dryRun ? ["--dry-run"] : []),
@@ -2731,6 +2760,10 @@ async function runProdArtifactApplyNow(manifest: DeployManifest, options: Deploy
const selected = selectedEnvironmentServices(manifest, options.serviceId);
const unsupported = selected.filter((service) => !prodArtifactConsumerServiceIds.has(service.id));
if (unsupported.length > 0) return prodArtifactUnsupportedResult(unsupported);
if (!options.dryRun) {
const blocked = selected.filter((service) => prodArtifactLiveApplyBlockedServiceIds.has(service.id));
if (blocked.length > 0) return prodArtifactLiveApplyBlockedResult(blocked);
}
return runArtifactConsumerApplyNow(manifest, options, "prod", selected);
}
@@ -2739,6 +2772,16 @@ function prodArtifactApplyJob(args: string[], options: DeployOptions): Record<st
throw new Error("deploy apply --env prod --dry-run should run in the foreground so the plan is visible immediately");
}
const runArgs = args.includes("--run-now") ? args : [...args, "--run-now"];
if (options.serviceId === null) {
throw new Error("deploy apply --env prod without --service is not allowed for mixed artifact consumers; use --service to avoid applying supervisor-gated services accidentally");
}
if (prodArtifactLiveApplyBlockedServiceIds.has(options.serviceId)) {
return prodArtifactLiveApplyBlockedResult([{
id: options.serviceId,
repo: "",
commitId: options.commitOverride ?? "",
}]);
}
const command = [process.execPath, rootPath("scripts", "cli.ts"), "deploy", ...runArgs];
const source = `${deployEnvironmentTargets.prod.gitRef}:deploy.json#environments.prod`;
const job = startJob("deploy_prod_artifact_apply", command, `Deploy prod artifact consumer from ${source}${options.serviceId === null ? "" : ` service=${options.serviceId}`}`);
@@ -2826,9 +2869,9 @@ 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 backend-core target-side rollout plus frontend/baidu-netdisk/decision-center artifact consumers; 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/decision-center/project-manager/oa-event-flow/code-queue-mgr artifact consumers; unsupported selected services: ${unsupported.join(", ")}. todo-note is dry-run only until runtime verification is proven. Use ci run-dev-e2e for smoke verification.`);
}
const devArtifactServices = selectedDevArtifactServices(manifest, options.serviceId);
const devArtifactServices = selectedDevArtifactServicesWithProdFallback(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 consumer services with target-side rollout services in one invocation; pass --service");
+20
View File
@@ -116,6 +116,26 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
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_PROJECT_MANAGER_DEPLOY_REF: runtimeSecret("UNIDESK_PROJECT_MANAGER_DEPLOY_REF"),
UNIDESK_PROJECT_MANAGER_DEPLOY_SERVICE_ID: runtimeSecret("UNIDESK_PROJECT_MANAGER_DEPLOY_SERVICE_ID") || "project-manager",
UNIDESK_PROJECT_MANAGER_DEPLOY_REPO: runtimeSecret("UNIDESK_PROJECT_MANAGER_DEPLOY_REPO"),
UNIDESK_PROJECT_MANAGER_DEPLOY_COMMIT: runtimeSecret("UNIDESK_PROJECT_MANAGER_DEPLOY_COMMIT"),
UNIDESK_PROJECT_MANAGER_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_PROJECT_MANAGER_DEPLOY_REQUESTED_COMMIT"),
UNIDESK_OA_EVENT_FLOW_DEPLOY_REF: runtimeSecret("UNIDESK_OA_EVENT_FLOW_DEPLOY_REF"),
UNIDESK_OA_EVENT_FLOW_DEPLOY_SERVICE_ID: runtimeSecret("UNIDESK_OA_EVENT_FLOW_DEPLOY_SERVICE_ID") || "oa-event-flow",
UNIDESK_OA_EVENT_FLOW_DEPLOY_REPO: runtimeSecret("UNIDESK_OA_EVENT_FLOW_DEPLOY_REPO"),
UNIDESK_OA_EVENT_FLOW_DEPLOY_COMMIT: runtimeSecret("UNIDESK_OA_EVENT_FLOW_DEPLOY_COMMIT"),
UNIDESK_OA_EVENT_FLOW_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_OA_EVENT_FLOW_DEPLOY_REQUESTED_COMMIT"),
UNIDESK_CODE_QUEUE_MGR_DEPLOY_REF: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DEPLOY_REF"),
UNIDESK_CODE_QUEUE_MGR_DEPLOY_SERVICE_ID: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DEPLOY_SERVICE_ID") || "code-queue-mgr",
UNIDESK_CODE_QUEUE_MGR_DEPLOY_REPO: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DEPLOY_REPO"),
UNIDESK_CODE_QUEUE_MGR_DEPLOY_COMMIT: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DEPLOY_COMMIT"),
UNIDESK_CODE_QUEUE_MGR_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_CODE_QUEUE_MGR_DEPLOY_REQUESTED_COMMIT"),
UNIDESK_TODO_NOTE_DEPLOY_REF: runtimeSecret("UNIDESK_TODO_NOTE_DEPLOY_REF"),
UNIDESK_TODO_NOTE_DEPLOY_SERVICE_ID: runtimeSecret("UNIDESK_TODO_NOTE_DEPLOY_SERVICE_ID") || "todo-note",
UNIDESK_TODO_NOTE_DEPLOY_REPO: runtimeSecret("UNIDESK_TODO_NOTE_DEPLOY_REPO"),
UNIDESK_TODO_NOTE_DEPLOY_COMMIT: runtimeSecret("UNIDESK_TODO_NOTE_DEPLOY_COMMIT"),
UNIDESK_TODO_NOTE_DEPLOY_REQUESTED_COMMIT: runtimeSecret("UNIDESK_TODO_NOTE_DEPLOY_REQUESTED_COMMIT"),
UNIDESK_AUTH_USERNAME: config.auth.username,
UNIDESK_AUTH_PASSWORD: config.auth.password,
UNIDESK_SESSION_SECRET: config.auth.sessionSecret,
+2 -2
View File
@@ -270,8 +270,8 @@ function artifactRegistryHelp(): unknown {
"registry endpoint is D601 loopback 127.0.0.1:5000 only",
"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 is a deprecated compatibility name; use deploy apply --env prod --service backend-core so standard artifact-consumer guardrails run first",
"deploy-service currently supports backend-core, baidu-netdisk, prod/dev frontend, and decision-center as standardized consumers",
"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, prod/dev frontend, decision-center, project-manager, oa-event-flow, and code-queue-mgr as standardized consumers; todo-note is runtime-verification blocked",
"status and health use provider-gateway Host SSH readonly checks",
],
legacyEntrypoints: {