fix: remoteize backend-core publish preflight

This commit is contained in:
Codex
2026-05-21 10:08:56 +00:00
parent b69c2fe599
commit 1749897d1a
3 changed files with 535 additions and 88 deletions
+303 -28
View File
@@ -87,7 +87,7 @@ interface DispatchResult {
raw: unknown;
}
type PublishPreflightFailureClassification = "auth-missing" | "remote-proxy-missing" | "provider-unreachable" | "local-docker-required" | "registry-not-installed" | "registry-unhealthy" | "remote-command-timeout" | "ssh-helper-command-shape-incompatible";
type PublishPreflightFailureClassification = "auth-missing" | "remote-proxy-missing" | "provider-unreachable" | "local-docker-required" | "registry-not-installed" | "registry-unhealthy" | "remote-command-timeout" | "ssh-helper-command-shape-incompatible" | "ci-runner-not-ready";
type PublishPreflightControlChannel = "backend-core" | "database" | "provider" | "registry";
type PublishPreflightDetailedChannel = "backend-core-api" | "provider-dispatch" | "provider-host-ssh" | "database" | "artifact-registry";
@@ -463,6 +463,7 @@ function recommendedPublishPreflightAction(
if (failureClassification === "remote-command-timeout") return "rerun artifact-registry health and inspect D601 host.ssh latency if the timeout repeats";
if (failureClassification === "registry-not-installed") return "install the D601 artifact registry through the controlled artifact-registry install path, then rerun health";
if (failureClassification === "registry-unhealthy") return "restore the D601 artifact registry service/container/API until artifact-registry health passes";
if (failureClassification === "ci-runner-not-ready") return "restore D601 unidesk-ci Tekton runner prerequisites before authorizing backend-core artifact publication";
if (missingControlChannels.includes("registry")) return "restore or install the D601 artifact registry until artifact-registry health passes";
return `restore missing control channel(s): ${missingControlChannels.join(", ") || "unknown"}`;
}
@@ -470,6 +471,7 @@ function recommendedPublishPreflightAction(
function publishPreflightControlPlane(
transport: PublishPreflightTransport,
failureClassification: PublishPreflightFailureClassification | null,
remoteCommandShape = "bun scripts/cli.ts --main-server-ip <host> ci publish-user-service --service <id> --commit <full-sha> --dry-run",
): Record<string, unknown> {
const kind = transportKind(transport);
const remoteCapable = kind !== "local-docker";
@@ -481,7 +483,7 @@ function publishPreflightControlPlane(
failureClassification,
preferredRunnerPath: "frontend-private-proxy",
fallbackRunnerPath: "main-server-local-docker",
remoteCommandShape: "bun scripts/cli.ts --main-server-ip <host> ci publish-user-service --service <id> --commit <full-sha> --dry-run",
remoteCommandShape,
providerTunnel: {
providerId: d601ProviderId,
command: "host.ssh",
@@ -497,6 +499,84 @@ function publishPreflightFailedScopes(preflight: PublishPreflight): string[] {
return preflight.missingChannels;
}
function ciRunnerPreflightScript(sourceHostPath: string): string {
return [
"set -euo pipefail",
`export KUBECONFIG=${shellQuote(d601Kubeconfig)}`,
"printf 'provider_host_ssh=ok\\n'",
"printf 'kubectl='",
"command -v kubectl >/dev/null && printf 'ok\\n' || { printf 'missing\\n'; exit 127; }",
"printf 'docker='",
"command -v docker >/dev/null && printf 'ok\\n' || { printf 'missing\\n'; exit 127; }",
"printf 'docker_socket='",
"if [ -S /var/run/docker.sock ] || [ -S /run/docker.sock ]; then printf 'true\\n'; else printf 'false\\n'; fi",
"printf 'namespace='",
"kubectl get namespace unidesk-ci >/dev/null 2>&1 && printf 'true\\n' || printf 'false\\n'",
"printf 'tekton_pipeline='",
"kubectl get pipeline/unidesk-backend-core-artifact-publish -n unidesk-ci >/dev/null 2>&1 && printf 'true\\n' || printf 'false\\n'",
"printf 'tekton_task='",
"kubectl get task/unidesk-backend-core-artifact-publish -n unidesk-ci >/dev/null 2>&1 && printf 'true\\n' || printf 'false\\n'",
"printf 'service_account='",
"kubectl get serviceaccount/unidesk-ci-runner -n unidesk-ci >/dev/null 2>&1 && printf 'true\\n' || printf 'false\\n'",
"printf 'pvc='",
"kubectl get pvc/unidesk-ci-cache -n unidesk-ci >/dev/null 2>&1 && printf 'true\\n' || printf 'false\\n'",
"printf 'source_parent_directory='",
`test -d ${shellQuote(posixPath.dirname(sourceHostPath))} && printf 'true\\n' || printf 'false\\n'`,
].join("\n");
}
function keyValueBool(stdout: string, key: string): boolean {
const match = new RegExp(`^${key}=(.*)$`, "mu").exec(stdout);
if (match === null) return false;
const value = match[1]?.trim().toLowerCase() ?? "";
return value === "true" || value === "ok";
}
function backendCoreCiRunnerReady(result: DispatchResult): boolean {
return result.ok
&& keyValueBool(result.stdout, "kubectl")
&& keyValueBool(result.stdout, "docker")
&& keyValueBool(result.stdout, "namespace")
&& keyValueBool(result.stdout, "tekton_pipeline")
&& keyValueBool(result.stdout, "tekton_task")
&& keyValueBool(result.stdout, "service_account")
&& keyValueBool(result.stdout, "pvc")
&& keyValueBool(result.stdout, "source_parent_directory");
}
function backendCoreArtifactRequirements(options: CiPublishBackendCoreOptions, plannedArtifact: ArtifactSummary): Record<string, unknown> {
return {
requiredLabels: {
"unidesk.ai/service-id": "backend-core",
"unidesk.ai/source-commit": options.commit,
"unidesk.ai/source-repo": options.repoUrl,
"unidesk.ai/dockerfile": options.dockerfile,
},
digest: {
requiredAfterPublish: true,
dryRunValue: plannedArtifact.digest,
fields: ["artifactSummary.digest", "artifactSummary.digestRef"],
source: "D601 registry manifest HEAD after ci publish-backend-core succeeds",
},
imageRef: plannedArtifact.imageRef,
};
}
function backendCoreDevApplyPath(options: CiPublishBackendCoreOptions, plannedArtifact: ArtifactSummary): Record<string, unknown> {
return {
environment: "dev",
pullOnly: true,
apply: `bun scripts/cli.ts deploy apply --env dev --service backend-core --commit ${options.commit}`,
sourceImage: plannedArtifact.imageRef,
forbidden: ["--env prod", "cargo build", "docker build", "docker compose build", "server rebuild backend-core"],
note: "PROD is intentionally not part of backend-core publish preflight; dev apply still needs explicit authorization after artifact publication.",
};
}
function backendCoreRemoteCommandShape(commit: string): string {
return `bun scripts/cli.ts --main-server-ip <host> ci publish-backend-core --commit ${commit} --dry-run`;
}
function dispatchPreflightFailure(command: string, result: DispatchResult): DispatchResult {
return {
ok: false,
@@ -1499,6 +1579,103 @@ async function publishUserServicePreflight(
};
}
async function publishBackendCorePreflight(
_config: UniDeskConfig,
options: CiPublishBackendCoreOptions,
plannedArtifact: ArtifactSummary,
transport: PublishPreflightTransport,
): Promise<PublishPreflight> {
const providerId = d601ProviderId;
const channels: PublishPreflightChannelProbe[] = [];
const overview = await transport.coreFetch("/api/overview", { maxResponseBytes: 500_000 });
const overviewBody = coreBody(overview);
const backendCoreOk = responseOk(overview) && overviewBody?.dbReady === true;
channels.push(channelProbe("backend-core-api", "backend-core", backendCoreOk, "dispatch API, provider catalog, task polling, and database-backed CI state", {
ok: responseOk(overview),
dbReady: overviewBody?.dbReady ?? null,
runnerDisposition: asRecord(overview)?.runnerDisposition ?? null,
failureKind: asRecord(overview)?.failureKind ?? null,
detail: backendCoreUnavailable(overview) ? overview : {
status: asRecord(overview)?.status ?? null,
body: overviewBody,
},
}));
channels.push(channelProbe("database", "database", backendCoreOk, "backend-core task dispatch, provider state, Tekton task polling, and source identity lookup", {
dbReady: overviewBody?.dbReady ?? false,
observedThrough: "backend-core /api/overview",
}));
const ciScript = ciRunnerPreflightScript(options.sourceHostPath);
const ciProbe = await transport.dispatchHostSsh(ciScript, 30_000, 15_000);
const ciRunnerReady = backendCoreCiRunnerReady(ciProbe);
channels.push(channelProbe("provider-dispatch", "provider", ciProbe.taskId !== null || ciProbe.ok, "backend-core /api/dispatch can create D601 host.ssh tasks", {
taskId: ciProbe.taskId,
status: ciProbe.status,
ok: ciProbe.taskId !== null || ciProbe.ok,
exitCode: ciProbe.exitCode,
stderrTail: ciProbe.stderr.slice(-1200),
}));
channels.push(channelProbe("provider-host-ssh", "provider", ciRunnerReady, "D601 host.ssh can observe Docker, kubectl, Tekton, PVC, and backend-core source parent without starting CI", {
taskId: ciProbe.taskId,
status: ciProbe.status,
exitCode: ciProbe.exitCode,
stdoutTail: ciProbe.stdout.slice(-1600),
stderrTail: ciProbe.stderr.slice(-1200),
raw: ciRunnerReady ? undefined : dispatchPreflightFailure("backend-core ci runner readonly probe", ciProbe).raw,
}));
const registryOptions = parseArtifactRegistryOptions(["--provider-id", providerId]);
const registryProbe = buildArtifactRegistryReadonlyProbe("health", registryOptions);
const registryDispatch = await transport.dispatchHostSsh(registryProbe.script, Math.max(registryProbe.timeoutMs, 30_000), registryProbe.timeoutMs);
const registryCommand = commandResultFromDispatch(transport.artifactRegistryCommand(registryProbe), transport.commandCwd, registryDispatch);
const registry = artifactRegistryReadonlyResultFromCommand(registryProbe, registryCommand);
const registryRecord = asRecord(registry);
const registryOk = registryRecord?.ok === true;
channels.push(channelProbe("artifact-registry", "registry", registryOk, "backend-core commit-pinned image push and later dev CD manifest checks", registry));
const missingChannels = channels.filter((item) => !item.ok).map((item) => item.channel);
const controlChannels = summarizePublishControlChannels(channels);
const missingControlChannels = controlChannels.filter((item) => !item.ok).map((item) => item.channel);
const ready = missingChannels.length === 0;
const baseFailureClassification = ready ? null : classifyPublishPreflightFailure(transport, overview, ciProbe, registry);
const failureClassification = !ready && baseFailureClassification === null ? "ci-runner-not-ready" : baseFailureClassification;
const recommendedAction = ready
? `authorize and run: bun scripts/cli.ts ci publish-backend-core --commit ${options.commit} --wait-ms 1200000`
: recommendedPublishPreflightAction(failureClassification, registry, missingControlChannels);
return {
ok: ready,
runnerDisposition: ready ? "ready" : "infra-blocked",
failureClassification,
serviceId: "backend-core",
commit: options.commit,
providerId,
supportedArtifactPublish: true,
missingChannels,
missingControlChannels,
controlChannels,
channels,
registry,
controlPlane: publishPreflightControlPlane(transport, failureClassification, backendCoreRemoteCommandShape(options.commit)),
recommendedAction,
remoteCommandShape: registryProbe.remoteCommandShape,
next: ready
? [
`bun scripts/cli.ts ci publish-backend-core --commit ${options.commit} --wait-ms 1200000`,
`verify ${plannedArtifact.imageRef} labels and digest before any dev artifact consumer apply`,
`dev-only follow-up: bun scripts/cli.ts deploy apply --env dev --service backend-core --commit ${options.commit}`,
]
: [
`Restore missing control channel(s): ${missingControlChannels.join(", ") || "unknown"}.`,
failureClassification === "local-docker-required"
? `From a Code Queue runner, rerun this read-only preflight through the remote frontend transport: ${backendCoreRemoteCommandShape(options.commit)}.`
: "Run from the main-server CLI or use remote frontend transport against a healthy frontend/backend-core path.",
"Restore backend-core/database/provider-gateway/Host SSH/Tekton/registry readiness before retrying backend-core artifact publication.",
"This preflight must remain read-only; do not start Tekton, compile Rust, push registry artifacts, deploy/apply, or restart services.",
],
boundary: "preflight is read-only: no Tekton PipelineRun, no Rust compile, no source export, no image push, no registry write, no deploy apply, no service restart",
};
}
async function readArtifactSummaryFromPipelineRun(name: string, context: ArtifactSummaryContext): Promise<ArtifactSummary> {
const result = await runRemoteKubectlRaw([
"set -euo pipefail",
@@ -1583,32 +1760,13 @@ async function publishBackendCoreArtifact(config: UniDeskConfig, options: CiPubl
};
const plannedArtifact = artifactSummaryDefaults(summaryContext);
if (options.dryRun) {
return {
ok: true,
mode: "dry-run",
pipeline: "unidesk-backend-core-artifact-publish",
namespace: "unidesk-ci",
repoUrl: options.repoUrl,
commit: options.commit,
sourceHostPath: options.sourceHostPath,
source: {
ok: true,
mode: "planned-only",
providerId: d601ProviderId,
repoUrl: options.repoUrl,
repoFetchUrl: repoSshUrl(options.repoUrl),
commit: options.commit,
dockerfile: options.dockerfile,
imageRepository: options.imageRepository,
sourceHostPath: options.sourceHostPath,
},
artifact: plannedArtifact.imageRef,
artifactSummary: plannedArtifact,
boundary: "dry-run only; no D601 source export, no Tekton submission, no production mutation",
next: [
`bun scripts/cli.ts ci publish-backend-core --commit ${options.commit} --wait-ms 1200000`,
],
};
return runCiPublishBackendCoreDryRunPreflight(config, [
"publish-backend-core",
"--commit",
options.commit,
"--dry-run",
...(options.waitMs > 0 ? ["--wait-ms", String(options.waitMs)] : []),
], localPublishPreflightTransport, options);
}
const source = await prepareBackendCoreArtifactSource(config, options);
const name = await remoteCreatePipelineRun(backendCoreArtifactPipelineRunManifest(options));
@@ -1836,6 +1994,123 @@ export async function runCiPublishUserServiceDryRunPreflight(
};
}
export async function runCiPublishBackendCoreDryRunPreflight(
config: UniDeskConfig,
args: string[],
transport: PublishPreflightTransport,
resolvedOptions?: CiPublishBackendCoreOptions,
): Promise<Record<string, unknown>> {
const commit = requireFullCommit(stringOption(args, "--commit") ?? stringOption(args, "--revision"));
if (!args.includes("--dry-run")) throw new Error("publish-backend-core preflight requires --dry-run");
if (stringOption(args, "--repo") !== null || stringOption(args, "--repo-url") !== null) {
throw new Error("ci publish-backend-core reads source repo from CI.json; edit CI.json instead of using --repo");
}
const artifact = resolveCatalogArtifact("backend-core");
if (artifact.kind !== "source-build") throw new Error("backend-core must be modeled as a source-build artifact in CI.json");
if (artifact.status === "blocked") return blockedArtifactResult(artifact, commit, blockedReason(artifact));
const dockerfile = requireRepoRelativePath(artifact.source.dockerfile, "CI.json.artifacts.backend-core.source.dockerfile");
const options: CiPublishBackendCoreOptions = resolvedOptions ?? {
repoUrl: artifact.source.repo,
commit,
waitMs: numberOption(args, "--wait-ms", 0),
sourceHostPath: backendCoreArtifactSourceHostPath(commit),
dockerfile,
imageRepository: artifact.image.repository,
dryRun: true,
};
const summaryContext: ArtifactSummaryContext = {
serviceId: "backend-core",
commit,
repoUrl: options.repoUrl,
dockerfile: options.dockerfile,
imageRepository: options.imageRepository,
};
const plannedArtifact = artifactSummaryDefaults(summaryContext);
const preflight = await publishBackendCorePreflight(config, options, plannedArtifact, transport);
const plannedRepoFetchUrl = repoSshUrl(options.repoUrl);
const blockedScopes = publishPreflightFailedScopes(preflight);
return {
ok: preflight.ok,
mode: "dry-run-preflight",
runnerDisposition: preflight.runnerDisposition,
pipeline: "unidesk-backend-core-artifact-publish",
namespace: "unidesk-ci",
repoUrl: options.repoUrl,
commit: options.commit,
targetCommit: options.commit,
sourceRepo: options.repoUrl,
serviceId: "backend-core",
providerId: d601ProviderId,
ciRunner: {
providerId: d601ProviderId,
environment: "D601",
namespace: "unidesk-ci",
pipeline: "unidesk-backend-core-artifact-publish",
wouldBuildOnD601: true,
realBuildRequiresAuthorization: true,
},
registryTarget: plannedArtifact.repository,
wouldBuildOnD601: true,
dryRunBuildStarted: false,
supportedArtifactPublish: preflight.supportedArtifactPublish,
missingChannels: preflight.missingChannels,
missingControlChannels: preflight.missingControlChannels,
controlChannels: preflight.controlChannels,
channels: preflight.channels,
failureClassification: preflight.failureClassification,
failedScopes: blockedScopes,
blockedScopes,
recommendedAction: preflight.recommendedAction,
remoteCommandShape: preflight.remoteCommandShape,
remoteControlPlaneCandidate: preflight.controlPlane,
controlPlane: preflight.controlPlane,
registry: preflight.registry,
sourceHostPath: options.sourceHostPath,
source: {
ok: preflight.ok,
mode: "planned-only",
providerId: d601ProviderId,
repoUrl: options.repoUrl,
repoFetchUrl: plannedRepoFetchUrl,
repoProbeUrl: repoConnectivityProbeUrl(plannedRepoFetchUrl),
commit: options.commit,
serviceId: "backend-core",
dockerfile: options.dockerfile,
imageRepository: options.imageRepository,
sourceHostPath: options.sourceHostPath,
},
sourceAuth: {
repoFetchUrl: plannedRepoFetchUrl,
egressProxy: providerGatewayWsEgressProxyUrl,
gitSshProxy: repoNeedsGithubSshIdentity(plannedRepoFetchUrl),
identityRequired: repoNeedsGithubSshIdentity(plannedRepoFetchUrl),
},
d601Ci: {
providerId: d601ProviderId,
namespace: "unidesk-ci",
pipeline: "unidesk-backend-core-artifact-publish",
wouldBuildOnD601: true,
dryRunWillStartTekton: false,
dryRunWillCompileRust: false,
dryRunWillPushRegistry: false,
},
artifact: plannedArtifact.imageRef,
artifactSummary: plannedArtifact,
artifactRequirements: backendCoreArtifactRequirements(options, plannedArtifact),
controlledPublish: {
environment: "D601",
namespace: "unidesk-ci",
pipeline: "unidesk-backend-core-artifact-publish",
command: `bun scripts/cli.ts ci publish-backend-core --commit ${options.commit} --wait-ms 1200000`,
requiresReadyControlChannels: publishPreflightControlChannelOrder,
requiresHumanAuthorization: true,
},
devApplyPath: backendCoreDevApplyPath(options, plannedArtifact),
boundary: preflight.boundary,
next: preflight.next,
};
}
async function runRemoteDevE2ELauncher(options: CiDevE2EOptions): Promise<DispatchResult> {
const scriptTimeoutMs = Math.max(options.scriptTimeoutMs, options.waitMs, 60_000);
const remoteTimeoutMs = 45_000;
+32 -29
View File
@@ -11,7 +11,7 @@ import {
buildArtifactRegistryReadonlyProbe,
parseArtifactRegistryOptions,
} from "./artifact-registry";
import { runCiPublishUserServiceDryRunPreflight } from "./ci";
import { runCiPublishBackendCoreDryRunPreflight, runCiPublishUserServiceDryRunPreflight } from "./ci";
export interface RemoteCliOptions {
host: string | null;
@@ -109,9 +109,9 @@ export function autoRemoteCiPublishUserServiceDryRunPlan(
env: NodeJS.ProcessEnv = process.env,
): AutoRemoteCiPublishPlan {
const [top, sub] = args;
const isPublishDryRun = top === "ci" && sub === "publish-user-service" && args.includes("--dry-run");
const isPublishDryRun = top === "ci" && (sub === "publish-user-service" || sub === "publish-backend-core") && args.includes("--dry-run");
if (!isPublishDryRun) {
return { enabled: false, host: null, reason: "not ci publish-user-service --dry-run", failureClassification: null, transport: "frontend", command: null };
return { enabled: false, host: null, reason: "not ci publish-user-service/publish-backend-core --dry-run", failureClassification: null, transport: "frontend", command: null };
}
if (truthyDisabled(env.UNIDESK_CI_PUBLISH_AUTO_REMOTE)) {
return { enabled: false, host: null, reason: "UNIDESK_CI_PUBLISH_AUTO_REMOTE disables automatic remote preflight", failureClassification: "local-docker-required", transport: "frontend", command: null };
@@ -721,36 +721,39 @@ async function remoteArtifactRegistry(session: FrontendSession, args: string[]):
async function remoteCi(session: FrontendSession, config: UniDeskConfig, args: string[]): Promise<unknown> {
const action = args[1] ?? "status";
if (action !== "publish-user-service" || !args.includes("--dry-run")) {
throw new Error("remote frontend transport supports only ci publish-user-service --dry-run preflight; real CI publication must run from the controlled main-server CLI after preflight is ready");
if ((action !== "publish-user-service" && action !== "publish-backend-core") || !args.includes("--dry-run")) {
throw new Error("remote frontend transport supports only ci publish-user-service --dry-run and ci publish-backend-core --dry-run preflight; real CI publication must run from the controlled main-server CLI after preflight is ready");
}
const transport = {
kind: "remote-frontend" as const,
remoteHost: session.baseUrl,
coreFetch: (path: string, init?: { method?: string; body?: unknown; maxResponseBytes?: number }) => frontendJson(session, path, init === undefined ? undefined : {
method: init.method,
body: init.body === undefined ? undefined : JSON.stringify(init.body),
}, 12_000, init?.maxResponseBytes ?? 500_000),
dispatchHostSsh: async (command: string, waitMs: number, remoteTimeoutMs: number) => {
const dispatched = await dispatchHostSshJson(session, "D601", command, remoteTimeoutMs, waitMs);
const task = dispatched.task;
const result = task?.result ?? {};
return {
ok: task?.status === "succeeded" && (typeof result.exitCode !== "number" || result.exitCode === 0) && dispatched.taskId !== null,
taskId: dispatched.taskId,
status: task?.status ?? null,
stdout: typeof result.stdout === "string" ? result.stdout : "",
stderr: typeof result.stderr === "string" ? result.stderr : "",
exitCode: typeof result.exitCode === "number" ? result.exitCode : null,
raw: task ?? dispatched.wait ?? dispatched.dispatch,
};
},
commandCwd: ".",
artifactRegistryCommand: (probe: ReturnType<typeof buildArtifactRegistryReadonlyProbe>) => ["frontend", "/api/dispatch", probe.providerId, "host.ssh", probe.action, dispatchedTaskShape(probe.remoteCommandShape)],
};
return {
transport: "frontend",
readonly: true,
result: await runCiPublishUserServiceDryRunPreflight(config, args.slice(1), {
kind: "remote-frontend",
remoteHost: session.baseUrl,
coreFetch: (path, init) => frontendJson(session, path, init === undefined ? undefined : {
method: init.method,
body: init.body === undefined ? undefined : JSON.stringify(init.body),
}, 12_000, init?.maxResponseBytes ?? 500_000),
dispatchHostSsh: async (command, waitMs, remoteTimeoutMs) => {
const dispatched = await dispatchHostSshJson(session, "D601", command, remoteTimeoutMs, waitMs);
const task = dispatched.task;
const result = task?.result ?? {};
return {
ok: task?.status === "succeeded" && (typeof result.exitCode !== "number" || result.exitCode === 0) && dispatched.taskId !== null,
taskId: dispatched.taskId,
status: task?.status ?? null,
stdout: typeof result.stdout === "string" ? result.stdout : "",
stderr: typeof result.stderr === "string" ? result.stderr : "",
exitCode: typeof result.exitCode === "number" ? result.exitCode : null,
raw: task ?? dispatched.wait ?? dispatched.dispatch,
};
},
commandCwd: ".",
artifactRegistryCommand: (probe) => ["frontend", "/api/dispatch", probe.providerId, "host.ssh", probe.action, dispatchedTaskShape(probe.remoteCommandShape)],
}),
result: action === "publish-backend-core"
? await runCiPublishBackendCoreDryRunPreflight(config, args.slice(1), transport)
: await runCiPublishUserServiceDryRunPreflight(config, args.slice(1), transport),
};
}