feat: add artifact publish preflight

This commit is contained in:
Codex
2026-05-20 21:49:50 +00:00
parent 68ae2722ab
commit 021a9eef01
10 changed files with 636 additions and 45 deletions
+122 -1
View File
@@ -6,6 +6,12 @@ import { parseNetworkPerfOptions, runNetworkPerf } from "./network-perf";
import { isSshSkillDiscoveryArgs, parseSshArgs } from "./ssh";
import { codexJudgeQueryAsync, codexOutputQueryAsync, codexTaskQueryAsync, codexTasksQueryAsync } from "./code-queue";
import { runDecisionCenterCommandAsync } from "./decision-center";
import {
artifactRegistryReadonlyResultFromCommand,
buildArtifactRegistryReadonlyProbe,
parseArtifactRegistryOptions,
} from "./artifact-registry";
import { runCiPublishUserServiceDryRunPreflight } from "./ci";
export interface RemoteCliOptions {
host: string | null;
@@ -517,6 +523,113 @@ async function remoteMicroservice(session: FrontendSession, args: string[]): Pro
throw new Error("remote microservice command must be: microservice list | status <id> | health <id> | diagnostics <id> | tunnel-self-test <id> | proxy <id> <path>");
}
function commandResultFromFrontendTask(command: string[], task: { status?: string; result?: Record<string, unknown> } | undefined) {
const result = task?.result ?? {};
const stdout = typeof result.stdout === "string" ? result.stdout : "";
const stderr = typeof result.stderr === "string" ? result.stderr : "";
return {
command,
cwd: ".",
exitCode: typeof result.exitCode === "number" ? result.exitCode : null,
stdout,
stderr,
signal: null,
timedOut: task?.status !== "succeeded" && stderr.includes("timeout"),
};
}
async function dispatchHostSshJson(
session: FrontendSession,
providerId: string,
command: string,
timeoutMs: number,
waitMs = Math.max(timeoutMs + 5000, 20_000),
): Promise<{ dispatch: FetchJsonResult; wait: unknown; task?: { status?: string; result?: Record<string, unknown> }; taskId: string | null }> {
const dispatch = await frontendJson(session, "/api/dispatch", {
method: "POST",
body: JSON.stringify({
providerId,
command: "host.ssh",
payload: { source: "cli-remote-artifact-registry", mode: "exec", command, timeoutMs },
}),
});
const taskId = (dispatch as { body?: { taskId?: string } }).body?.taskId ?? "";
const wait = taskId.length > 0 ? await waitForFrontendTask(session, taskId, waitMs) : null;
const task = (wait as { task?: { status?: string; result?: Record<string, unknown> } } | null)?.task;
return { dispatch, wait, task, taskId: taskId.length > 0 ? taskId : null };
}
async function remoteArtifactRegistry(session: FrontendSession, args: string[]): Promise<unknown> {
const action = args[1] ?? "status";
if (action !== "status" && action !== "health") {
throw new Error("remote frontend transport supports artifact-registry status|health only; use main-server SSH for mutating install/deploy commands");
}
const options = parseArtifactRegistryOptions(args.slice(2));
const probe = buildArtifactRegistryReadonlyProbe(action, options);
const dispatched = await dispatchHostSshJson(session, probe.providerId, probe.script, probe.timeoutMs);
const command = ["frontend", "/api/dispatch", probe.providerId, "host.ssh", action];
const result = commandResultFromFrontendTask(command, dispatched.task);
const registryResult = artifactRegistryReadonlyResultFromCommand(probe, result);
return {
transport: "frontend",
readonly: true,
dispatch: dispatched.dispatch,
wait: dispatched.wait,
result: dispatched.taskId === null
? {
ok: false,
readonly: true,
installed: false,
healthy: false,
decision: "infra-blocked",
retryable: true,
runnerDisposition: "infra-blocked",
healthyScopes: [],
failedScopes: ["backend-core-api"],
runtimeApiHealthy: false,
channels: [
{ channel: "backend-core-api", ok: false, requiredFor: "frontend /api/dispatch backend-core session creation", detail: dispatched.dispatch },
{ channel: "provider-dispatch", ok: false, requiredFor: "host.ssh task creation", detail: dispatched.dispatch },
],
registry: registryResult,
}
: registryResult,
};
}
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");
}
return {
transport: "frontend",
readonly: true,
result: await runCiPublishUserServiceDryRunPreflight(config, args.slice(1), {
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],
}),
};
}
async function remoteCodeQueue(session: FrontendSession, args: string[]): Promise<unknown> {
const action = args[1] ?? "task";
if (action !== "task" && action !== "summary" && action !== "show" && action !== "tasks" && action !== "overview" && action !== "output" && action !== "judge") {
@@ -610,7 +723,7 @@ async function runRemoteCliOverFrontend(options: RemoteCliOptions, config: UniDe
emitRemoteJson(name, {
transport: "frontend",
baseUrl: session.baseUrl,
commands: ["debug health", "debug dispatch", "debug task", "ssh <providerId> <command>", "ssh <providerId> skills", "microservice list", "microservice status <id>", "microservice health <id>", "microservice diagnostics <id>", "microservice tunnel-self-test <id>", "microservice proxy <id> <path>", "decision upload <markdown-file>", "decision list", "decision show <id>", "codex task <taskId>", "codex tasks", "codex judge <taskId> --attempt N", "network perf"],
commands: ["debug health", "debug dispatch", "debug task", "ssh <providerId> <command>", "ssh <providerId> skills", "artifact-registry status|health", "ci publish-user-service --dry-run", "microservice list", "microservice status <id>", "microservice health <id>", "microservice diagnostics <id>", "microservice tunnel-self-test <id>", "microservice proxy <id> <path>", "decision upload <markdown-file>", "decision list", "decision show <id>", "codex task <taskId>", "codex tasks", "codex judge <taskId> --attempt N", "network perf"],
});
return 0;
}
@@ -630,6 +743,14 @@ async function runRemoteCliOverFrontend(options: RemoteCliOptions, config: UniDe
emitRemoteJson(name, await remoteMicroservice(session, args));
return 0;
}
if (top === "artifact-registry") {
emitRemoteJson(name, await remoteArtifactRegistry(session, args));
return 0;
}
if (top === "ci") {
emitRemoteJson(name, await remoteCi(session, config, args));
return 0;
}
if (top === "decision" || top === "decision-center") {
const fetcher = (path: string, init?: { method?: string; body?: unknown }): Promise<FetchJsonResult> => {
const requestInit = init === undefined