cfn-forge
Version:
CloudFormation deployment automation tool with git-based workflows
187 lines (151 loc) ⢠5.47 kB
JavaScript
/**
* Watch Command - Cross-platform deployment watcher using Node.js
*/
const path = require('path');
const fs = require('fs').promises;
const yaml = require('js-yaml');
const { logger } = require('../utils/logger');
const DeploymentEngine = require('../../lib/core/deployment/DeploymentEngine');
async function watchCommand(options) {
const command = this;
logger.info('Starting deployment watcher...');
try {
// Check if in a cfn-forge project
try {
await fs.access('.cfn-forge.yaml');
} catch {
logger.error('Not in a cfn-forge project!');
logger.info('Run "cfn-forge init" to create a new project');
process.exit(1);
}
// Load project configuration
const configContent = await fs.readFile('.cfn-forge.yaml', 'utf8');
const config = yaml.load(configContent);
// Determine environment
const environment = command.parent.opts().env || config.defaultEnvironment || 'development';
if (!config.environments || !config.environments[environment]) {
logger.error(`Environment '${environment}' not found in configuration`);
logger.info(`Available environments: ${Object.keys(config.environments || {}).join(', ')}`);
process.exit(1);
}
const envConfig = config.environments[environment];
logger.info(`Using environment: ${environment}`);
logger.info(`Stack: ${envConfig.stackName}`);
logger.info(`Region: ${envConfig.region || process.env.AWS_REGION || 'default'}`);
// Create deployment engine with AWS config
const deploymentEngine = new DeploymentEngine({
aws: {
region: envConfig.region || command.parent.opts().region || process.env.AWS_REGION,
profile: command.parent.opts().profile || envConfig.profile,
},
deployOnTags: config.deployOnTags || false,
watchPaths: ['.'],
ignorePatterns: [
'node_modules/**',
'.git/**',
'dist/**',
'build/**',
'**/*.tmp',
'.cfn-forge/**',
'deploy/**',
...(options.ignore ? [options.ignore] : []),
],
});
// Initialize engine
await deploymentEngine.initialize({
environment,
...config,
});
// Set up event listeners
deploymentEngine.on('watchingStarted', ({ paths }) => {
logger.info(`š Watching paths: ${paths.join(', ')}`);
logger.info('š Waiting for git changes...');
logger.info('Press Ctrl+C to stop');
});
deploymentEngine.on('deploymentStarted', ({ stackName }) => {
logger.info(`\nš Deploying 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.stackName,
templatePath: envConfig.template || config.template || 'template.yaml',
parameters: prepareParameters(envConfig, options),
tags: {
...config.tags,
...envConfig.tags,
Environment: environment,
},
force: false,
};
// Start watching
await deploymentEngine.startWatching({
interval: options.interval ? parseInt(options.interval) : 2000,
deploymentOptions: deployOptions,
});
// Handle graceful shutdown
const shutdown = async () => {
logger.info('\nš Shutting down deployment watcher...');
await deploymentEngine.stopWatching();
process.exit(0);
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
// Keep process alive
process.stdin.resume();
} catch (error) {
logger.error(`Failed to start deployment watcher: ${error.message}`);
process.exit(1);
}
}
/**
* Prepare parameters for deployment
*/
function prepareParameters(envConfig, options) {
let allParameters = {};
// Start with environment-specific parameters from .cfn-forge.yaml
if (envConfig.parameters) {
allParameters = { ...envConfig.parameters };
}
// Parse CLI parameters if provided (highest priority)
if (options.parameters) {
const cliParams = parseParameterString(options.parameters);
Object.assign(allParameters, cliParams);
}
return allParameters;
}
/**
* 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}"`);
}
parameters[key] = value;
}
return parameters;
}
module.exports = watchCommand;