fix: add safe frontend rebuild workflow

This commit is contained in:
Codex
2026-05-05 02:45:58 +00:00
parent cce8a78508
commit 5a62af6260
7 changed files with 79 additions and 3 deletions
+9 -1
View File
@@ -1,6 +1,6 @@
import { readConfig } from "./src/config";
import { debugDispatch, debugHealth, debugTask, isDebugDispatchCommand, type DebugDispatchCommand } from "./src/debug";
import { stackLogs, stackStatus, startStack, stopStack } from "./src/docker";
import { isRebuildableService, rebuildService, stackLogs, stackStatus, startStack, stopStack } from "./src/docker";
import { runE2E } from "./src/e2e";
import { emitError, emitJson } from "./src/output";
import { jobWithTail, listJobs, readJob, runJob } from "./src/jobs";
@@ -25,6 +25,7 @@ function help(): unknown {
{ command: "server stop", description: "Fire-and-forget docker-compose down for the fixed UniDesk stack." },
{ command: "server status", description: "Show fixed ports, containers, service health, and public URLs." },
{ command: "server logs [--tail-bytes N]", description: "Return bounded tails from file logs and docker logs." },
{ command: "server rebuild <backend-core|frontend|provider-gateway>", description: "Build first, then label-replace one service without Docker Compose v1 recreate fallback." },
{ command: "ssh <providerId> [ssh-like args...]", description: "Open a Host SSH / WSL SSH maintenance session through the provider-gateway bridge." },
{ command: "job list", description: "List async jobs from .state/jobs." },
{ command: "job status <jobId|latest> [--tail-bytes N]", description: "Show job state with bounded stdout/stderr tails." },
@@ -141,6 +142,13 @@ async function main(): Promise<void> {
emitJson(commandName, stackLogs(config, numberOption("--tail-bytes", 3000)));
return;
}
if (sub === "rebuild") {
if (!isRebuildableService(third)) {
throw new Error("server rebuild requires one of: backend-core, frontend, provider-gateway");
}
emitJson(commandName, rebuildService(config, third));
return;
}
}
if (top === "job") {
+48
View File
@@ -18,6 +18,13 @@ export interface ContainerStatus {
ports: string;
}
const rebuildableServices = ["backend-core", "frontend", "provider-gateway"] as const;
export type RebuildableService = typeof rebuildableServices[number];
export function isRebuildableService(value: string | undefined): value is RebuildableService {
return rebuildableServices.some((service) => service === value);
}
export function resolveComposeCommand(config: UniDeskConfig, envFile: string): string[] {
const composeFile = rootPath(config.docker.composeFile);
if (commandOk(["docker", "compose", "version"], repoRoot)) {
@@ -133,6 +140,47 @@ export function stopStack(config: UniDeskConfig): unknown {
return { job, runtimeEnv, command, portsBeforeStop: fixedPorts(config) };
}
export function rebuildService(config: UniDeskConfig, service: RebuildableService): unknown {
const runtimeEnv = writeComposeEnv(config, false);
const compose = resolveComposeCommand(config, runtimeEnv.envFile);
const buildCommand = [...compose, "build", service];
const listServiceContainersCommand = [
"docker",
"ps",
"-aq",
"--filter",
`label=com.docker.compose.project=${config.docker.projectName}`,
"--filter",
`label=com.docker.compose.service=${service}`,
];
const upCommand = [...compose, "up", "-d", "--no-deps", service];
const script = [
"set -euo pipefail",
`echo ${shellJoin(["rebuild_service", service, "build_first_then_replace_container"])}`,
shellJoin(buildCommand),
`ids=$(${shellJoin(listServiceContainersCommand)})`,
`if [ -n "$ids" ]; then echo "remove_existing_compose_service_containers service=${service} ids=$ids"; docker rm -f $ids; else echo "no_existing_compose_service_containers service=${service}"; fi`,
shellJoin(upCommand),
].join("; ");
const command = ["bash", "-lc", script];
const job = startJob("server_rebuild", command, `Rebuild and replace UniDesk ${service} without Docker Compose v1 recreate`);
return {
job,
runtimeEnv,
service,
command,
strategy: {
buildBeforeRemove: true,
removeScope: {
projectLabel: config.docker.projectName,
serviceLabel: service,
},
noDeps: true,
namedVolumesPreserved: true,
},
};
}
function fixedPorts(config: UniDeskConfig): Array<{ name: string; port: number; listening: boolean }> {
return [
{ name: "frontend", port: config.network.frontend.port, listening: isPortListening(config.network.frontend.port) },