CI / ci (push) Canceled after 0s
Removed dockge from the app as it is not required due to the manager page picking up most of its abilities.
92 lines
3.7 KiB
TypeScript
92 lines
3.7 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
/**
|
|
* Every call here runs `docker compose` with `cwd` set to a stack's own
|
|
* directory (stacks/<slug>/) rather than passing explicit -f/--env-file
|
|
* flags — compose already auto-loads `compose.yaml` and `.env` from the
|
|
* current directory, so a stack behaves identically whether it's started
|
|
* by this app or by hand from the command line.
|
|
*
|
|
* This app's own container needs the Docker socket mounted and the
|
|
* `docker compose` CLI installed (see manager/Dockerfile) — these calls
|
|
* are executed by the HOST's dockerd via that socket, not by a nested
|
|
* Docker daemon inside this container.
|
|
*/
|
|
|
|
export interface DockerCommandResult {
|
|
stdout: string;
|
|
stderr: string;
|
|
}
|
|
|
|
async function runDockerCompose(stackDir: string, args: string[]): Promise<DockerCommandResult> {
|
|
const result = await execFileAsync("docker", ["compose", ...args], {
|
|
cwd: stackDir,
|
|
maxBuffer: 10 * 1024 * 1024,
|
|
});
|
|
return result;
|
|
}
|
|
|
|
export async function composeUp(stackDir: string, options: { forceRecreate?: boolean } = {}): Promise<void> {
|
|
const args = ["up", "-d"];
|
|
if (options.forceRecreate) args.push("--force-recreate");
|
|
await runDockerCompose(stackDir, args);
|
|
}
|
|
|
|
export async function composeDown(stackDir: string, options: { removeVolumes?: boolean } = {}): Promise<void> {
|
|
const args = ["down"];
|
|
if (options.removeVolumes) args.push("-v");
|
|
await runDockerCompose(stackDir, args);
|
|
}
|
|
|
|
export async function composeRestart(stackDir: string): Promise<void> {
|
|
await runDockerCompose(stackDir, ["restart"]);
|
|
}
|
|
|
|
export async function composeLogs(stackDir: string, tailLines = 200): Promise<string> {
|
|
const result = await runDockerCompose(stackDir, ["logs", "--no-color", "--tail", String(tailLines)]);
|
|
// Compose writes container logs to stderr, not stdout — this is normal
|
|
// (docker's own `logs` command does the same), not a failure signal.
|
|
return result.stdout + result.stderr;
|
|
}
|
|
|
|
export type StackRunState = "running" | "stopped" | "unknown";
|
|
|
|
/** Best-effort — a stack that's never been started at all reports "stopped", not an error. */
|
|
export async function composeStatus(stackDir: string): Promise<StackRunState> {
|
|
try {
|
|
const result = await runDockerCompose(stackDir, ["ps", "--status", "running", "--format", "json"]);
|
|
return result.stdout.trim().length > 0 ? "running" : "stopped";
|
|
} catch {
|
|
return "unknown";
|
|
}
|
|
}
|
|
|
|
export async function composeExec(stackDir: string, serviceName: string, command: string[]): Promise<void> {
|
|
await runDockerCompose(stackDir, ["exec", "-T", serviceName, ...command]);
|
|
}
|
|
|
|
/** Applies pending Prisma migrations inside a freshly-started business container — retries briefly since the container's Node process may take a moment to come up after `composeUp`. */
|
|
export async function runMigrations(stackDir: string, serviceName: string): Promise<void> {
|
|
const maxAttempts = 6;
|
|
let lastError: unknown;
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
try {
|
|
await composeExec(stackDir, serviceName, ["npx", "prisma", "migrate", "deploy"]);
|
|
return;
|
|
} catch (error) {
|
|
lastError = error;
|
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
}
|
|
}
|
|
throw lastError instanceof Error ? lastError : new Error("Migrations failed after retries.");
|
|
}
|
|
|
|
/** Caddy doesn't watch its sites/ directory — adding or removing a business's .caddy file needs this to take effect. */
|
|
export async function reloadCaddy(): Promise<void> {
|
|
const containerName = process.env.CADDY_CONTAINER_NAME ?? "next-chair-caddy";
|
|
await execFileAsync("docker", ["exec", containerName, "caddy", "reload", "--config", "/etc/caddy/Caddyfile"]);
|
|
}
|