cfn-forge
Version:
CloudFormation deployment automation tool with git-based workflows
233 lines (194 loc) • 6.73 kB
JavaScript
/**
* Deployment utilities for managing bash script installation
*/
const fs = require('fs').promises;
const path = require('path');
const { logger } = require('./logger');
async function ensureDeploymentScripts() {
const deployDir = path.join(process.cwd(), 'deploy');
const modulesDir = path.join(deployDir, 'modules');
try {
// Create directories
await fs.mkdir(modulesDir, { recursive: true });
// Copy main deploy script
const mainScriptSource = path.join(process.env.CFN_FORGE_CORE, 'deploy.sh');
const mainScriptDest = path.join(deployDir, 'deploy.sh');
await copyDeployScript(mainScriptSource, mainScriptDest);
// Copy all modules
const modules = [
'constants.sh',
'utils.sh',
'error-handling.sh',
'git-utils.sh',
'aws-utils.sh',
'validation.sh',
'interactive.sh',
'deployment.sh',
];
for (const module of modules) {
const source = path.join(process.env.CFN_FORGE_CORE, 'modules', module);
const dest = path.join(modulesDir, module);
await copyDeployScript(source, dest);
}
// Create initial .env file if it doesn't exist
const envFile = path.join(deployDir, '.env');
const envExists = await fs.access(envFile).then(() => true).catch(() => false);
if (!envExists) {
await createInitialEnvFile(envFile);
}
// Remove install marker
try {
await fs.unlink(path.join(deployDir, '.install-required'));
} catch {
// Ignore if doesn't exist
}
logger.success('Deployment scripts installed successfully');
} catch (error) {
logger.error(`Failed to install deployment scripts: ${error.message}`);
throw error;
}
}
async function copyDeployScript(source, dest) {
try {
const content = await fs.readFile(source, 'utf8');
// Update any paths that need to be relative to the project
const updatedContent = content
.replace(/\$CFN_FORGE_HOME/g, process.env.CFN_FORGE_HOME)
.replace(/\$CFN_FORGE_ROOT/g, process.env.CFN_FORGE_ROOT);
await fs.writeFile(dest, updatedContent);
await fs.chmod(dest, 0o755); // Make executable
} catch (error) {
// If source doesn't exist, try to download from GitHub
if (error.code === 'ENOENT') {
await downloadDeployScript(source, dest);
} else {
throw error;
}
}
}
async function downloadDeployScript(source, dest) {
const scriptName = path.basename(source);
// Allow branch override via environment variable, default to main for stability
const branch = process.env.CFN_FORGE_BRANCH || 'main';
const baseUrl = `https://raw.githubusercontent.com/dmleblanc/cfn-forge/${branch}/src/core`;
let url;
if (scriptName === 'deploy.sh') {
url = `${baseUrl}/deploy.sh`;
} else {
url = `${baseUrl}/modules/${scriptName}`;
}
logger.debug(`Downloading ${scriptName} from ${url} (branch: ${branch})`);
const https = require('https');
const content = await new Promise((resolve, reject) => {
https.get(url, (res) => {
let data = '';
res.on('data', (chunk) => data += chunk);
res.on('end', () => resolve(data));
res.on('error', reject);
}).on('error', reject);
});
await fs.writeFile(dest, content);
await fs.chmod(dest, 0o755);
}
async function createInitialEnvFile(envFile) {
const projectConfig = await loadProjectConfig();
const environment = process.env.CFN_FORGE_ENV || 'dev';
const envConfig = projectConfig.environments?.[environment] || {};
const content = `# Auto-generated by cfn-forge
# Edit this file to customize your deployment configuration
# CloudFormation Stack Configuration
STACK_NAME=${envConfig.stackName || projectConfig.project?.name || 'my-stack'}
TEMPLATE_FILE=../infrastructure/main.yaml
AWS_REGION=${envConfig.region || projectConfig.defaults?.region || 'us-east-1'}
SECRET_NAME=${envConfig.secretName || `${envConfig.stackName}-secrets`}
# S3 Deployment Configuration
BUCKET_NAME=${envConfig.bucketName || `${envConfig.stackName}-deployments-${Date.now()}`}
# Lambda Configuration
LAMBDA_SOURCE_DIR=${projectConfig.lambda?.sourceDir || '../lambda'}
# Deployment Behavior
DEPLOY_ON_TAGS=${envConfig.deployOnTags === true ? 'true' : 'false'}
# Environment
ENVIRONMENT=${environment}
# Auto-generated values (managed by deployment script)
# LAMBDA_S3_KEY=
# VERSION=
# GIT_COMMIT=
# GIT_BRANCH=
# GIT_TAG=
# LAST_DEPLOY_DATE=
# LAST_SUCCESSFUL_DEPLOY_DATE=
# LAST_DEPLOY_FAILED=
`;
await fs.writeFile(envFile, content);
logger.debug('Created initial .env file');
}
async function loadProjectConfig() {
try {
const yaml = require('js-yaml');
const configPath = path.join(process.cwd(), '.cfn-forge.yaml');
const content = await fs.readFile(configPath, 'utf8');
return yaml.load(content);
} catch {
return {};
}
}
async function updateDeploymentScripts() {
logger.info('Checking for deployment script updates...');
const deployDir = path.join(process.cwd(), 'deploy');
const versionFile = path.join(deployDir, '.version');
try {
const currentVersion = await fs.readFile(versionFile, 'utf8').catch(() => '0.0.0');
const latestVersion = require('../../../package.json').version;
if (currentVersion !== latestVersion) {
logger.info(`Updating deployment scripts from ${currentVersion} to ${latestVersion}`);
await ensureDeploymentScripts();
await fs.writeFile(versionFile, latestVersion);
logger.success('Deployment scripts updated successfully');
} else {
logger.debug('Deployment scripts are up to date');
}
} catch (error) {
logger.error(`Failed to update deployment scripts: ${error.message}`);
}
}
async function validateDeploymentEnvironment() {
const checks = [
{
name: 'AWS CLI',
command: 'aws --version',
error: 'AWS CLI is not installed. Please install it first.',
},
{
name: 'Git',
command: 'git --version',
error: 'Git is not installed. Please install it first.',
},
{
name: 'fswatch',
command: 'fswatch --version',
error: 'fswatch is not installed. Please install it first.',
optional: true,
},
];
const results = [];
for (const check of checks) {
try {
const { execSync } = require('child_process');
execSync(check.command, { stdio: 'ignore' });
results.push({ name: check.name, status: 'ok' });
} catch {
results.push({
name: check.name,
status: check.optional ? 'warning' : 'error',
message: check.error,
});
}
}
return results;
}
module.exports = {
ensureDeploymentScripts,
updateDeploymentScripts,
validateDeploymentEnvironment,
loadProjectConfig,
};