jaxon-optimizely-dxp-mcp
Version:
AI-powered automation for Optimizely DXP - deploy, monitor, and manage environments through natural conversations
442 lines (374 loc) • 16.4 kB
JavaScript
/**
* Deployment Action Operations
* Handles start, complete, and reset operations for deployments
* Part of Jaxon Digital Optimizely DXP MCP Server
*/
const { PowerShellHelper, ResponseBuilder, ErrorHandler, Config } = require('../../index');
const PowerShellCommandBuilder = require('../../powershell-command-builder');
const DeploymentFormatters = require('./deployment-formatters');
const DeploymentValidator = require('../../deployment-validator');
const { getGlobalMonitor } = require('../../deployment-monitor');
class DeploymentActionOperations {
/**
* Start a new deployment
*/
static async handleStartDeployment(args) {
if (!args.apiKey || !args.apiSecret || !args.projectId) {
return ResponseBuilder.invalidParams('Missing required parameters');
}
try {
const result = await this.startDeployment(args);
return ResponseBuilder.success(result);
} catch (error) {
console.error('Start deployment error:', error);
return ResponseBuilder.internalError('Failed to start deployment', error.message);
}
}
static async startDeployment(args) {
const {
apiKey, apiSecret, projectId,
sourceEnvironment, targetEnvironment,
deploymentType, sourceApps,
includeBlob, includeDatabase,
directDeploy, useMaintenancePage
} = args;
console.error(`Starting deployment from ${sourceEnvironment} to ${targetEnvironment} for project ${projectId}`);
// Validate deployment path
const pathValidation = DeploymentValidator.validateDeploymentPath(sourceEnvironment, targetEnvironment);
if (!pathValidation.valid) {
return ResponseBuilder.error(
`❌ Invalid Deployment Path\n\n${pathValidation.error}\n\n💡 ${pathValidation.suggestion}`
);
}
// Show warnings if any
if (pathValidation.warnings && pathValidation.warnings.length > 0) {
let warningMsg = '⚠️ **Deployment Warnings:**\n\n';
pathValidation.warnings.forEach(warn => {
warningMsg += `${warn.message}\n`;
if (warn.suggestion) {
warningMsg += ` 💡 ${warn.suggestion}\n`;
}
warningMsg += '\n';
});
console.error(warningMsg);
}
// Validate deployment parameters
const paramValidation = DeploymentValidator.validateDeploymentParams(args);
if (!paramValidation.valid) {
return ResponseBuilder.error(
`❌ Invalid Parameters\n\n${paramValidation.errors.join('\n')}`
);
}
// Use sanitized parameters
const sanitizedArgs = paramValidation.sanitized;
// Check deployment timing
const timingCheck = DeploymentValidator.validateDeploymentTiming({
targetEnvironment
});
if (timingCheck.warnings && timingCheck.warnings.length > 0) {
timingCheck.warnings.forEach(warn => {
console.error(`Timing warning: ${warn.message}`);
});
}
// Determine if this is upward (code) or downward (content) deployment
const isUpward = pathValidation.isUpward;
// Apply smart defaults based on deployment direction
let deployCode = false;
let deployContent = false;
if (sanitizedArgs.deploymentType) {
// User specified deployment type
if (sanitizedArgs.deploymentType === 'code') {
deployCode = true;
} else if (sanitizedArgs.deploymentType === 'content') {
deployContent = true;
} else if (sanitizedArgs.deploymentType === 'all') {
deployCode = true;
deployContent = true;
}
} else {
// Apply smart defaults
if (isUpward) {
deployCode = true; // Code flows up
console.error('Defaulting to CODE deployment (upward flow)');
} else {
deployContent = true; // Content flows down
console.error('Defaulting to CONTENT deployment (downward flow)');
}
}
// Build command using the new builder
const builder = PowerShellCommandBuilder.create('Start-EpiDeployment')
.addParam('ProjectId', projectId)
.addParam('SourceEnvironment', sourceEnvironment)
.addParam('TargetEnvironment', targetEnvironment);
// Add deployment type parameters
if (deployCode) {
// SourceApp is required for code deployments
const appsToUse = sourceApps && sourceApps.length > 0
? sourceApps
: ['cms']; // Default to CMS app
builder.addArray('SourceApp', appsToUse);
console.error(`Deploying code with apps: ${appsToUse.join(', ')}`);
}
if (deployContent) {
// Add content deployment flags
builder.addSwitchIf(includeBlob !== false, 'IncludeBlob')
.addSwitchIf(includeDatabase !== false, 'IncludeDatabase');
console.error(`Deploying content with IncludeBlob=${includeBlob !== false}, IncludeDatabase=${includeDatabase !== false}`);
}
// Add optional parameters
builder.addSwitchIf(directDeploy === true, 'DirectDeploy')
.addSwitchIf(useMaintenancePage === true, 'UseMaintenancePage');
const command = builder.build();
console.error(`Executing command: ${command}`);
// Execute with retry logic - deployments are critical operations
const result = await PowerShellHelper.executeWithRetry(
command,
{ apiKey, apiSecret, projectId },
{
parseJson: true,
operation: 'start_deployment',
cacheInvalidate: true
},
{
maxAttempts: 3,
initialDelay: 2000, // Start with 2 second delay
verbose: true
}
);
// Check for errors
if (result.stderr) {
console.error('PowerShell stderr:', result.stderr);
const error = ErrorHandler.detectError(result.stderr, {
operation: 'Start Deployment',
sourceEnvironment,
targetEnvironment,
projectId
});
if (error) {
return ErrorHandler.formatError(error);
}
}
// Format response
if (result.parsedData) {
const formattedResult = DeploymentFormatters.formatDeploymentStarted(result.parsedData, args);
// Extract deployment ID from parsed data and start monitoring
if (result.parsedData.id) {
try {
const monitor = getGlobalMonitor();
const monitorId = monitor.startMonitoring({
deploymentId: result.parsedData.id,
projectId: args.projectId,
apiKey: args.apiKey,
apiSecret: args.apiSecret,
interval: 60 * 1000 // 1 minute default
});
console.error(`Started monitoring deployment ${result.parsedData.id} (Monitor ID: ${monitorId})`);
} catch (monitorError) {
console.error(`Failed to start monitoring: ${monitorError.message}`);
// Don't fail the deployment if monitoring fails
}
}
return formattedResult;
}
return ResponseBuilder.addFooter('Deployment started but no details available');
}
/**
* Complete a deployment in verification state
*/
static async handleCompleteDeployment(args) {
if (!args.apiKey || !args.apiSecret || !args.projectId || !args.deploymentId) {
return ResponseBuilder.invalidParams('Missing required parameters');
}
try {
const result = await this.completeDeployment(args);
return ResponseBuilder.success(result);
} catch (error) {
console.error('Complete deployment error:', error);
return ResponseBuilder.internalError('Failed to complete deployment', error.message);
}
}
static async completeDeployment(args) {
const { apiKey, apiSecret, projectId, deploymentId } = args;
console.error(`Completing deployment ${deploymentId} for project ${projectId}`);
// Build command using the new builder
const command = PowerShellCommandBuilder.create('Complete-EpiDeployment')
.addParam('ProjectId', projectId)
.addParam('Id', deploymentId)
.build();
// Execute with retry logic
const result = await PowerShellHelper.executeWithRetry(
command,
{ apiKey, apiSecret, projectId },
{
parseJson: true,
operation: 'complete_deployment',
cacheInvalidate: true
},
{
maxAttempts: 3,
verbose: true
}
);
// Check for errors
if (result.stderr) {
const error = ErrorHandler.detectError(result.stderr, {
operation: 'Complete Deployment',
deploymentId,
projectId
});
if (error) {
return ErrorHandler.formatError(error);
}
}
// Format response
if (result.parsedData) {
return DeploymentFormatters.formatDeploymentCompleted(result.parsedData);
}
return ResponseBuilder.addFooter('Deployment completed successfully');
}
/**
* Reset/rollback a deployment
*/
static async handleResetDeployment(args) {
if (!args.apiKey || !args.apiSecret || !args.projectId || !args.deploymentId) {
return ResponseBuilder.invalidParams('Missing required parameters');
}
try {
const result = await this.resetDeployment(args);
return ResponseBuilder.success(result);
} catch (error) {
console.error('Reset deployment error:', error);
return ResponseBuilder.internalError('Failed to reset deployment', error.message);
}
}
static async resetDeployment(args) {
const { apiKey, apiSecret, projectId, deploymentId, projectName } = args;
console.error(`Resetting deployment ${deploymentId} for project ${projectId}`);
// First, get deployment details to determine if DB rollback is needed
const statusCommand = PowerShellCommandBuilder.create('Get-EpiDeployment')
.addParam('ProjectId', projectId)
.addParam('Id', deploymentId)
.build();
const statusResult = await PowerShellHelper.executeEpiCommand(
statusCommand,
{ apiKey, apiSecret, projectId },
{ parseJson: true }
);
let includeDbRollback = false;
let deploymentData = null;
if (statusResult.parsedData) {
// Check if this deployment included database changes
deploymentData = statusResult.parsedData;
includeDbRollback = deploymentData.includeDatabase === true;
}
// Build reset command using the new builder
const command = PowerShellCommandBuilder.create('Reset-EpiDeployment')
.addParam('ProjectId', projectId)
.addParam('Id', deploymentId)
.build();
// Execute with cache invalidation
const result = await PowerShellHelper.executeEpiCommandWithInvalidation(
command,
{ apiKey, apiSecret, projectId },
{
parseJson: true,
operation: 'reset_deployment'
}
);
// Check for errors
if (result.stderr) {
const error = ErrorHandler.detectError(result.stderr, {
operation: 'Reset Deployment',
deploymentId,
projectId
});
if (error) {
return ErrorHandler.formatError(error);
}
}
// Merge deployment data if available
const resetData = result.parsedData || {};
if (deploymentData && deploymentData.parameters) {
resetData.parameters = deploymentData.parameters;
}
// Start monitoring the reset in the background
this.monitorResetProgress(deploymentId, projectId, apiKey, apiSecret, projectName);
// Format response
return DeploymentFormatters.formatDeploymentReset(resetData, includeDbRollback, projectName);
}
/**
* Monitor reset progress in the background
*/
static async monitorResetProgress(deploymentId, projectId, apiKey, apiSecret, projectName) {
const checkInterval = 30000; // Check every 30 seconds
const maxChecks = 20; // Maximum 10 minutes
let checkCount = 0;
const checkStatus = async () => {
checkCount++;
try {
const command = PowerShellCommandBuilder.create('Get-EpiDeployment')
.addParam('ProjectId', projectId)
.addParam('Id', deploymentId)
.build();
const result = await PowerShellHelper.executeEpiCommand(
command,
{ apiKey, apiSecret, projectId },
{ parseJson: true }
);
if (result.parsedData) {
const status = result.parsedData.status;
// Check if reset is complete
if (status === 'Reset' || status === 'Completed' || status === 'Failed') {
const message = this.formatResetCompleteMessage(
deploymentId,
status,
result.parsedData,
projectName
);
console.error('\n' + message);
return; // Stop monitoring
}
}
} catch (error) {
console.error('Error checking reset status:', error.message);
}
// Continue checking if not at max
if (checkCount < maxChecks) {
setTimeout(checkStatus, checkInterval);
} else {
console.error(`\n⚠️ Reset monitoring timed out for deployment ${deploymentId}. Please check status manually.`);
}
};
// Start checking after initial delay
setTimeout(checkStatus, checkInterval);
}
/**
* Format reset completion message
*/
static formatResetCompleteMessage(deploymentId, status, deployment, projectName) {
const { FORMATTING: { STATUS_ICONS } } = Config;
let message = '\n' + '='.repeat(60) + '\n';
if (status === 'Reset' || status === 'Completed') {
message += `${STATUS_ICONS.SUCCESS} **Reset Complete`;
} else {
message += `${STATUS_ICONS.ERROR} **Reset Failed`;
}
if (projectName) {
message += ` - ${projectName}**\n\n`;
} else {
message += '**\n\n';
}
message += `**Deployment ID**: ${deploymentId}\n`;
message += `**Final Status**: ${status}\n`;
if (status === 'Reset' || status === 'Completed') {
message += '\n✅ The deployment has been successfully rolled back.\n';
message += 'The environment has been restored to its previous state.\n';
} else {
message += '\n❌ The reset operation failed.\n';
message += 'Please check the deployment logs for more information.\n';
}
message += '\n' + '='.repeat(60);
return message;
}
}
module.exports = DeploymentActionOperations;