jaxon-optimizely-dxp-mcp
Version:
AI-powered automation for Optimizely DXP - deploy, monitor, and manage environments through natural conversations
421 lines (353 loc) • 16.6 kB
JavaScript
/**
* Package Tools Module
* Handles package upload and management operations
* Part of Jaxon Digital Optimizely DXP MCP Server
*/
const { PowerShellHelper, ResponseBuilder, ErrorHandler, Config } = require('../index');
const UploadProgress = require('../upload-progress');
const fs = require('fs');
const path = require('path');
class PackageTools {
/**
* Upload deployment package
*/
static async handleUploadDeploymentPackage(args) {
if (!args.apiKey || !args.apiSecret || !args.projectId || !args.packagePath) {
return ResponseBuilder.invalidParams('Missing required parameters');
}
try {
const result = await this.uploadDeploymentPackage(args);
return ResponseBuilder.success(result);
} catch (error) {
console.error('Upload package error:', error);
return ResponseBuilder.internalError('Failed to upload package', error.message);
}
}
static async uploadDeploymentPackage(args) {
const { apiKey, apiSecret, projectId, packagePath, chunkSize } = args;
console.error(`Uploading deployment package: ${packagePath}`);
// Check file size for progress tracking
let fileSize = 0;
let tracker = null;
try {
const stats = await fs.promises.stat(packagePath);
fileSize = stats.size;
// Show progress for files larger than 10MB
if (fileSize > 10 * 1024 * 1024) {
console.error(`📦 Package size: ${(fileSize / (1024 * 1024)).toFixed(2)} MB`);
tracker = UploadProgress.createTracker(packagePath);
// For very large files, show a warning
if (fileSize > 100 * 1024 * 1024) {
console.error(`⚠️ Large file detected! Upload may take several minutes.`);
console.error(` Consider using generate_sas_upload_url for files >100MB`);
}
}
} catch (error) {
console.error(`Could not determine file size: ${error.message}`);
}
// Build command with progress support
let command = `Add-EpiDeploymentPackage -ProjectId '${projectId}' -Path '${packagePath}'`;
if (chunkSize) command += ` -ChunkSize ${chunkSize}`;
// Add verbose flag for progress tracking if file is large
if (fileSize > 10 * 1024 * 1024) {
command += ' -Verbose';
}
// Execute with progress monitoring
const executeWithProgress = async () => {
const result = await PowerShellHelper.executeEpiCommandStreaming(
command,
{ apiKey, apiSecret, projectId },
{
parseJson: true,
timeout: Math.max(300000, fileSize / 1000), // Dynamic timeout based on file size
onProgress: tracker ? (data) => {
// Parse progress from verbose output
const progressMatch = data.match(/(\d+)%/);
if (progressMatch) {
const percentage = parseInt(progressMatch[1]);
tracker.setProgress((percentage / 100) * fileSize);
}
// Also look for byte progress
const bytesMatch = data.match(/(\d+)\s*bytes/i);
if (bytesMatch) {
const bytes = parseInt(bytesMatch[1]);
tracker.setProgress(bytes);
}
} : undefined
}
);
// Mark upload complete
if (tracker) {
tracker.complete();
}
return result;
};
let result;
try {
result = await executeWithProgress();
} catch (error) {
if (tracker) {
tracker.fail(error);
}
throw error;
}
// Check for errors
if (result.stderr) {
const error = ErrorHandler.detectError(result.stderr, {
operation: 'Upload Package',
projectId,
packagePath
});
if (error) {
return ErrorHandler.formatError(error);
}
}
// Format response with upload statistics
if (result.parsedData) {
const uploadStats = tracker ? tracker.getStatus() : { fileSize };
return this.formatUploadResponse(result.parsedData, packagePath, uploadStats);
}
// Check for success patterns in stdout
if (result.stdout) {
const uploadMatch = result.stdout.match(/Uploaded.*to\s+([^\s]+)/i);
if (uploadMatch) {
return this.formatUploadSuccess(uploadMatch[1], packagePath);
}
}
return ResponseBuilder.addFooter('Package upload initiated');
}
static formatUploadResponse(data, packagePath, uploadStats = {}) {
const { FORMATTING: { STATUS_ICONS } } = Config;
let response = `${STATUS_ICONS.SUCCESS} **Package Upload Completed**\n\n`;
response += `**Package:** ${packagePath}\n`;
if (data.location) {
response += `**Location:** ${data.location}\n`;
}
if (data.size) {
response += `**Size:** ${data.size}\n`;
} else if (uploadStats.fileSize) {
const sizeMB = (uploadStats.fileSize / (1024 * 1024)).toFixed(2);
response += `**Size:** ${sizeMB} MB\n`;
}
if (data.uploadTime) {
response += `**Upload Time:** ${data.uploadTime} seconds\n`;
} else if (uploadStats.duration) {
response += `**Upload Time:** ${uploadStats.duration.toFixed(1)} seconds\n`;
}
if (uploadStats.averageSpeed) {
const speedMB = (uploadStats.averageSpeed / (1024 * 1024)).toFixed(2);
response += `**Average Speed:** ${speedMB} MB/s\n`;
}
const tips = [
'Package is now ready for deployment',
'Use start_deployment to deploy this package',
'Package location can be used in deployment commands'
];
response += '\n' + ResponseBuilder.formatTips(tips);
return ResponseBuilder.addFooter(response);
}
static formatUploadSuccess(location, packagePath) {
const { FORMATTING: { STATUS_ICONS } } = Config;
let response = `${STATUS_ICONS.SUCCESS} **Package Upload Successful**\n\n`;
response += `**Package:** ${packagePath}\n`;
response += `**Location:** \`${location}\`\n\n`;
const tips = [
'Package has been uploaded to DXP storage',
'Save the location for deployment operations',
'Use this location with start_deployment'
];
response += ResponseBuilder.formatTips(tips);
return ResponseBuilder.addFooter(response);
}
/**
* Deploy package and start (combined workflow)
*/
static async handleDeployPackageAndStart(args) {
if (!args.apiKey || !args.apiSecret || !args.projectId ||
!args.packagePath || !args.targetEnvironment) {
return ResponseBuilder.invalidParams('Missing required parameters');
}
try {
// First upload the package
const uploadResult = await this.uploadDeploymentPackage({
apiKey: args.apiKey,
apiSecret: args.apiSecret,
projectId: args.projectId,
packagePath: args.packagePath
});
// Extract package location from upload result
let packageLocation = null;
if (uploadResult.includes('Location:')) {
const match = uploadResult.match(/Location:\s*`?([^`\n]+)`?/i);
if (match) {
packageLocation = match[1].trim();
}
}
if (!packageLocation) {
return ResponseBuilder.internalError('Failed to get package location after upload');
}
// Now start deployment with the uploaded package
const DeploymentTools = require('./deployment-tools');
const deployResult = await DeploymentTools.startDeployment({
apiKey: args.apiKey,
apiSecret: args.apiSecret,
projectId: args.projectId,
targetEnvironment: args.targetEnvironment,
packages: [packageLocation],
useMaintenancePage: args.useMaintenancePage,
directDeploy: args.directDeploy,
zeroDowntimeMode: args.zeroDowntimeMode,
warmUpUrl: args.warmUpUrl,
waitForCompletion: args.waitForCompletion,
waitTimeoutMinutes: args.waitTimeoutMinutes
});
// Combine results
const combinedResult = this.formatCombinedWorkflow(uploadResult, deployResult, packageLocation);
return ResponseBuilder.success(combinedResult);
} catch (error) {
console.error('Deploy package and start error:', error);
return ResponseBuilder.internalError('Failed to deploy package', error.message);
}
}
static formatCombinedWorkflow(uploadResult, deployResult, packageLocation) {
const { FORMATTING: { STATUS_ICONS } } = Config;
let response = `${STATUS_ICONS.ROCKET} **Package Deployment Workflow Completed**\n\n`;
response += `${STATUS_ICONS.SUCCESS} **Step 1: Package Upload**\n`;
response += `Package Location: \`${packageLocation}\`\n\n`;
response += `${STATUS_ICONS.SUCCESS} **Step 2: Deployment Started**\n`;
response += deployResult;
return response;
}
/**
* Deploy package from Azure DevOps artifact URL
*/
static async handleDeployAzureArtifact(args) {
if (!args.apiKey || !args.apiSecret || !args.projectId ||
!args.artifactUrl || !args.targetEnvironment) {
return ResponseBuilder.invalidParams('Missing required parameters: apiKey, apiSecret, projectId, artifactUrl, targetEnvironment');
}
const {
artifactUrl,
azureDevOpsPat,
artifactName = 'drop',
cleanupAfterDeploy = true,
...deploymentArgs
} = args;
// Get PAT from args or environment
const pat = azureDevOpsPat || process.env.AZURE_DEVOPS_PAT;
if (!pat) {
return ResponseBuilder.invalidParams('Azure DevOps Personal Access Token is required. Provide azureDevOpsPat parameter or set AZURE_DEVOPS_PAT environment variable.');
}
let downloadedArtifactPath = null;
try {
console.error('🔗 Starting Azure DevOps artifact deployment...');
// Step 1: Download the artifact from Azure DevOps
const AzureDevOpsTools = require('./azure-devops-tools');
console.error('📥 Step 1: Downloading artifact from Azure DevOps');
const downloadResult = await AzureDevOpsTools.downloadArtifact(artifactUrl, {
pat,
artifactName,
timeout: 600000 // 10 minutes for large artifacts
});
if (!downloadResult.success) {
return ResponseBuilder.internalError('Failed to download Azure DevOps artifact');
}
downloadedArtifactPath = downloadResult.downloadPath;
console.error(`✅ Artifact downloaded: ${downloadResult.sizeMB} MB`);
// Step 2: Upload to DXP
console.error('📤 Step 2: Uploading to Optimizely DXP');
const uploadResult = await this.uploadDeploymentPackage({
apiKey: deploymentArgs.apiKey,
apiSecret: deploymentArgs.apiSecret,
projectId: deploymentArgs.projectId,
packagePath: downloadedArtifactPath
});
// Extract package location from upload result
let packageLocation = null;
if (uploadResult.includes('Location:')) {
const match = uploadResult.match(/Location:\s*`?([^`\n]+)`?/i);
if (match) {
packageLocation = match[1].trim();
}
}
if (!packageLocation) {
return ResponseBuilder.internalError('Failed to get package location after upload');
}
console.error('🚀 Step 3: Starting deployment');
// Step 3: Start deployment
const DeploymentTools = require('./deployment-tools');
const deployResult = await DeploymentTools.startDeployment({
...deploymentArgs,
packages: [packageLocation]
});
// Format the combined result
const combinedResult = this.formatAzureArtifactWorkflow(
artifactUrl,
downloadResult,
uploadResult,
deployResult,
packageLocation
);
// Clean up downloaded artifact if requested
if (cleanupAfterDeploy && downloadedArtifactPath) {
try {
await AzureDevOpsTools.cleanupArtifact(downloadedArtifactPath);
} catch (cleanupError) {
console.error(`⚠️ Cleanup warning: ${cleanupError.message}`);
}
}
return ResponseBuilder.success(combinedResult);
} catch (error) {
console.error('Azure artifact deployment error:', error);
// Clean up on error
if (downloadedArtifactPath && cleanupAfterDeploy) {
try {
const AzureDevOpsTools = require('./azure-devops-tools');
await AzureDevOpsTools.cleanupArtifact(downloadedArtifactPath);
} catch (cleanupError) {
console.error(`Cleanup error: ${cleanupError.message}`);
}
}
return ResponseBuilder.internalError('Failed to deploy Azure DevOps artifact', error.message);
}
}
static formatAzureArtifactWorkflow(artifactUrl, downloadResult, uploadResult, deployResult, packageLocation) {
const { FORMATTING: { STATUS_ICONS } } = Config;
let response = `${STATUS_ICONS.ROCKET} **Azure DevOps Artifact Deployment Completed**\n\n`;
// Step 1: Download from Azure DevOps
response += `${STATUS_ICONS.SUCCESS} **Step 1: Azure DevOps Download**\n`;
response += `Artifact: ${downloadResult.artifactName}\n`;
response += `Size: ${downloadResult.sizeMB} MB\n`;
response += `Source: Azure DevOps Build Pipeline\n\n`;
// Step 2: Upload to DXP
response += `${STATUS_ICONS.SUCCESS} **Step 2: DXP Package Upload**\n`;
response += `Package Location: \`${packageLocation}\`\n\n`;
// Step 3: Deployment
response += `${STATUS_ICONS.SUCCESS} **Step 3: Deployment Started**\n`;
response += deployResult;
const tips = [
'Artifact was automatically downloaded from Azure DevOps',
'Package is now deployed using standard DXP deployment process',
'Original artifact file was cleaned up to save disk space'
];
response += '\n' + ResponseBuilder.formatTips(tips);
return ResponseBuilder.addFooter(response);
}
/**
* Enhanced deploy package that supports both local files and Azure DevOps artifacts
*/
static async handleDeployPackageEnhanced(args) {
const { packagePath, artifactUrl } = args;
// Determine deployment type
if (artifactUrl) {
// Azure DevOps artifact deployment
return await this.handleDeployAzureArtifact(args);
} else if (packagePath) {
// Traditional local file deployment
return await this.handleDeployPackageAndStart(args);
} else {
return ResponseBuilder.invalidParams('Either packagePath or artifactUrl must be provided');
}
}
}
module.exports = PackageTools;