@friggframework/frigg-cli
Version:
Frigg Framework CLI tool
303 lines (258 loc) • 10.2 kB
JavaScript
const { spawn } = require('child_process');
const path = require('path');
const fs = require('fs');
// Import doctor command for post-deployment health check
const { doctorCommand } = require('../doctor-command');
// Configuration constants
const PATHS = {
APP_DEFINITION: 'index.js',
INFRASTRUCTURE: 'infrastructure.js'
};
const COMMANDS = {
SERVERLESS: 'osls' // OSS-Serverless (drop-in replacement for serverless v3)
};
/**
* Constructs filtered environment variables for serverless deployment
* @param {string[]} appDefinedVariables - Array of environment variable names from app definition
* @returns {Object} Filtered environment variables object
*/
function buildFilteredEnvironment(appDefinedVariables) {
return {
// Essential system variables needed to run serverless
PATH: process.env.PATH,
HOME: process.env.HOME,
USER: process.env.USER,
// AWS credentials and configuration (all AWS_ prefixed variables)
...Object.fromEntries(
Object.entries(process.env).filter(([key]) =>
key.startsWith('AWS_')
)
),
// App-defined environment variables
...Object.fromEntries(
appDefinedVariables
.map((key) => [key, process.env[key]])
.filter(([_, value]) => value !== undefined)
),
};
}
/**
* Loads and parses the app definition from index.js
* @returns {Object|null} App definition object or null if not found
*/
function loadAppDefinition() {
const appDefPath = path.join(process.cwd(), PATHS.APP_DEFINITION);
if (!fs.existsSync(appDefPath)) {
return null;
}
try {
const { Definition } = require(appDefPath);
return Definition;
} catch (error) {
console.warn('Could not load appDefinition environment config:', error.message);
return null;
}
}
/**
* Extracts environment variable names from app definition
* @param {Object} appDefinition - App definition object
* @returns {string[]} Array of environment variable names
*/
function extractEnvironmentVariables(appDefinition) {
if (!appDefinition?.environment) {
return [];
}
console.log('🔧 Loading environment configuration from appDefinition...');
const appDefinedVariables = Object.keys(appDefinition.environment).filter(
(key) => appDefinition.environment[key] === true
);
console.log(` Found ${appDefinedVariables.length} environment variables: ${appDefinedVariables.join(', ')}`);
return appDefinedVariables;
}
/**
* Handles environment validation warnings
* @param {Object} validation - Validation result object
* @param {Object} options - Deploy command options
*/
function handleValidationWarnings(validation, options) {
if (validation.missing.length === 0 || options.skipEnvValidation) {
return;
}
console.warn(`⚠️ Warning: Missing ${validation.missing.length} environment variables: ${validation.missing.join(', ')}`);
console.warn(' These variables are optional and deployment will continue');
console.warn(' Run with --skip-env-validation to bypass this check');
}
/**
* Validates environment variables and builds filtered environment
* @param {Object} appDefinition - App definition object
* @param {Object} options - Deploy command options
* @returns {Object} Filtered environment variables
*/
function validateAndBuildEnvironment(appDefinition, options) {
if (!appDefinition) {
return buildFilteredEnvironment([]);
}
const appDefinedVariables = extractEnvironmentVariables(appDefinition);
// Try to use the env-validator if available
try {
const { validateEnvironmentVariables } = require('@friggframework/devtools/infrastructure/env-validator');
const validation = validateEnvironmentVariables(appDefinition);
handleValidationWarnings(validation, options);
return buildFilteredEnvironment(appDefinedVariables);
} catch (validatorError) {
// Validator not available, do basic validation
const missingVariables = appDefinedVariables.filter((variable) => !process.env[variable]);
if (missingVariables.length > 0) {
console.warn(`⚠️ Warning: Missing ${missingVariables.length} environment variables: ${missingVariables.join(', ')}`);
console.warn(' These variables are optional and deployment will continue');
console.warn(' Set them in your CI/CD environment or .env file if needed');
}
return buildFilteredEnvironment(appDefinedVariables);
}
}
/**
* Executes the serverless deployment command
* @param {Object} environment - Environment variables to pass to serverless
* @param {Object} options - Deploy command options
* @returns {Promise<number>} Exit code
*/
function executeServerlessDeployment(environment, options) {
return new Promise((resolve, reject) => {
console.log('🚀 Deploying serverless application...');
const serverlessArgs = [
'deploy',
'--config',
PATHS.INFRASTRUCTURE,
'--stage',
options.stage,
];
// Add --force flag if force option is true
if (options.force === true) {
serverlessArgs.push('--force');
}
const childProcess = spawn(COMMANDS.SERVERLESS, serverlessArgs, {
cwd: path.resolve(process.cwd()),
stdio: 'inherit',
env: {
...environment,
SLS_STAGE: options.stage, // Set stage for resource discovery
},
});
childProcess.on('error', (error) => {
console.error(`Error executing command: ${error.message}`);
reject(error);
});
childProcess.on('close', (code) => {
if (code !== 0) {
console.log(`Child process exited with code ${code}`);
resolve(code);
} else {
resolve(0);
}
});
});
}
/**
* Get stack name from app definition
* @param {Object} appDefinition - App definition
* @param {Object} options - Deploy options
* @returns {string|null} Stack name
*/
function getStackName(appDefinition, options) {
// Try to get from app definition
if (appDefinition?.name) {
const stage = options.stage || 'dev';
return `${appDefinition.name}-${stage}`;
}
// Try to get from infrastructure.js
const infraPath = path.join(process.cwd(), PATHS.INFRASTRUCTURE);
if (fs.existsSync(infraPath)) {
try {
const infraModule = require(infraPath);
if (infraModule.service) {
const stage = options.stage || 'dev';
return `${infraModule.service}-${stage}`;
}
} catch (error) {
// Ignore errors reading infrastructure file
}
}
return null;
}
/**
* Run post-deployment health check
* @param {string} stackName - CloudFormation stack name
* @param {Object} options - Deploy options
*/
async function runPostDeploymentHealthCheck(stackName, options) {
console.log('\n' + '═'.repeat(80));
console.log('Running post-deployment health check...');
console.log('═'.repeat(80));
try {
// Run doctor command (will exit process on its own)
// Note: We need to catch the exit to prevent deploy from exiting
const originalExit = process.exit;
let doctorExitCode = 0;
// Temporarily override process.exit to capture exit code
process.exit = (code) => {
doctorExitCode = code || 0;
};
try {
await doctorCommand(stackName, {
region: options.region,
format: 'console',
verbose: options.verbose,
});
} catch (error) {
console.log(`\n⚠️ Health check encountered an error: ${error.message}`);
if (options.verbose) {
console.error(error.stack);
}
} finally {
// Restore original process.exit
process.exit = originalExit;
}
// Inform user about health check results
if (doctorExitCode === 0) {
console.log('\n✓ Post-deployment health check: PASSED');
} else if (doctorExitCode === 2) {
console.log('\n⚠️ Post-deployment health check: DEGRADED');
console.log(' Run "frigg repair" to fix detected issues');
} else {
console.log('\n✗ Post-deployment health check: FAILED');
console.log(' Run "frigg doctor" for detailed report');
console.log(' Run "frigg repair" to fix detected issues');
}
} catch (error) {
console.log(`\n⚠️ Post-deployment health check failed: ${error.message}`);
if (options.verbose) {
console.error(error.stack);
}
}
}
async function deployCommand(options) {
console.log('Deploying the serverless application...');
const appDefinition = loadAppDefinition();
const environment = validateAndBuildEnvironment(appDefinition, options);
// Execute deployment
const exitCode = await executeServerlessDeployment(environment, options);
// Check if deployment was successful
if (exitCode !== 0) {
console.error(`\n✗ Deployment failed with exit code ${exitCode}`);
process.exit(exitCode);
}
console.log('\n✓ Deployment completed successfully!');
// Run post-deployment health check (unless --skip-doctor)
if (!options.skipDoctor) {
const stackName = getStackName(appDefinition, options);
if (stackName) {
await runPostDeploymentHealthCheck(stackName, options);
} else {
console.log('\n⚠️ Could not determine stack name - skipping health check');
console.log(' Run "frigg doctor <stack-name>" manually to check stack health');
}
} else {
console.log('\n⏭️ Skipping post-deployment health check (--skip-doctor)');
}
}
module.exports = { deployCommand };