fix: expose deploy artifact matrix plan fields

This commit is contained in:
Codex
2026-05-20 21:04:57 +00:00
parent a8e7cb8a13
commit f4c0f88312
4 changed files with 265 additions and 39 deletions
+2
View File
@@ -112,6 +112,8 @@ Production `code-queue-mgr` is a separate main-server Compose sidecar artifact c
`bun scripts/cli.ts deploy plan --env dev [--service <id>]` reads `origin/master:deploy.json#environments.dev` and prints a dry-run environment plan without checking or mutating live runtime resources. `deploy check --env dev` uses the same dry-run environment plan. `--env prod` is available for parity as a dry-run planning path; it reads `origin/master:deploy.json#environments.prod` and must not use a dirty local `deploy.json`.
Environment plan output must be sufficient to review the artifact matrix without running a live apply. Each service item includes `deploymentPath`, `artifactConsumer.consumerKind`, `artifactConsumer.registryImage`, `artifactConsumer.noRuntimeSourceBuild`, `artifactConsumer.dryRunOnly`, `target`, `validation` and `liveApply` where relevant. `consumerKind=d601-direct-compose` means the reviewed consumer touches only the D601 Docker/Compose service and private health path; `consumerKind=d601-k3s-managed` means the reviewed consumer imports the artifact into native k3s/containerd and verifies through the Kubernetes API service proxy; `consumerKind=main-server-compose` means the reviewed consumer streams or loads the D601 artifact into the main-server Compose service; `consumerKind=d601-dev-target-side-build` is reserved for the controlled dev backend-core source-build exception. Artifact consumer plan items must explicitly report `noRuntimeSourceBuild=true` and list forbidden build/public exposure actions. Blocked or gated services must keep structured `dryRunOnly` / `blockedReason` output, for example `met-nonlinear` `runtime-verification-blocked` and `k3sctl-adapter` supervisor-only production apply.
`bun scripts/cli.ts deploy apply [--file deploy.json | --env dev|prod] [--service <id>] [--commit <full-sha>] [--dry-run] [--force]` starts an asynchronous job only for supported targets. Use `bun scripts/cli.ts job status <jobId> --tail-bytes 30000` to observe progress. `--dry-run` resolves the same plan but does not build or replace runtime objects. `--force` rebuilds even when the live commit matches. Environment apply is not the dev e2e trigger; use `bun scripts/cli.ts ci run-dev-e2e` for the Git-controlled temporary namespace smoke flow. `--env dev` apply is enabled for persistent D601 `backend-core` target-side rollout and for `frontend`/`baidu-netdisk`/`decision-center`/`mdtodo`/`claudeqq`/dev-only `code-queue`/`project-manager`/`oa-event-flow`/`code-queue-mgr`/`todo-note`/`findjob`/`pipeline`/`met-nonlinear` artifact consumers. `--env prod` apply exposes the D601 registry artifact consumer for `backend-core`, `frontend`, `baidu-netdisk`, `decision-center`, `mdtodo`, `claudeqq`, `project-manager`, `oa-event-flow`, `todo-note`, `findjob`, `pipeline` and `met-nonlinear`; `code-queue-mgr` prod live apply is supervisor-gated and `k3sctl-adapter` is plan/dry-run only. `--commit` may override one selected reviewed artifact consumer in either dev or prod, for example `deploy apply --env dev --service frontend --commit <release-v1-full-sha>`, and the image must already exist as `127.0.0.1:5000/unidesk/<service-id>:<commit>`. Unsupported prod services, especially `code-queue`, return a structured `unsupported` payload instead of silently falling back to a maintenance-channel source build.
All deploy commands output JSON. Long operations must use `.state/jobs/` and bounded log tails; no deploy path may succeed with missing progress output.
@@ -0,0 +1,74 @@
import { spawnSync } from "node:child_process";
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
}
function asRecord(value: unknown, label: string): Record<string, unknown> {
assertCondition(typeof value === "object" && value !== null && !Array.isArray(value), `${label} must be an object`, value);
return value as Record<string, unknown>;
}
function runDeployPlan(environment: "dev" | "prod", serviceId: string): Record<string, unknown> {
const result = spawnSync("bun", ["scripts/cli.ts", "deploy", "plan", "--env", environment, "--service", serviceId], {
cwd: process.cwd(),
encoding: "utf8",
maxBuffer: 8 * 1024 * 1024,
});
assertCondition(result.status === 0, `deploy plan should exit 0 for ${environment}/${serviceId}`, {
status: result.status,
stdout: result.stdout.slice(-2000),
stderr: result.stderr.slice(-2000),
});
const envelope = asRecord(JSON.parse(result.stdout) as unknown, "cli envelope");
assertCondition(envelope.ok === true, `deploy plan envelope should be ok for ${environment}/${serviceId}`, envelope);
const data = asRecord(envelope.data, "deploy plan data");
const services = Array.isArray(data.services) ? data.services : [];
assertCondition(services.length === 1, `deploy plan should return one service for ${environment}/${serviceId}`, data);
return asRecord(services[0], "service plan");
}
function listIncludes(value: unknown, expected: string): boolean {
return Array.isArray(value) && value.some((item) => item === expected);
}
const findjob = runDeployPlan("dev", "findjob");
const findjobArtifact = asRecord(findjob.artifactConsumer, "findjob artifactConsumer");
const findjobTarget = asRecord(findjob.target, "findjob target");
assertCondition(findjobArtifact.consumerKind === "d601-direct-compose", "findjob dev must be a D601 direct Compose artifact consumer", findjobArtifact);
assertCondition(findjobArtifact.noRuntimeSourceBuild === true, "findjob dry-run must declare no runtime source build", findjobArtifact);
assertCondition(findjobTarget.runtimeHost === "D601", "findjob target should be D601", findjobTarget);
assertCondition(findjobTarget.composeService === "server", "findjob compose service should be server", findjobTarget);
assertCondition(listIncludes(findjobTarget.forbiddenActions, "docker compose build"), "findjob plan should forbid Compose build", findjobTarget);
const mdtodo = runDeployPlan("prod", "mdtodo");
const mdtodoArtifact = asRecord(mdtodo.artifactConsumer, "mdtodo artifactConsumer");
const mdtodoTarget = asRecord(mdtodo.target, "mdtodo target");
assertCondition(mdtodoArtifact.consumerKind === "d601-k3s-managed", "mdtodo prod must be a D601 k3s-managed artifact consumer", mdtodoArtifact);
assertCondition(mdtodoArtifact.noRuntimeSourceBuild === true, "mdtodo dry-run must declare no runtime source build", mdtodoArtifact);
assertCondition(mdtodoTarget.namespace === "unidesk", "mdtodo prod target namespace should be unidesk", mdtodoTarget);
assertCondition(listIncludes(mdtodoTarget.forbiddenActions, "NodePort"), "mdtodo plan should forbid NodePort", mdtodoTarget);
const metNonlinear = runDeployPlan("prod", "met-nonlinear");
const metArtifact = asRecord(metNonlinear.artifactConsumer, "met-nonlinear artifactConsumer");
assertCondition(metArtifact.consumerKind === "d601-direct-compose", "met-nonlinear should remain D601 direct Compose", metArtifact);
assertCondition(metArtifact.dryRunOnly === true, "met-nonlinear must remain dry-run only", metArtifact);
assertCondition(String(metArtifact.blockedReason ?? "").includes("runtime-verification-blocked"), "met-nonlinear blocked reason should mention runtime verification", metArtifact);
const k3sctl = runDeployPlan("prod", "k3sctl-adapter");
const k3sctlArtifact = asRecord(k3sctl.artifactConsumer, "k3sctl-adapter artifactConsumer");
const k3sctlTarget = asRecord(k3sctl.target, "k3sctl-adapter target");
assertCondition(k3sctlArtifact.consumerKind === "d601-direct-compose", "k3sctl-adapter should be D601 direct Compose", k3sctlArtifact);
assertCondition(k3sctlArtifact.dryRunOnly === true, "k3sctl-adapter must be dry-run only for worker automation", k3sctlArtifact);
assertCondition(String(k3sctlArtifact.blockedReason ?? "").includes("supervisor"), "k3sctl-adapter blocked reason should mention supervisor confirmation", k3sctlArtifact);
assertCondition(k3sctlTarget.composeService === "k3sctl-adapter", "k3sctl target service should be k3sctl-adapter", k3sctlTarget);
process.stdout.write(`${JSON.stringify({
ok: true,
checks: [
"deploy plan distinguishes D601 direct Compose from D601 k3s-managed artifact consumers",
"deploy plan exposes no-runtime-source-build and forbidden build/public-port actions",
"met-nonlinear remains runtime-verification-blocked",
"k3sctl-adapter remains supervisor-only dry-run",
],
}, null, 2)}\n`);
+3
View File
@@ -284,6 +284,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
fileItem("scripts/code-queue-pr-preflight-contract-test.ts"),
fileItem("scripts/src/ci.ts"),
fileItem("scripts/src/e2e.ts"),
fileItem("scripts/deploy-artifact-matrix-contract-test.ts"),
fileItem("scripts/code-queue-prompt-observation-test.ts"),
fileItem("scripts/gh-cli-issue-guard-contract-test.ts"),
fileItem("scripts/gh-cli-pr-contract-test.ts"),
@@ -302,6 +303,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
items.push(commandItem("code-queue:issue3-diagnostics-and-image-preflight", ["bun", "scripts/code-queue-issue3-regression-test.ts"], 30_000));
items.push(commandItem("code-queue:trace-summary-contract", ["bun", "scripts/code-queue-trace-summary-contract-test.ts"], 30_000));
items.push(commandItem("code-queue:pr-preflight-contract", ["bun", "scripts/code-queue-pr-preflight-contract-test.ts"], 30_000));
items.push(commandItem("deploy:artifact-matrix-contract", ["bun", "scripts/deploy-artifact-matrix-contract-test.ts"], 30_000));
items.push(commandItem("code-queue:active-run-heartbeat-visible", ["bun", "scripts/code-queue-liveness-diagnostics-test.ts", "--only", "code-queue:active-run-heartbeat-visible"], 30_000));
items.push(commandItem("code-queue:trace-gap-not-stale", ["bun", "scripts/code-queue-liveness-diagnostics-test.ts", "--only", "code-queue:trace-gap-not-stale"], 30_000));
items.push(commandItem("code-queue:stale-active-owner-expired", ["bun", "scripts/code-queue-liveness-diagnostics-test.ts", "--only", "code-queue:stale-active-owner-expired"], 30_000));
@@ -318,6 +320,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
items.push(skippedItem("code-queue:issue3-diagnostics-and-image-preflight", "Code Queue issue #3 regression fixtures are opt-in with script checks", "--scripts-typecheck or --full"));
items.push(skippedItem("code-queue:trace-summary-contract", "Code Queue trace summary contract is opt-in with script checks", "--scripts-typecheck or --full"));
items.push(skippedItem("code-queue:pr-preflight-contract", "Code Queue PR preflight contract is opt-in with script checks", "--scripts-typecheck or --full"));
items.push(skippedItem("deploy:artifact-matrix-contract", "deploy artifact matrix contract is opt-in with script checks", "--scripts-typecheck or --full"));
items.push(skippedItem("code-queue:liveness-diagnostics-fixtures", "Code Queue liveness diagnostics fixtures are opt-in with script checks", "--scripts-typecheck or --full"));
items.push(skippedItem("baidu-netdisk:artifact-guard-contract", "Baidu Netdisk artifact guard contract is opt-in with script checks", "--scripts-typecheck or --full"));
items.push(skippedItem("artifact-registry:direct-compose-dry-run-matrix", "main-server direct artifact consumer dry-run matrix is opt-in with script checks", "--scripts-typecheck or --full"));
+186 -39
View File
@@ -119,6 +119,8 @@ interface DeployEnvironmentManifestSource {
fetchedAt: string;
}
type ArtifactConsumerPlanKind = "main-server-compose" | "d601-direct-compose" | "d601-k3s-managed" | "d601-dev-target-side-build" | "unsupported";
const defaultDeployFile = "deploy.json";
const defaultTimeoutMs = 900_000;
const resolveFetchTimeout = "120s";
@@ -148,6 +150,9 @@ const prodArtifactLiveApplyBlockedServiceIds = new Map<string, string>([
["met-nonlinear", "met-nonlinear is blocked for live artifact deploy because config.json points at docker/unidesk/Dockerfile.ml while the compose service is met-nonlinear-ts."],
["k3sctl-adapter", "k3sctl-adapter is an infrastructure control bridge; this executor exposes artifact consumer plan/dry-run only. Real production deployment requires supervisor confirmation outside this task."],
]);
const artifactConsumerDryRunBlockedServiceIds = new Map<string, string>([
["met-nonlinear", "runtime-verification-blocked: CI publishes the ML image Dockerfile contract, but the long-running Compose service is met-nonlinear-ts; CD cannot yet prove the running container image label matches the requested commit."],
]);
const deployEnvironmentTargets: Record<DeployEnvironment, DeployEnvironmentTarget> = {
dev: {
environment: "dev",
@@ -881,6 +886,133 @@ function environmentTargetSummary(target: DeployEnvironmentTarget): Record<strin
};
}
function artifactConsumerPlanKind(service: UniDeskMicroserviceConfig, environment: DeployEnvironment): ArtifactConsumerPlanKind {
if (environment === "dev" && devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id)) {
return "d601-dev-target-side-build";
}
if (service.deployment.mode === "k3sctl-managed") return "d601-k3s-managed";
if (service.providerId === "D601" && service.deployment.mode === "unidesk-direct") return "d601-direct-compose";
if (targetIsMain(service) && isDirectComposeDeployMode(service)) return "main-server-compose";
return "unsupported";
}
function deploymentPathForEnvironmentService(service: UniDeskMicroserviceConfig, environment: DeployEnvironment): string {
return deploymentPathForEnvironmentServiceId(service.id, environment);
}
function deploymentPathForEnvironmentServiceId(serviceId: string, environment: DeployEnvironment): string {
if (environment === "prod") return prodArtifactConsumerServiceIds.has(serviceId) ? "d601-registry-artifact-consumer" : "unsupported";
if (devArtifactConsumerServiceIds.has(serviceId)) return "d601-registry-artifact-consumer";
if (devApplySupportedServiceIds.has(serviceId)) return "d601-dev-target-side-build";
return "unsupported";
}
function artifactConsumerPlanTarget(service: UniDeskMicroserviceConfig, environment: DeployEnvironment): Record<string, unknown> {
const kind = artifactConsumerPlanKind(service, environment);
const liveBlockReason = environment === "prod" ? prodArtifactLiveApplyBlockedServiceIds.get(service.id) ?? null : null;
const dryRunBlockedReason = artifactConsumerDryRunBlockedServiceIds.get(service.id) ?? null;
const targetImage = artifactConsumerTargetImageHint(service, environment);
const common = {
consumerKind: kind,
runtimeHost: kind === "main-server-compose" ? "main-server" : "D601",
deploymentMode: service.deployment.mode,
noRuntimeSourceBuild: kind !== "d601-dev-target-side-build",
dryRunOnly: dryRunBlockedReason !== null || (environment === "prod" && prodArtifactLiveApplyBlockedServiceIds.has(service.id)),
blockedReason: dryRunBlockedReason ?? liveBlockReason,
};
if (kind === "d601-k3s-managed") {
return {
...common,
namespace: service.deployment.namespace ?? (environment === "dev" ? "unidesk-dev" : k8sNamespace),
deployment: service.repository.composeService,
service: service.deployment.k3sServiceId ?? service.repository.composeService,
containerName: service.repository.containerName,
manifest: service.repository.composeFile,
targetImage,
deployCommandShape: "registry manifest check + native k3s containerd import + Kubernetes Deployment image/env/annotation update + API service proxy health",
forbiddenActions: ["docker build", "docker compose build", "NodePort", "hostPort", "provider-gateway direct business backend"],
};
}
if (kind === "d601-direct-compose" || kind === "main-server-compose") {
return {
...common,
workDir: targetWorkDir(service),
composeFile: service.repository.composeFile,
composeService: service.repository.composeService,
containerName: service.repository.containerName,
targetImage,
deployCommandShape: `docker compose up -d --no-build --no-deps --force-recreate ${service.repository.composeService}`,
forbiddenActions: ["docker build", "docker compose build", "docker compose up --build", "dirty worktree source deploy"],
};
}
if (kind === "d601-dev-target-side-build") {
return {
...common,
namespace: service.deployment.namespace ?? "unidesk-dev",
deployment: service.repository.composeService,
manifest: service.repository.composeFile,
workDir: targetWorkDir(service),
deployCommandShape: "D601 dev target-side source materialization/build/import/rollout",
forbiddenActions: ["production namespace mutation", "production database mutation"],
};
}
return common;
}
function artifactConsumerTargetImageHint(service: UniDeskMicroserviceConfig, environment: DeployEnvironment): string {
if (service.id === "frontend" && environment === "prod") return "unidesk-frontend";
if (service.id === "frontend" && environment === "dev") return "unidesk-frontend:dev";
if (service.id === "k3sctl-adapter") return "unidesk-k3sctl-adapter:d601";
if (service.id === "met-nonlinear") return "met-nonlinear-ml:tf26";
if (service.deployment.mode === "k3sctl-managed") return `unidesk-${service.id}:${environment === "dev" ? "dev" : "d601"}`;
const container = service.repository.containerName;
if (container.endsWith("-backend")) return container.slice(0, -"-backend".length);
return container;
}
function artifactConsumerPlanValidation(service: UniDeskMicroserviceConfig, environment: DeployEnvironment): string[] {
const kind = artifactConsumerPlanKind(service, environment);
if (kind === "d601-dev-target-side-build") {
return [
"reads origin/master:deploy.json for dev desired state",
"materializes and builds only the selected dev service on D601",
"rolls out only unidesk-dev resources",
];
}
const base = [
"reads origin/master:deploy.json for commit intent",
"requires a commit-pinned artifact in 127.0.0.1:5000",
"verifies image labels for service id, source commit, and Dockerfile",
"does not build source on the runtime target",
];
if (kind === "d601-k3s-managed") {
return [
...base,
"imports the artifact into native k3s containerd",
"verifies Deployment metadata and health through the Kubernetes API service proxy",
];
}
if (kind === "d601-direct-compose" || kind === "main-server-compose") {
return [
...base,
"recreates only the selected Compose service with --no-build --no-deps --force-recreate",
"verifies running image labels and private service health",
];
}
return ["unsupported service remains blocked before source materialization or runtime mutation"];
}
function environmentPlanServiceConfig(config: UniDeskConfig | null, service: DeployManifestService, environment: DeployEnvironment): UniDeskMicroserviceConfig | null {
if (environment === "dev") {
const devService = devK3sDeployService(service.id);
if (devService !== undefined) return devService;
}
const configured = config?.microservices.find((candidate) => candidate.id === service.id);
if (configured !== undefined) return configured;
if (config !== null) return coreDeployService(config, service.id, environment) ?? null;
return null;
}
function targetIsMain(service: UniDeskMicroserviceConfig): boolean {
return service.providerId === "main-server";
}
@@ -2657,6 +2789,7 @@ async function checkOrPlan(config: UniDeskConfig, manifest: DeployManifest, opti
}
function environmentDryRunPlan(
config: UniDeskConfig | null,
manifest: DeployManifest,
source: DeployEnvironmentManifestSource,
options: DeployOptions,
@@ -2707,49 +2840,63 @@ function environmentDryRunPlan(
deployJsonFromWorktree: false,
dirtyWorktreeUsed: false,
},
services: services.map((service) => ({
id: service.id,
repo: service.repo,
commitId: service.commitId,
environment,
targetNamespace: target.namespace,
providerId: target.provider.providerId,
databaseFingerprint: fingerprint,
deploymentPath: environment === "prod"
? prodArtifactConsumerServiceIds.has(service.id)
? "d601-registry-artifact-consumer"
: "unsupported"
: devArtifactConsumerServiceIds.has(service.id)
? "d601-registry-artifact-consumer"
: devApplySupportedServiceIds.has(service.id)
? "d601-dev-target-side-build"
: "unsupported",
unsupported: (environment === "prod" && !prodArtifactConsumerServiceIds.has(service.id))
|| (environment === "dev" && !devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id))
? {
ok: false,
supported: false,
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.",
}
: environment === "dev" && !devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id)
services: services.map((service) => {
const serviceConfig = environmentPlanServiceConfig(config, service, environment);
const unsupported = (environment === "prod" && !prodArtifactConsumerServiceIds.has(service.id))
|| (environment === "dev" && !devApplySupportedServiceIds.has(service.id) && !devArtifactConsumerServiceIds.has(service.id));
const dryRunBlockedReason = artifactConsumerDryRunBlockedServiceIds.get(service.id) ?? null;
const planTarget = serviceConfig === null ? null : artifactConsumerPlanTarget(serviceConfig, environment);
return {
id: service.id,
repo: service.repo,
commitId: service.commitId,
environment,
targetNamespace: target.namespace,
providerId: target.provider.providerId,
databaseFingerprint: fingerprint,
deploymentPath: serviceConfig === null
? deploymentPathForEnvironmentServiceId(service.id, environment)
: deploymentPathForEnvironmentService(serviceConfig, environment),
artifactConsumer: serviceConfig === null ? {
consumerKind: "unsupported",
noRuntimeSourceBuild: true,
dryRunOnly: true,
blockedReason: "service is missing from config.json and no synthetic deploy service is available for this environment",
} : {
consumerKind: artifactConsumerPlanKind(serviceConfig, environment),
registryImage: `127.0.0.1:5000/unidesk/${service.id}:${service.commitId}`,
noRuntimeSourceBuild: artifactConsumerPlanKind(serviceConfig, environment) !== "d601-dev-target-side-build",
dryRunOnly: (environment === "prod" && prodArtifactLiveApplyBlockedServiceIds.has(service.id)) || dryRunBlockedReason !== null,
blockedReason: dryRunBlockedReason ?? (environment === "prod" ? prodArtifactLiveApplyBlockedServiceIds.get(service.id) ?? null : null),
},
target: planTarget,
validation: serviceConfig === null ? undefined : artifactConsumerPlanValidation(serviceConfig, environment),
unsupported: unsupported
? {
ok: false,
supported: false,
reason: "No standardized dev 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,
liveApply: environment === "prod" && prodArtifactLiveApplyBlockedServiceIds.has(service.id)
? {
allowed: false,
reason: prodArtifactLiveApplyBlockedServiceIds.get(service.id),
dryRunOnly: true,
}
: environment === "prod"
? { allowed: prodArtifactConsumerServiceIds.has(service.id) }
: undefined,
})),
liveApply: dryRunBlockedReason !== null
? {
allowed: false,
reason: dryRunBlockedReason,
dryRunOnly: true,
}
: 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,
};
}
@@ -3031,7 +3178,7 @@ export async function runDeployCommand(config: UniDeskConfig | null, args: strin
const commitOverrideError = commitOverrideArtifactConsumerError(options);
if (commitOverrideError !== null) throw new Error(commitOverrideError);
const { manifest, source } = readEnvironmentDeployManifest(options.environment);
if (action === "check" || action === "plan") return environmentDryRunPlan(manifest, source, options, action);
if (action === "check" || action === "plan") return environmentDryRunPlan(config, manifest, source, options, action);
if (options.environment === "prod") {
const unsupported = prodArtifactUnsupportedServices(manifest, options.serviceId);
if (unsupported.length > 0) return prodArtifactUnsupportedResult(unsupported);