UNPKG

cfn-forge

Version:

CloudFormation deployment automation tool with git-based workflows

1,031 lines (896 loc) • 32.6 kB
/** * CFN-Forge Deploy Command * Orchestrates CloudFormation deployments using cross-platform Node.js architecture */ const { execSync } = require('child_process'); const fs = require('fs').promises; const path = require('path'); const yaml = require('js-yaml'); const { logger } = require('../utils/logger'); const { getAWSConfig, setAWSEnvironmentVariables } = require('./config'); const { getProjectAWSProfile, validateAWSAccount } = require('../utils/config'); const { withAWSErrorHandling } = require('../utils/aws-error-parser'); const DeploymentEngine = require('../../lib/core/deployment/DeploymentEngine'); /** * Validate that we're in a CFN-Forge project */ async function validateProject() { const op = logger.trackOperation('Project Validation', { cwd: process.cwd(), }); const startTime = op.start(); try { await fs.access('.cfn-forge.yaml'); op.success(startTime, 'Project validation passed'); } catch (error) { const enhancedError = new Error( 'Not in a CFN-Forge project directory.\n' + 'Run `cfn-forge init` to initialize a new project.', ); enhancedError.code = 'PROJECT_NOT_FOUND'; enhancedError.originalError = error; op.error(enhancedError, startTime); throw enhancedError; } } /** * Load and validate project configuration */ async function loadProjectConfig() { const op = logger.trackOperation('Configuration Loading', { configFile: '.cfn-forge.yaml', }); const startTime = op.start(); try { const content = await fs.readFile('.cfn-forge.yaml', 'utf8'); const config = yaml.load(content); if (!config.project) { const error = new Error('Invalid .cfn-forge.yaml: missing project section'); error.code = 'INVALID_CONFIG'; throw error; } if (!config.environments) { const error = new Error('Invalid .cfn-forge.yaml: missing environments section'); error.code = 'INVALID_CONFIG'; throw error; } logger.debug(`Loaded config with ${Object.keys(config.environments).length} environments`); op.success(startTime, 'Configuration loaded successfully'); return config; } catch (error) { if (error.code === 'ENOENT') { const enhancedError = new Error('.cfn-forge.yaml not found. Run `cfn-forge init` first.'); enhancedError.code = 'CONFIG_NOT_FOUND'; enhancedError.originalError = error; op.error(enhancedError, startTime); throw enhancedError; } if (error.name === 'YAMLException') { const enhancedError = new Error(`YAML syntax error in .cfn-forge.yaml: ${error.message}`); enhancedError.code = 'YAML_SYNTAX_ERROR'; enhancedError.originalError = error; op.error(enhancedError, startTime); throw enhancedError; } op.error(error, startTime); throw error; } } /** * Resolve target environment for deployment */ function resolveEnvironment(options, config) { // Priority order: // 1. Command line --env flag // 2. Git branch mapping // 3. Default to 'dev' if (options.environment) { logger.info(`Using environment from command line: ${options.environment}`); return options.environment; } // Get current git branch for automatic environment mapping try { const branch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim(); // Common branch -> environment mappings const branchMappings = { main: 'prod', master: 'prod', production: 'prod', staging: 'staging', develop: 'staging', development: 'dev', dev: 'dev', }; const mappedEnv = branchMappings[branch]; if (mappedEnv && config.environments[mappedEnv]) { logger.info(`🌿 Detected branch '${branch}' → environment '${mappedEnv}'`); return mappedEnv; } } catch (error) { logger.debug('Could not detect git branch for environment mapping'); } // Default to dev return 'dev'; } /** * Validate environment configuration */ function validateEnvironment(config, environment) { if (!config.environments[environment]) { const available = Object.keys(config.environments).join(', '); throw new Error( `Environment '${environment}' not found in .cfn-forge.yaml.\n` + `Available environments: ${available}`, ); } const envConfig = config.environments[environment]; // Check for stack name - either directly specified or base + suffix const hasDirectStackName = envConfig.stackName; const hasBaseStackName = config.cloudformation?.stackName; const hasStackSuffix = envConfig.stackSuffix; if (!hasDirectStackName && (!hasBaseStackName || !hasStackSuffix)) { throw new Error( `Environment '${environment}' missing stackName configuration.\n` + `Either specify 'stackName' directly in the environment, or use 'cloudformation.stackName' + 'environments.${environment}.stackSuffix'`, ); } // Construct final stack name if (hasDirectStackName) { envConfig.finalStackName = envConfig.stackName; } else { envConfig.finalStackName = `${hasBaseStackName}${hasStackSuffix}`; } // Check for template - either environment-specific or global const templatePath = envConfig.template || config.cloudformation?.template; if (!templatePath) { throw new Error(`Environment '${environment}' missing template configuration`); } envConfig.finalTemplate = templatePath; return envConfig; } /** * Validate CloudFormation template exists and is valid */ async function validateTemplate(templatePath) { const op = logger.trackOperation('Template Validation', { templatePath, }); const startTime = op.start(); try { await fs.access(templatePath); } catch (error) { const enhancedError = new Error(`CloudFormation template not found: ${templatePath}`); enhancedError.code = 'TEMPLATE_NOT_FOUND'; enhancedError.originalError = error; op.error(enhancedError, startTime); throw enhancedError; } // Validate template syntax with CloudFormation intrinsic functions support try { const content = await fs.readFile(templatePath, 'utf8'); // Try to parse as YAML with CloudFormation support or JSON try { // Define CloudFormation YAML schema (same as cost command) const cfnSchema = yaml.DEFAULT_SCHEMA.extend([ new yaml.Type('!Ref', { kind: 'scalar', construct(data) { return { Ref: data }; }, }), new yaml.Type('!GetAtt', { kind: 'scalar', construct(data) { return { 'Fn::GetAtt': data.split('.') }; }, }), new yaml.Type('!Sub', { kind: 'scalar', construct(data) { return { 'Fn::Sub': data }; }, }), new yaml.Type('!Join', { kind: 'sequence', construct(data) { return { 'Fn::Join': data }; }, }), new yaml.Type('!Select', { kind: 'sequence', construct(data) { return { 'Fn::Select': data }; }, }), new yaml.Type('!Split', { kind: 'sequence', construct(data) { return { 'Fn::Split': data }; }, }), new yaml.Type('!Equals', { kind: 'sequence', construct(data) { return { 'Fn::Equals': data }; }, }), new yaml.Type('!And', { kind: 'sequence', construct(data) { return { 'Fn::And': data }; }, }), new yaml.Type('!Or', { kind: 'sequence', construct(data) { return { 'Fn::Or': data }; }, }), new yaml.Type('!Not', { kind: 'sequence', construct(data) { return { 'Fn::Not': data }; }, }), new yaml.Type('!If', { kind: 'sequence', construct(data) { return { 'Fn::If': data }; }, }), new yaml.Type('!Condition', { kind: 'scalar', construct(data) { return { Condition: data }; }, }), new yaml.Type('!GetAZs', { kind: 'scalar', construct(data) { return { 'Fn::GetAZs': data }; }, }), new yaml.Type('!Cidr', { kind: 'sequence', construct(data) { return { 'Fn::Cidr': data }; }, }), ]); yaml.load(content, { schema: cfnSchema }); } catch (yamlError) { try { JSON.parse(content); logger.debug('Template parsed as JSON successfully'); } catch (jsonError) { const enhancedError = new Error(`Template syntax error: ${yamlError.message}`); enhancedError.code = 'TEMPLATE_SYNTAX_ERROR'; enhancedError.originalError = yamlError; enhancedError.templatePath = templatePath; op.error(enhancedError, startTime); throw enhancedError; } } op.success(startTime, `Template validation passed: ${templatePath}`); } catch (error) { if (!error.code) { const enhancedError = new Error(`Template validation failed: ${error.message}`); enhancedError.code = 'TEMPLATE_VALIDATION_FAILED'; enhancedError.originalError = error; enhancedError.templatePath = templatePath; op.error(enhancedError, startTime); throw enhancedError; } throw error; } } /** * Validate AWS access and permissions */ const validateAWSAccess = withAWSErrorHandling('AWS Access Validation', async () => { const op = logger.trackOperation('AWS Access Validation', { region: process.env.AWS_DEFAULT_REGION || process.env.AWS_REGION || 'default', }); const startTime = op.start(); try { // Check AWS credentials const identity = execSync('aws sts get-caller-identity --output json', { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], }); const callerInfo = JSON.parse(identity); logger.debug(`AWS access validated: ${callerInfo.Arn}`); op.success(startTime, `AWS access validated for ${callerInfo.Arn}`); return callerInfo; } catch (error) { error.stderr = error.stderr || error.message; op.error(error, startTime); throw error; } }); /** * Check git repository status */ function validateGitStatus(options) { const op = logger.trackOperation('Git Status Validation', { force: options.force || false, }); const startTime = op.start(); try { // Check if we're in a git repository execSync('git rev-parse --git-dir', { stdio: 'pipe' }); // Check for uncommitted changes (unless forced) if (!options.force) { const status = execSync('git status --porcelain', { encoding: 'utf8' }); if (status.trim()) { logger.warn('Uncommitted changes detected:'); console.log(status); logger.info('Commit changes or use --force to deploy anyway'); const error = new Error('Uncommitted changes detected'); error.code = 'UNCOMMITTED_CHANGES'; error.statusOutput = status; op.error(error, startTime); return false; } } else { logger.info('Force deployment enabled - ignoring uncommitted changes'); } op.success(startTime, 'Git status validation passed'); return true; } catch (error) { const enhancedError = new Error( 'Not in a git repository.\n' + 'Initialize git with: git init', ); enhancedError.code = 'NOT_GIT_REPOSITORY'; enhancedError.originalError = error; op.error(enhancedError, startTime); throw enhancedError; } } /** * Setup AWS environment for deployment */ async function setupAWSEnvironment(config, environment, options) { // Resolve AWS profile in priority order: // 1. Command line --aws-profile // 2. Environment-specific config // 3. Global project config // 4. Default profile let targetProfile = null; if (options.awsProfile) { targetProfile = options.awsProfile; logger.info(`Using AWS profile from command line: ${targetProfile}`); } else { const projectProfile = getProjectAWSProfile(environment); if (projectProfile && projectProfile.profile) { targetProfile = projectProfile.profile; logger.info(`Using AWS profile from project config: ${targetProfile}`); // Validate account ID if specified if (projectProfile.accountId) { logger.info(`Validating AWS account: ${projectProfile.accountId}`); await validateAWSAccount(projectProfile.accountId); logger.success('AWS account validation passed'); } } } // Set up AWS environment variables if (targetProfile) { setAWSEnvironmentVariables(targetProfile); } // Set region from environment config if specified const envConfig = config.environments[environment]; if (envConfig.region) { process.env.AWS_DEFAULT_REGION = envConfig.region; process.env.AWS_REGION = envConfig.region; logger.debug(`AWS region set to: ${envConfig.region}`); } } /** * Prepare parameters for deployment (synchronous version for new engine) */ function prepareParameters(envConfig, options) { let allParameters = {}; // 1. Start with environment-specific parameters from .cfn-forge.yaml if (envConfig.parameters) { allParameters = { ...envConfig.parameters }; } // 2. Parse CLI parameters (highest priority) if (options.parameters) { const cliParams = parseParameterString(options.parameters); Object.assign(allParameters, cliParams); } return allParameters; } /** * Parse CloudFormation parameters from various sources */ async function parseParameters(envConfig, options) { const op = logger.trackOperation('Parameter Parsing', { hasEnvParams: !!(envConfig.parameters), hasCLIParams: !!(options.parameters), hasParamFile: !!(options.parameterFile), }); const startTime = op.start(); try { let allParameters = {}; // 1. Start with environment-specific parameters from .cfn-forge.yaml if (envConfig.parameters) { allParameters = { ...envConfig.parameters }; logger.debug(`Loaded ${Object.keys(envConfig.parameters).length} parameters from environment config`); } // 2. Load parameters from file if specified if (options.parameterFile) { try { const paramFileContent = await fs.readFile(options.parameterFile, 'utf8'); const fileParams = JSON.parse(paramFileContent); if (typeof fileParams !== 'object' || Array.isArray(fileParams)) { throw new Error('Parameter file must contain a JSON object'); } // Filter out comment fields (starting with _) and merge file parameters const actualFileParams = {}; Object.entries(fileParams).forEach(([key, value]) => { if (!key.startsWith('_')) { actualFileParams[key] = value; } }); Object.assign(allParameters, actualFileParams); logger.debug(`Loaded ${Object.keys(actualFileParams).length} parameters from file: ${options.parameterFile}`); } catch (error) { const enhancedError = new Error(`Failed to load parameter file: ${options.parameterFile}\n${error.message}`); enhancedError.code = 'PARAMETER_FILE_ERROR'; enhancedError.originalError = error; op.error(enhancedError, startTime); throw enhancedError; } } // 3. Parse CLI parameters (highest priority) if (options.parameters) { try { const cliParams = parseParameterString(options.parameters); // Merge CLI parameters (override file and environment config) Object.assign(allParameters, cliParams); logger.debug(`Loaded ${Object.keys(cliParams).length} parameters from CLI`); } catch (error) { const enhancedError = new Error(`Invalid CLI parameters format: ${options.parameters}\n${error.message}`); enhancedError.code = 'CLI_PARAMETERS_ERROR'; enhancedError.originalError = error; op.error(enhancedError, startTime); throw enhancedError; } } // Convert to CloudFormation parameter override format const parameterString = Object.entries(allParameters) .map(([key, value]) => `${key}=${value}`) .join(','); logger.debug(`Final parameter string: ${parameterString}`); op.success(startTime, `Parsed ${Object.keys(allParameters).length} total parameters`); return { parameters: allParameters, parameterString, }; } catch (error) { if (!error.code) { op.error(error, startTime); } throw error; } } /** * Parse parameter string from CLI (key=value,key2=value2) */ function parseParameterString(paramString) { const parameters = {}; // Split by comma and parse key=value pairs const pairs = paramString.split(',').map((pair) => pair.trim()).filter((pair) => pair); for (const pair of pairs) { const equalIndex = pair.indexOf('='); if (equalIndex === -1) { throw new Error(`Invalid parameter format: "${pair}" (expected key=value)`); } const key = pair.substring(0, equalIndex).trim(); const value = pair.substring(equalIndex + 1).trim(); if (!key) { throw new Error(`Empty parameter key in: "${pair}"`); } // Validate parameter key format (CloudFormation rules) if (!/^[a-zA-Z][a-zA-Z0-9]*$/.test(key)) { throw new Error(`Invalid parameter key: "${key}" (must start with letter, alphanumeric only)`); } // Handle quoted values let parsedValue = value; if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { parsedValue = value.slice(1, -1); } parameters[key] = parsedValue; } return parameters; } /** * Validate parameters against CloudFormation template */ async function validateParameters(templatePath, parameters) { const op = logger.trackOperation('Parameter Validation', { templatePath, parameterCount: Object.keys(parameters).length, }); const startTime = op.start(); try { // Read and parse template to get parameter definitions const templateContent = await fs.readFile(templatePath, 'utf8'); let template; try { // Try YAML first (with CloudFormation schema) const cfnSchema = yaml.DEFAULT_SCHEMA.extend([ new yaml.Type('!Ref', { kind: 'scalar', construct(data) { return { Ref: data }; }, }), new yaml.Type('!GetAtt', { kind: 'scalar', construct(data) { return { 'Fn::GetAtt': data.split('.') }; }, }), new yaml.Type('!Sub', { kind: 'scalar', construct(data) { return { 'Fn::Sub': data }; }, }), new yaml.Type('!Join', { kind: 'sequence', construct(data) { return { 'Fn::Join': data }; }, }), new yaml.Type('!Select', { kind: 'sequence', construct(data) { return { 'Fn::Select': data }; }, }), new yaml.Type('!Split', { kind: 'sequence', construct(data) { return { 'Fn::Split': data }; }, }), new yaml.Type('!Equals', { kind: 'sequence', construct(data) { return { 'Fn::Equals': data }; }, }), new yaml.Type('!And', { kind: 'sequence', construct(data) { return { 'Fn::And': data }; }, }), new yaml.Type('!Or', { kind: 'sequence', construct(data) { return { 'Fn::Or': data }; }, }), new yaml.Type('!Not', { kind: 'sequence', construct(data) { return { 'Fn::Not': data }; }, }), new yaml.Type('!If', { kind: 'sequence', construct(data) { return { 'Fn::If': data }; }, }), new yaml.Type('!Condition', { kind: 'scalar', construct(data) { return { Condition: data }; }, }), new yaml.Type('!GetAZs', { kind: 'scalar', construct(data) { return { 'Fn::GetAZs': data }; }, }), new yaml.Type('!Cidr', { kind: 'sequence', construct(data) { return { 'Fn::Cidr': data }; }, }), ]); template = yaml.load(templateContent, { schema: cfnSchema }); } catch (yamlError) { try { // Try JSON fallback template = JSON.parse(templateContent); } catch (jsonError) { throw new Error(`Template parsing failed: ${yamlError.message}`); } } const templateParameters = template.Parameters || {}; const issues = []; // Check for required parameters without defaults for (const [paramName, paramDef] of Object.entries(templateParameters)) { if (!paramDef.Default && !parameters.hasOwnProperty(paramName)) { issues.push(`Required parameter missing: ${paramName}`); } // Validate parameter values against constraints if (parameters.hasOwnProperty(paramName)) { const value = parameters[paramName]; // Check AllowedValues constraint if (paramDef.AllowedValues && !paramDef.AllowedValues.includes(value)) { issues.push(`Parameter ${paramName}: "${value}" not in allowed values [${paramDef.AllowedValues.join(', ')}]`); } // Check MinLength constraint if (paramDef.MinLength && value.length < paramDef.MinLength) { issues.push(`Parameter ${paramName}: "${value}" too short (minimum ${paramDef.MinLength} characters)`); } // Check MaxLength constraint if (paramDef.MaxLength && value.length > paramDef.MaxLength) { issues.push(`Parameter ${paramName}: "${value}" too long (maximum ${paramDef.MaxLength} characters)`); } // Check AllowedPattern constraint if (paramDef.AllowedPattern) { const pattern = new RegExp(paramDef.AllowedPattern); if (!pattern.test(value)) { issues.push(`Parameter ${paramName}: "${value}" does not match required pattern: ${paramDef.AllowedPattern}`); } } } } // Check for unexpected parameters for (const paramName of Object.keys(parameters)) { if (!templateParameters.hasOwnProperty(paramName)) { issues.push(`Unexpected parameter: ${paramName} (not defined in template)`); } } if (issues.length > 0) { const error = new Error(`Parameter validation failed:\n • ${issues.join('\n • ')}`); error.code = 'PARAMETER_VALIDATION_FAILED'; error.issues = issues; op.error(error, startTime); throw error; } op.success(startTime, `Validated ${Object.keys(parameters).length} parameters`); return { templateParameters, validatedParameters: parameters, issues: [], }; } catch (error) { if (!error.code) { op.error(error, startTime); } throw error; } } /** * Create environment variables for bash deployment scripts */ async function createDeploymentEnvironment(config, environment, options) { const envConfig = config.environments[environment]; // Parse and merge all parameter sources const { parameterString } = await parseParameters(envConfig, options); const deployEnv = { // CFN-Forge configuration CFN_FORGE_ENVIRONMENT: environment, CFN_FORGE_STACK_NAME: envConfig.finalStackName, CFN_FORGE_TEMPLATE: envConfig.finalTemplate, CFN_FORGE_REGION: envConfig.region || process.env.AWS_DEFAULT_REGION || 'us-east-1', // Deployment options CFN_FORGE_ONCE: options.once ? 'true' : 'false', CFN_FORGE_WAIT: options.wait ? 'true' : 'false', CFN_FORGE_DRY_RUN: options.dryRun ? 'true' : 'false', CFN_FORGE_NO_ROLLBACK: options.noRollback ? 'true' : 'false', // Git information CFN_FORGE_TAG: options.tag || '', // S3 packaging CFN_FORGE_PACKAGING_BUCKET: config.deployment?.packagingBucket || '', CFN_FORGE_CREATE_BUCKET: config.deployment?.createBucket ? 'true' : 'false', // CloudFormation parameters (enhanced) CFN_FORGE_PARAMETERS: parameterString, // Inherit all current environment variables ...process.env, }; return deployEnv; } /** * Execute one-time deployment using Node.js deployment engine */ async function executeOnceDeployment(config, environment, options) { logger.info(`Starting deployment to '${environment}' environment`); const envConfig = config.environments[environment]; logger.info(`Stack: ${envConfig.finalStackName}`); logger.info(`Region: ${envConfig.region || 'default'}`); if (options.dryRun) { logger.info('Dry run mode - showing deployment plan'); } try { // Create deployment engine with AWS config const deploymentEngine = new DeploymentEngine({ aws: { region: envConfig.region || process.env.AWS_REGION, profile: options.profile || envConfig.profile, }, deployOnTags: config.deployOnTags || false, }); // Initialize engine await deploymentEngine.initialize({ environment, ...config, }); // Set up event listeners for progress deploymentEngine.on('deploymentStarted', ({ stackName }) => { logger.info(`Deploying stack: ${stackName}`); }); deploymentEngine.on('waitingForStack', ({ stackName, operation }) => { logger.info(`Waiting for stack ${operation}...`); }); deploymentEngine.on('deploymentCompleted', ({ status, operation }) => { if (status === 'SUCCESS') { logger.success(`Deployment ${operation} completed successfully!`); } else if (operation === 'NO_CHANGE') { logger.info('No changes detected - stack is up to date'); } }); deploymentEngine.on('deploymentFailed', ({ error }) => { logger.error(`Deployment failed: ${error}`); }); // Prepare deployment options const deployOptions = { environment, stackName: envConfig.finalStackName, templatePath: envConfig.finalTemplate, parameters: prepareParameters(envConfig, options), tags: { ...config.tags, ...envConfig.tags, Environment: environment, }, dryRun: options.dryRun || false, force: options.force || false, }; // Execute deployment const result = await deploymentEngine.deploy(deployOptions); // Display outputs if available if (result.outputs && Object.keys(result.outputs).length > 0) { logger.info('\nStack Outputs:'); Object.entries(result.outputs).forEach(([key, output]) => { logger.info(` ${key}: ${output.value}`); }); } return result; } catch (error) { logger.error(`Deployment failed: ${error.message}`); throw error; } } /** * Start watch mode deployment using Node.js deployment engine */ async function startWatchDeployment(config, environment, options) { logger.info(`šŸ‘€ Starting deployment watcher for '${environment}' environment`); logger.info('šŸ“ Watching for git changes...'); try { const envConfig = config.environments[environment]; // Create deployment engine with AWS config const deploymentEngine = new DeploymentEngine({ aws: { region: envConfig.region || process.env.AWS_REGION, profile: options.profile || envConfig.profile, }, deployOnTags: config.deployOnTags || false, watchPaths: ['.'], ignorePatterns: [ 'node_modules/**', '.git/**', 'dist/**', 'build/**', '**/*.tmp', '.cfn-forge/**', 'deploy/**', ], }); // Initialize engine await deploymentEngine.initialize({ environment, ...config, }); // Set up event listeners deploymentEngine.on('watchingStarted', ({ paths }) => { logger.info(`šŸ‘€ Watching paths: ${paths.join(', ')}`); }); deploymentEngine.on('deploymentStarted', ({ stackName }) => { logger.info(`\nDeploying stack: ${stackName}`); }); deploymentEngine.on('waitingForStack', ({ stackName, operation }) => { logger.info(`Waiting for stack ${operation}...`); }); deploymentEngine.on('deploymentCompleted', ({ status, operation, stackName }) => { if (status === 'SUCCESS') { logger.success(`Deployment ${operation} completed successfully!`); } else if (status === 'SKIPPED') { logger.info('ā­ļø No changes detected - skipping deployment'); } }); deploymentEngine.on('deploymentFailed', ({ error }) => { logger.error(`Deployment failed: ${error}`); }); // Prepare deployment options const deployOptions = { environment, stackName: envConfig.finalStackName, templatePath: envConfig.finalTemplate, parameters: prepareParameters(envConfig, options), tags: { ...config.tags, ...envConfig.tags, Environment: environment, }, force: options.force || false, }; // Start watching await deploymentEngine.startWatching({ interval: 2000, deploymentOptions: deployOptions, }); // Handle graceful shutdown process.on('SIGINT', async () => { logger.info('\nšŸ›‘ Shutting down deployment watcher...'); await deploymentEngine.stopWatching(); process.exit(0); }); // Keep process alive process.stdin.resume(); } catch (error) { logger.error(`Failed to start deployment watcher: ${error.message}`); throw error; } } /** * Main deploy command handler */ async function deployCommand(options) { logger.setContext('command', 'deploy'); logger.setContext('options', options); // Enable timestamps for deployment output logger.enableTimestamps(); try { logger.info('CFN-Forge Deployment'); logger.info(''); // 1. Validate project and environment const { config, environment, envConfig } = await logger.withErrorHandling('Project Setup', async () => { await validateProject(); const config = await loadProjectConfig(); // 2. Resolve target environment logger.debug(`Deploy options received: ${JSON.stringify(options)}`); const environment = resolveEnvironment(options, config); const envConfig = validateEnvironment(config, environment); logger.info(`Target: ${environment} environment`); return { config, environment, envConfig }; }, { configFile: '.cfn-forge.yaml', cwd: process.cwd(), }); // 3. Validate template and git status await logger.withErrorHandling('Pre-deployment Validation', async () => { await validateTemplate(envConfig.finalTemplate); // Only validate git status if git-based deployment is enabled if (config.deployment?.gitBased !== false) { if (!validateGitStatus(options)) { const error = new Error('Git validation failed'); error.code = 'GIT_VALIDATION_FAILED'; throw error; } } else { logger.debug('Skipping git validation (gitBased: false)'); } }, { template: envConfig.finalTemplate, environment, }); // 4. Parse and validate parameters const { parameters } = await logger.withErrorHandling('Parameter Processing', async () => { const { parameters, parameterString } = await parseParameters(envConfig, options); // Validate parameters against template await validateParameters(envConfig.finalTemplate, parameters); // Display parameter summary if (Object.keys(parameters).length > 0) { logger.info('CloudFormation Parameters:'); Object.entries(parameters).forEach(([key, value]) => { logger.info(` • ${key} = ${value}`); }); } else { logger.info('No CloudFormation parameters specified'); } return { parameters, parameterString }; }, { template: envConfig.finalTemplate, environment, cliParams: options.parameters, paramFile: options.parameterFile, }); // 5. Validate AWS access await logger.withErrorHandling('AWS Validation', async () => { await validateAWSAccess(); await setupAWSEnvironment(config, environment, options); }, { environment, awsProfile: options.awsProfile, }); // 6. Execute deployment based on mode await logger.withErrorHandling('Deployment Execution', async () => { if (options.once) { await executeOnceDeployment(config, environment, options); } else { await startWatchDeployment(config, environment, options); } }, { mode: options.once ? 'once' : 'watch', environment, stack: envConfig.finalStackName, }); } catch (error) { logger.errorWithContext(error, 'Deploy Command'); // Show error summary and exit logger.reportErrorSummary(); process.exit(1); } finally { logger.clearContext(); } } module.exports = deployCommand;