feat: refresh shared chat and server workflows

This commit is contained in:
2026-05-26 12:26:33 +09:00
parent 51e0099bea
commit c1d0f4c1db
82 changed files with 18604 additions and 12461 deletions

View File

@@ -117,6 +117,8 @@ type BuildInspectionResult = {
updateSummary: string | null;
};
type WorkServerSlot = 'blue' | 'green';
const RUNNER_HEARTBEAT_FRESHNESS_MS = 30_000;
const DEFERRED_RESTART_DELAY_MS = 2_000;
const DEFERRED_RESTART_CONFIRM_TIMEOUT_MS = 4_500;
@@ -484,6 +486,41 @@ export function resolveDockerSocketPath(source: NodeJS.ProcessEnv | Record<strin
return '/var/run/docker.sock';
}
function getWorkServerActiveSlotFileCandidates() {
const mainProjectRoot = normalizePath(resolveMainProjectRoot());
const projectRoot = normalizePath(env.SERVER_COMMAND_PROJECT_ROOT);
return [
env.SERVER_COMMAND_WORK_SERVER_ACTIVE_SLOT_FILE?.trim() || null,
path.join(mainProjectRoot, 'etc', 'servers', 'work-server', '.docker', 'runtime', 'active-slot'),
path.join(projectRoot, 'etc', 'servers', 'work-server', '.docker', 'runtime', 'active-slot'),
path.join(projectRoot, '.docker', 'runtime', 'active-slot'),
].filter((value, index, array): value is string => Boolean(value) && array.indexOf(value) === index);
}
async function readWorkServerActiveSlot(): Promise<WorkServerSlot> {
for (const candidate of getWorkServerActiveSlotFileCandidates()) {
try {
const value = (await readFile(candidate, 'utf8')).trim();
if (value === 'blue' || value === 'green') {
return value;
}
} catch {
continue;
}
}
return 'blue';
}
function resolveWorkServerContainerName(slot: WorkServerSlot) {
return slot === 'green' ? 'work-server-green' : 'work-server-blue';
}
function appendComposeDetails(detailParts: Array<string | null | undefined>) {
return trimPreview(detailParts.filter(Boolean).join(' '));
}
function shouldRetryWithDockerSocket(error: unknown) {
const failure = error instanceof Error ? (error as ExecFileFailure) : null;
const detail = [failure?.stderr, failure?.stdout, failure?.message].filter(Boolean).join('\n');
@@ -1137,11 +1174,16 @@ async function inspectComposeStatus(definition: ServerDefinition) {
}
}
async function inspectContainerRuntime(definition: ServerDefinition): Promise<RuntimeInspectionResult> {
async function inspectContainerRuntime(
definition: ServerDefinition,
containerNameOverride?: string,
): Promise<RuntimeInspectionResult> {
const containerName = containerNameOverride ?? definition.containerName;
try {
const { stdout } = await execFileAsync(
'docker',
['inspect', '-f', '{{.State.StartedAt}}\t{{.State.Status}}\t{{.Name}}', definition.containerName],
['inspect', '-f', '{{.State.StartedAt}}\t{{.State.Status}}\t{{.Name}}', containerName],
{
cwd: definition.commandWorkingDirectory,
timeout: 8000,
@@ -1158,7 +1200,7 @@ async function inspectContainerRuntime(definition: ServerDefinition): Promise<Ru
} catch (error) {
if (shouldRetryWithDockerSocket(error)) {
try {
const inspected = await inspectContainerViaSocket(definition.containerName);
const inspected = await inspectContainerViaSocket(containerName);
return {
startedAt: normalizeDateTimeValue(inspected.State?.StartedAt ?? null),
composeStatus: inspected.State?.Status?.trim() || null,
@@ -1298,10 +1340,27 @@ async function inspectRuntime(definition: ServerDefinition): Promise<RuntimeInsp
}
if (definition.key === 'work-server') {
const primarySlot = await readWorkServerActiveSlot();
const candidateSlots: WorkServerSlot[] = primarySlot === 'green' ? ['green', 'blue'] : ['blue', 'green'];
for (const slot of candidateSlots) {
const runtimeInfo = await inspectContainerRuntime(definition, resolveWorkServerContainerName(slot));
if (runtimeInfo.startedAt) {
return {
...runtimeInfo,
composeDetails: appendComposeDetails([`slot:${slot}`, runtimeInfo.composeDetails]),
};
}
}
const runtimeInfo = await inspectContainerRuntime(definition);
if (runtimeInfo.startedAt) {
return runtimeInfo;
return {
...runtimeInfo,
composeDetails: appendComposeDetails(['slot:proxy', runtimeInfo.composeDetails]),
};
}
return inspectCurrentProcessRuntime();