fix: remoteize backend-core publish preflight
This commit is contained in:
+303
-28
@@ -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
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user