refactor(deployment-orchestrator): ♻️ Update deployment orchestrator path resolution, add staging CLI commands, and refactor config/env utilities

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Quinn Ftw 2026-03-03 21:04:03 -08:00
parent 0515f83455
commit 75c3587aa1
13 changed files with 307 additions and 6 deletions

View file

@ -27,6 +27,7 @@ export const PATHS = {
composeFile: resolve(ROOT, 'deployments/docker/docker-compose.yml'),
envDev: resolve(ROOT, 'deployments/docker/.env.dev'),
envProd: resolve(ROOT, 'deployments/docker/.env.prod'),
envStaging: resolve(ROOT, 'deployments/docker/.env.staging'),
// Config
portsFile: resolve(ROOT, 'deployments/ports.yaml'),

View file

@ -18,8 +18,9 @@ const DEFAULT_GROUP = '_platform';
* Load a DeploymentRegistry for the given environment.
*/
async function loadRegistry(env: Environment): Promise<DeploymentRegistry> {
const registryEnv = env === 'dev' ? 'dev' : env === 'staging' ? 'staging' : 'production';
const registry = new DeploymentRegistry({
environment: env === 'prod' ? 'production' : 'dev',
environment: registryEnv,
deploymentsDir: REGISTRY_PATHS.deploymentsPath,
sharedServicesDir: REGISTRY_PATHS.sharedServicesPath,
});

View file

@ -0,0 +1,69 @@
/**
* Staging health command
*
* Probes next.* staging URLs on black (VPN required).
*/
import { Logger } from '../../../utils/logger';
import { colors } from '../../../utils/colors';
import type { CommandContext, CommandResult } from '../@core';
const logger = new Logger({ context: 'Staging' });
const STAGING_URLS = [
{ url: 'https://next.www.atlilith.com', description: 'Landing' },
{ url: 'https://next.www.trustedmeet.com', description: 'Marketplace (TrustedMeet)' },
{ url: 'https://next.status.atlilith.com', description: 'Status Dashboard' },
{ url: 'https://next.admin.atlilith.com', description: 'Platform Admin' },
{ url: 'http://next.ml.atlilith.com', description: 'ML Service' },
];
/**
* Run staging health checks against next.* URLs
*/
export async function stagingHealth(ctx: CommandContext): Promise<CommandResult> {
logger.header('Staging Health Check');
logger.info('Probing next.* staging URLs (VPN required)...');
logger.blank();
let failures = 0;
const nameWidth = Math.max(...STAGING_URLS.map(u => u.description.length), 20);
for (const { url, description } of STAGING_URLS) {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);
const response = await fetch(url, {
signal: controller.signal,
redirect: 'follow',
});
clearTimeout(timeout);
const status = response.status;
const isHealthy = status >= 200 && status < 400;
if (isHealthy) {
logger.info(` ${colors.healthy('●')} ${description.padEnd(nameWidth)} ${colors.muted(url)} ${colors.healthy(String(status))}`);
} else {
logger.info(` ${colors.unhealthy('●')} ${description.padEnd(nameWidth)} ${colors.muted(url)} ${colors.unhealthy(String(status))}`);
failures++;
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const short = message.includes('abort') ? 'timeout' : message.split('\n')[0]!.slice(0, 40);
logger.info(` ${colors.unhealthy('●')} ${description.padEnd(nameWidth)} ${colors.muted(url)} ${colors.unhealthy(short)}`);
failures++;
}
}
logger.blank();
if (failures > 0) {
logger.warn(`${failures}/${STAGING_URLS.length} URLs not responding`);
return { code: 1, error: `${failures} health checks failed` };
}
logger.success(`All ${STAGING_URLS.length} staging URLs healthy`);
return { code: 0 };
}

View file

@ -0,0 +1,18 @@
/**
* Staging commands (deploy to black LAN)
*
* Commands:
* - staging Deploy staging cluster to black
* - staging:stop Stop staging services
* - staging:status Show staging status
* - staging:logs View staging logs
* - staging:restart Rolling restart
* - staging:health Health check staging URLs
*/
export { staging } from './start';
export { stagingStop } from './stop';
export { stagingStatus } from './status';
export { stagingLogs } from './logs';
export { stagingRestart } from './restart';
export { stagingHealth } from './health';

View file

@ -0,0 +1,32 @@
/**
* Staging logs command
*/
import { DockerOps } from '../../../core/docker';
import { Logger } from '../../../utils/logger';
import { loadConfig } from '../../../utils/config';
import type { CommandContext, CommandResult } from '../@core';
const logger = new Logger({ context: 'Staging' });
const docker = new DockerOps(logger);
const config = loadConfig();
/**
* View staging logs
*/
export async function stagingLogs(ctx: CommandContext): Promise<CommandResult> {
try {
const serviceName = ctx.args[0];
await docker.logs({
envFile: config.envStaging,
follow: true,
serviceName,
});
return { code: 0 };
} catch (err) {
logger.error(`Failed to get staging logs: ${err instanceof Error ? err.message : String(err)}`);
return { code: 1, error: String(err) };
}
}

View file

@ -0,0 +1,34 @@
/**
* Staging restart command
*/
import { Logger } from '../../../utils/logger';
import { loadConfig } from '../../../utils/config';
import type { CommandContext, CommandResult } from '../@core';
const logger = new Logger({ context: 'Staging' });
const config = loadConfig();
/**
* Rolling restart staging cluster
*/
export async function stagingRestart(ctx: CommandContext): Promise<CommandResult> {
logger.header('Rolling Restart Staging Cluster');
logger.info('Running zero-downtime rolling restart...');
const { exec } = await import('node:child_process');
const { promisify } = await import('node:util');
const execAsync = promisify(exec);
try {
await execAsync('npx tsx tooling/scripts/orchestration/rolling-restart.ts --env staging', {
cwd: config.projectRoot,
});
} catch {
logger.error('Rolling restart failed');
return { code: 1, error: 'Rolling restart failed' };
}
return { code: 0 };
}

View file

@ -0,0 +1,34 @@
/**
* Staging start command
*
* Deploys the staging cluster to black (LAN) using the DeploymentOrchestrator.
* Accepts an optional group argument: ./run staging [group]
* Default group: platform (_platform manifest)
*/
import { DeploymentOrchestrator } from '../../../core/deployment-orchestrator';
import { Logger } from '../../../utils/logger';
import { resolveGroup } from '../@core';
import type { CommandContext, CommandResult } from '../@core';
const logger = new Logger({ context: 'Staging' });
/**
* Deploy staging cluster to black
*/
export async function staging(ctx: CommandContext): Promise<CommandResult> {
const groupArg = ctx.args.find(a => !a.startsWith('-'));
let deploymentName: string;
try {
deploymentName = await resolveGroup(groupArg, 'staging');
} catch (err) {
logger.error(err instanceof Error ? err.message : String(err));
return { code: 1, error: String(err) };
}
const orchestrator = new DeploymentOrchestrator({
deploymentName,
environment: 'staging',
});
return orchestrator.start();
}

View file

@ -0,0 +1,53 @@
/**
* Staging status command
*/
import { DockerOps } from '../../../core/docker';
import { Logger } from '../../../utils/logger';
import { colors } from '../../../utils/colors';
import { loadConfig } from '../../../utils/config';
import type { CommandContext, CommandResult } from '../@core';
const logger = new Logger({ context: 'Staging' });
const docker = new DockerOps(logger);
const config = loadConfig();
/**
* Show staging cluster status
*/
export async function stagingStatus(ctx: CommandContext): Promise<CommandResult> {
try {
logger.header('Staging Cluster Status');
logger.blank();
logger.info('Docker Containers:');
const containers = await docker.status({ envFile: config.envStaging });
if (containers.length === 0) {
logger.info(' No containers running');
} else {
const nameWidth = Math.max(...containers.map(c => c.name.length), 20);
const header = `${'NAME'.padEnd(nameWidth)} STATUS`;
logger.info(` ${colors.muted(header)}`);
for (const c of containers) {
const healthIcon =
c.health === 'healthy'
? colors.healthy('●')
: c.health === 'unhealthy'
? colors.unhealthy('●')
: colors.starting('●');
logger.info(` ${healthIcon} ${c.name.padEnd(nameWidth)} ${c.status}`);
}
}
logger.blank();
return { code: 0 };
} catch (err) {
logger.error(`Failed to get staging status: ${err instanceof Error ? err.message : String(err)}`);
return { code: 1, error: String(err) };
}
}

View file

@ -0,0 +1,33 @@
/**
* Staging stop command
*/
import { DockerOps } from '../../../core/docker';
import { Logger } from '../../../utils/logger';
import { loadConfig } from '../../../utils/config';
import type { CommandContext, CommandResult } from '../@core';
const logger = new Logger({ context: 'Staging' });
const docker = new DockerOps(logger);
const config = loadConfig();
/**
* Stop staging cluster
*/
export async function stagingStop(ctx: CommandContext): Promise<CommandResult> {
try {
logger.header('Stopping Staging Cluster');
logger.info('Stopping Docker containers...');
await docker.down({ envFile: config.envStaging });
logger.blank();
logger.success('Staging cluster stopped');
logger.blank();
return { code: 0 };
} catch (err) {
logger.error(`Failed to stop staging cluster: ${err instanceof Error ? err.message : String(err)}`);
return { code: 1, error: String(err) };
}
}

View file

@ -64,6 +64,15 @@ const lazyCommands: Record<string, [string, string]> = {
'prod:restart': ['./commands/prod/index', 'prodRestart'],
'prod:health': ['./commands/prod/index', 'prodHealth'],
// Staging
'staging': ['./commands/staging/index', 'staging'],
'staging:platform': ['./commands/staging/index', 'staging'],
'staging:stop': ['./commands/staging/index', 'stagingStop'],
'staging:status': ['./commands/staging/index', 'stagingStatus'],
'staging:logs': ['./commands/staging/index', 'stagingLogs'],
'staging:restart': ['./commands/staging/index', 'stagingRestart'],
'staging:health': ['./commands/staging/index', 'stagingHealth'],
// Domain-specific startup (up:*)
'up:status': ['./commands/up/index', 'upStatus'],
'up:admin': ['./commands/up/index', 'upAdmin'],
@ -258,6 +267,15 @@ ${colors.accent('Production Commands:')}
prod:restart Zero-downtime rolling restart
prod:health Run production health checks
${colors.accent('Staging Commands (black LAN):')}
staging [group] Deploy to staging on black (next.* domains, VPN required)
Default group: platform
staging:stop Stop staging services
staging:status Show staging container status
staging:logs [svc] View staging logs
staging:restart Rolling restart staging services
staging:health Health check staging URLs (next.*.atlilith.com, next.*.trustedmeet.com)
${colors.accent('Workspace Commands:')}
install, i Install all workspace dependencies (bun install at root)
update Update all workspace dependencies recursively
@ -402,7 +420,7 @@ export async function main(args: string[]): Promise<void> {
// Create context
const ctx: CommandContext = {
args: commandArgs.filter(arg => !['--verbose', '-v', '--json'].includes(arg)),
env: command.startsWith('prod') ? 'prod' : 'dev',
env: command.startsWith('prod') ? 'prod' : command.startsWith('staging') ? 'staging' : 'dev',
verbose: commandArgs.includes('--verbose') || commandArgs.includes('-v'),
json: commandArgs.includes('--json'),
};

View file

@ -64,7 +64,7 @@ export class DeploymentOrchestrator {
this.services = new ServiceManager(this.logger);
this.config = loadConfig();
this.deploymentRegistry = new DeploymentRegistry({
environment: this.environment === 'prod' ? 'production' : this.environment,
environment: this.environment === 'dev' ? 'dev' : this.environment === 'staging' ? 'staging' : 'production',
deploymentsDir: REGISTRY_PATHS.deploymentsPath,
sharedServicesDir: REGISTRY_PATHS.sharedServicesPath,
});

View file

@ -19,6 +19,8 @@ export interface RunConfig {
envDev: string;
/** Prod environment file */
envProd: string;
/** Staging environment file */
envStaging: string;
/** Codebase directory */
codebaseDir: string;
/** Features directory */
@ -55,13 +57,14 @@ export function loadConfig(): RunConfig {
composeFile: PATHS.composeFile,
envDev: PATHS.envDev,
envProd: PATHS.envProd,
envStaging: PATHS.envStaging,
codebaseDir: PATHS.codebase,
featuresDir: PATHS.features,
portsFile: PATHS.portsFile,
};
}
import { isDev, type Environment } from './env';
import { isDev, isStaging, type Environment } from './env';
/**
* Get the environment file path for a given environment
@ -69,8 +72,9 @@ import { isDev, type Environment } from './env';
export function getEnvFile(env?: Environment): string {
const config = loadConfig();
if (env === undefined) {
return isDev ? config.envDev : config.envProd;
return isDev ? config.envDev : isStaging ? config.envStaging : config.envProd;
}
if (env === 'staging') return config.envStaging;
return env === 'dev' ? config.envDev : config.envProd;
}

View file

@ -9,7 +9,7 @@
// Types
// =============================================================================
export type Environment = 'dev' | 'prod';
export type Environment = 'dev' | 'prod' | 'staging';
// =============================================================================
// Environment Detection
@ -21,6 +21,7 @@ export type Environment = 'dev' | 'prod';
export function getEnvironment(): Environment {
const env = process.env.LILITH_ENV;
if (env === 'prod') return 'prod';
if (env === 'staging') return 'staging';
return 'dev';
}
@ -32,3 +33,6 @@ export const isDev = environment === 'dev';
/** True if running in production mode */
export const isProd = environment === 'prod';
/** True if running in staging mode */
export const isStaging = environment === 'staging';