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:
parent
0515f83455
commit
75c3587aa1
13 changed files with 307 additions and 6 deletions
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
69
run/cli/commands/staging/health.ts
Normal file
69
run/cli/commands/staging/health.ts
Normal 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 };
|
||||
}
|
||||
18
run/cli/commands/staging/index.ts
Normal file
18
run/cli/commands/staging/index.ts
Normal 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';
|
||||
32
run/cli/commands/staging/logs.ts
Normal file
32
run/cli/commands/staging/logs.ts
Normal 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) };
|
||||
}
|
||||
}
|
||||
34
run/cli/commands/staging/restart.ts
Normal file
34
run/cli/commands/staging/restart.ts
Normal 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 };
|
||||
}
|
||||
34
run/cli/commands/staging/start.ts
Normal file
34
run/cli/commands/staging/start.ts
Normal 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();
|
||||
}
|
||||
53
run/cli/commands/staging/status.ts
Normal file
53
run/cli/commands/staging/status.ts
Normal 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) };
|
||||
}
|
||||
}
|
||||
33
run/cli/commands/staging/stop.ts
Normal file
33
run/cli/commands/staging/stop.ts
Normal 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) };
|
||||
}
|
||||
}
|
||||
|
|
@ -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'),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue