feat: extend main-server artifact consumers
This commit is contained in:
@@ -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
@@ -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
@@ -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");
|
||||
|
||||
@@ -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
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user