/**
* Deployment Formatters
* Handles formatting of deployment responses
* Part of Jaxon Digital Optimizely DXP MCP Server
*/
const { Config, ResponseBuilder } = require('../../index');
class DeploymentFormatters {
/**
* Format date/time in user's local timezone with timezone name
*/
static formatLocalDateTime(dateInput) {
if (!dateInput) return 'N/A';
const date = new Date(dateInput);
if (isNaN(date.getTime())) return 'Invalid date';
// Get the user's timezone
const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
// Format date and time portions separately
const dateOptions = {
year: 'numeric',
month: 'short',
day: 'numeric'
};
const timeOptions = {
hour: '2-digit',
minute: '2-digit',
timeZoneName: 'short'
};
const datePart = date.toLocaleDateString('en-US', dateOptions);
const timePart = date.toLocaleTimeString('en-US', timeOptions);
// Format as: Aug 6, 2025 (12:26 PM CDT)
return `${datePart} (${timePart})`;
}
/**
* Format duration between two dates
*/
static formatDuration(startDate, endDate) {
const start = new Date(startDate);
const end = new Date(endDate);
const duration = end - start;
if (duration < 0) return 'Invalid duration';
const minutes = Math.floor(duration / 60000);
const seconds = Math.floor((duration % 60000) / 1000);
if (minutes > 60) {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
return `${hours}h ${mins}m`;
}
return minutes > 0 ? `${minutes}m ${seconds}s` : `${seconds}s`;
}
/**
* Format a list of deployments
*/
static formatDeploymentList(deployments, projectId = null, limit = null, projectName = null) {
const { FORMATTING: { STATUS_ICONS } } = Config;
// Get project info from configured projects if not provided
if (!projectId || !projectName) {
try {
const ProjectTools = require('../project-tools');
const projects = ProjectTools.getConfiguredProjects();
if (projects && projects.length > 0) {
const defaultProject = projects.find(p => p.isDefault) || projects[0];
projectId = projectId || defaultProject.id;
projectName = projectName || defaultProject.name;
}
} catch (error) {
// Fall back to environment variables if ProjectTools fails
projectName = projectName || process.env.OPTIMIZELY_PROJECT_NAME;
projectId = projectId || process.env.OPTIMIZELY_PROJECT_ID;
}
}
// DXP-66: Prepare structured data for automation tools
const structuredData = {
projectId: projectId,
projectName: projectName,
deployments: [],
totalCount: 0,
displayLimit: limit || 5
};
let response = `${STATUS_ICONS.DEPLOY} **Recent Deployments`;
if (projectName) {
response += ` - ${projectName}**\n\n`;
} else if (projectId) {
response += ` - ${projectId}**\n\n`;
} else {
response += `**\n\n`;
}
if (Array.isArray(deployments) && deployments.length > 0) {
// Sort by start time (most recent first)
const sorted = deployments.sort((a, b) => {
const dateA = new Date(a.startTime || a.created || 0);
const dateB = new Date(b.startTime || b.created || 0);
return dateB - dateA;
});
// Apply limit if specified, otherwise show up to 5 for performance
const displayLimit = limit || 5;
const recentDeployments = sorted.slice(0, displayLimit);
// DXP-66: Build structured data array for automation tools
structuredData.totalCount = deployments.length;
recentDeployments.forEach((deployment, index) => {
const status = deployment.status || 'Unknown';
let statusIcon = STATUS_ICONS.IN_PROGRESS;
if (status.toLowerCase().includes('success') || status.toLowerCase().includes('completed')) {
statusIcon = STATUS_ICONS.SUCCESS;
} else if (status.toLowerCase().includes('fail')) {
statusIcon = STATUS_ICONS.ERROR;
} else if (status.toLowerCase().includes('reset')) {
statusIcon = STATUS_ICONS.WARNING;
} else if (status.toLowerCase().includes('verification')) {
statusIcon = STATUS_ICONS.VERIFICATION;
}
// DXP-66: Add deployment to structured data
const sourceEnv = deployment.startEnvironment || deployment.parameters?.sourceEnvironment || null;
const targetEnv = deployment.endEnvironment || deployment.parameters?.targetEnvironment || 'Unknown';
structuredData.deployments.push({
deploymentId: deployment.id,
status: status,
sourceEnvironment: sourceEnv,
targetEnvironment: targetEnv,
startTime: deployment.startTime || null,
endTime: deployment.endTime || null,
percentComplete: deployment.percentComplete || 0
});
response += `${statusIcon} **Deployment #${deployment.id}**\n`;
// Get package upload info
const isPackageUpload = deployment.parameters?.packages && deployment.parameters?.packages.length > 0;
// Format deployment path - handle package uploads differently
if (isPackageUpload && !sourceEnv) {
const packageName = deployment.parameters.packages[0];
response += `• Package: ${packageName} → ${targetEnv}\n`;
} else if (!sourceEnv || sourceEnv === 'Unknown') {
response += `• To: ${targetEnv} (Package Upload)\n`;
} else {
response += `• From: ${sourceEnv} → To: ${targetEnv}\n`;
}
response += `• Status: **${status}**`;
// Add progress for in-progress deployments
if (deployment.percentComplete !== undefined && deployment.percentComplete < 100) {
response += ` (${deployment.percentComplete}%)`;
}
response += '\n';
if (deployment.startTime) {
response += `• Started: ${this.formatLocalDateTime(deployment.startTime)}`;
// Add duration for completed deployments
if (deployment.endTime) {
const duration = this.formatDuration(deployment.startTime, deployment.endTime);
response += ` (Duration: ${duration})`;
}
response += '\n';
}
// Show deployment type if available and limit is small
if (displayLimit <= 5 && deployment.parameters) {
if (deployment.parameters.sourceApps && deployment.parameters.sourceApps.length > 0) {
response += `• Apps: ${deployment.parameters.sourceApps.join(', ')}`;
// Add flags for blob/db
const flags = [];
if (deployment.parameters.includeBlob) flags.push('Blob');
if (deployment.parameters.includeDb) flags.push('DB');
if (flags.length > 0) {
response += ` (+${flags.join(', ')})`;
}
response += '\n';
}
}
// Show errors/warnings if present
if (deployment.deploymentErrors && deployment.deploymentErrors.length > 0) {
response += `• **❌ ${deployment.deploymentErrors.length} Error(s)**\n`;
}
if (deployment.deploymentWarnings && deployment.deploymentWarnings.length > 0) {
response += `• **⚠️ ${deployment.deploymentWarnings.length} Warning(s)**\n`;
}
// Show preview URL for deployments awaiting verification
if (status.toLowerCase().includes('verification')) {
// DXP-87: Show validation links from API
if (deployment.validationLinks && deployment.validationLinks.length > 0) {
deployment.validationLinks.forEach(link => {
response += `• **🔗 Verification**: ${link}\n`;
});
} else {
const previewUrl = this.getPreviewUrl(targetEnv, projectId);
if (previewUrl) {
if (targetEnv === 'Production') {
response += `• **Preview URL (Slot)**: ${previewUrl}\n`;
} else {
response += `• **Preview URL**: ${previewUrl}\n`;
}
} else if (targetEnv === 'Production') {
response += `• **Verification URL**: Check DXP portal for slot URL\n`;
}
}
}
response += '\n';
});
if (deployments.length > displayLimit) {
response += `_Showing ${displayLimit} most recent deployments out of ${deployments.length} total_\n`;
}
} else {
response += 'No deployments found.\n';
}
// DXP-66: Return both structured data and formatted message
response = ResponseBuilder.addFooter(response);
return { data: structuredData, message: response };
}
/**
* Format a single deployment
*/
static formatSingleDeployment(deployment, projectName = null) {
const { FORMATTING: { STATUS_ICONS } } = Config;
// Get project info from configured projects if not provided
let projectId = null;
if (!projectName) {
try {
const ProjectTools = require('../project-tools');
const projects = ProjectTools.getConfiguredProjects();
if (projects && projects.length > 0) {
const defaultProject = projects.find(p => p.isDefault) || projects[0];
projectId = defaultProject.id;
projectName = projectName || defaultProject.name;
}
} catch (error) {
// Fall back to environment variables if ProjectTools fails
projectId = process.env.OPTIMIZELY_PROJECT_ID;
projectName = projectName || process.env.OPTIMIZELY_PROJECT_NAME;
}
}
const status = deployment.status || 'Unknown';
const sourceEnv = deployment.startEnvironment || deployment.parameters?.sourceEnvironment || null;
const targetEnv = deployment.endEnvironment || deployment.parameters?.targetEnvironment || 'Unknown';
const isPackageUpload = deployment.parameters?.packages && deployment.parameters?.packages.length > 0;
const previewUrl = status.toLowerCase().includes('verification') ? this.getPreviewUrl(targetEnv, projectId) : null;
// Build structured data for automation tools
const structuredData = {
deploymentId: deployment.id,
status: status,
sourceEnvironment: sourceEnv,
targetEnvironment: targetEnv,
percentComplete: deployment.percentComplete,
startTime: deployment.startTime,
endTime: deployment.endTime,
isPackageUpload: isPackageUpload,
previewUrl: previewUrl,
parameters: deployment.parameters,
warnings: deployment.deploymentWarnings || [],
errors: deployment.deploymentErrors || [],
validationLinks: deployment.validationLinks || []
};
// Build human-readable message
let statusIcon = STATUS_ICONS.IN_PROGRESS;
if (status.toLowerCase().includes('success') || status.toLowerCase().includes('completed')) {
statusIcon = STATUS_ICONS.SUCCESS;
} else if (status.toLowerCase().includes('fail')) {
statusIcon = STATUS_ICONS.ERROR;
} else if (status.toLowerCase().includes('reset')) {
statusIcon = STATUS_ICONS.WARNING;
} else if (status.toLowerCase().includes('verification')) {
statusIcon = STATUS_ICONS.VERIFICATION;
}
let message = `${STATUS_ICONS.DEPLOY} **Deployment Details`;
if (projectName) {
message += ` - ${projectName}**\n\n`;
} else {
message += `**\n\n`;
}
message += `${statusIcon} **Deployment #${deployment.id}**\n\n`;
message += `**Status**: ${status}\n`;
if (isPackageUpload && !sourceEnv) {
message += `**Package**: ${deployment.parameters.packages[0]}\n`;
message += `**To**: ${targetEnv}\n`;
message += `**Type**: Package Upload\n`;
} else if (!sourceEnv || sourceEnv === 'Unknown') {
message += `**To**: ${targetEnv}\n`;
message += `**Type**: Package Upload\n`;
} else {
message += `**From**: ${sourceEnv}\n`;
message += `**To**: ${targetEnv}\n`;
}
// Show progress if available
if (deployment.percentComplete !== undefined) {
message += `**Progress**: ${deployment.percentComplete}%\n`;
}
// Always show preview URL for deployments awaiting verification
if (status.toLowerCase().includes('verification')) {
// DXP-87: Show validation links from API
if (deployment.validationLinks && deployment.validationLinks.length > 0) {
message += `\n**🔗 Verification URLs**:\n`;
deployment.validationLinks.forEach(link => {
message += `• ${link}\n`;
});
message += `_Review your changes at the verification URL(s) above before completing_\n`;
} else if (previewUrl) {
if (targetEnv === 'Production') {
message += `\n**🔗 Preview URL (Slot)**: ${previewUrl}\n`;
message += `_Review your changes in the deployment slot before completing_\n`;
} else {
message += `\n**🔗 Preview URL**: ${previewUrl}\n`;
message += `_Review your changes at the preview URL above_\n`;
}
} else if (targetEnv === 'Production') {
message += `\n**🔗 Verification URL**: Check DXP portal for slot URL\n`;
}
}
// Timing information
message += '\n**📅 Timeline**:\n';
if (deployment.startTime) {
message += `• Started: ${this.formatLocalDateTime(deployment.startTime)}\n`;
}
if (deployment.endTime) {
message += `• Ended: ${this.formatLocalDateTime(deployment.endTime)}\n`;
// Calculate duration if both times are available
if (deployment.startTime) {
const duration = this.formatDuration(deployment.startTime, deployment.endTime);
message += `• Duration: ${duration}\n`;
}
}
// Deployment configuration details
if (deployment.parameters) {
message += '\n**⚙️ Configuration**:\n';
if (deployment.parameters.sourceApps && deployment.parameters.sourceApps.length > 0) {
message += `• Apps: ${deployment.parameters.sourceApps.join(', ')}\n`;
}
if (deployment.parameters.includeBlob !== undefined) {
message += `• Include Blob: ${deployment.parameters.includeBlob ? 'Yes' : 'No'}\n`;
}
if (deployment.parameters.includeDb !== undefined) {
message += `• Include Database: ${deployment.parameters.includeDb ? 'Yes' : 'No'}\n`;
}
if (deployment.parameters.maintenancePage !== undefined) {
message += `• Maintenance Page: ${deployment.parameters.maintenancePage ? 'Enabled' : 'Disabled'}\n`;
}
if (deployment.parameters.zeroDowntimeMode && deployment.parameters.zeroDowntimeMode !== 'NotApplicable') {
message += `• Zero Downtime Mode: ${deployment.parameters.zeroDowntimeMode}\n`;
}
// Reset parameters if this was a rollback
if (deployment.parameters.resetParameters) {
message += '\n**🔄 Reset Configuration**:\n';
const reset = deployment.parameters.resetParameters;
if (reset.resetWithDbRollback !== undefined) {
message += `• Database Rollback: ${reset.resetWithDbRollback ? 'Yes' : 'No'}\n`;
}
if (reset.validateBeforeSwap !== undefined) {
message += `• Validate Before Swap: ${reset.validateBeforeSwap ? 'Yes' : 'No'}\n`;
}
if (reset.complete !== undefined) {
message += `• Auto-Complete: ${reset.complete ? 'Yes' : 'No'}\n`;
}
}
}
// Validation links if available
if (deployment.validationLinks && deployment.validationLinks.length > 0) {
message += '\n**🔗 Validation Links**:\n';
deployment.validationLinks.forEach(link => {
message += `• ${link}\n`;
});
}
// Warnings
if (deployment.deploymentWarnings && deployment.deploymentWarnings.length > 0) {
message += '\n**⚠️ Warnings**:\n';
deployment.deploymentWarnings.forEach(warning => {
message += `• ${warning}\n`;
});
}
// Errors
if (deployment.deploymentErrors && deployment.deploymentErrors.length > 0) {
message += '\n**❌ Errors**:\n';
deployment.deploymentErrors.forEach(err => {
message += `• ${err}\n`;
});
}
// Validation messages (legacy field)
if (deployment.validationMessages && deployment.validationMessages.length > 0) {
message += '\n**Validation Messages**:\n';
deployment.validationMessages.forEach(msg => {
message += `• ${msg}\n`;
});
}
// Add action hint for verification state
if (status.toLowerCase().includes('verification')) {
message += '\n**Next Actions**:\n';
message += '• Review changes at the preview URL\n';
message += '• Use `complete_deployment` to finalize\n';
message += '• Use `reset_deployment` to rollback\n';
}
message = ResponseBuilder.addFooter(message);
// Return both structured data and message
return { data: structuredData, message: message };
}
/**
* Format multiple deployments with optional limit
*/
static formatMultipleDeployments(deployments, limit) {
const { FORMATTING: { STATUS_ICONS } } = Config;
// Get project info from configured projects
let projectId = null;
let projectName = null;
try {
const ProjectTools = require('../project-tools');
const projects = ProjectTools.getConfiguredProjects();
if (projects && projects.length > 0) {
const defaultProject = projects.find(p => p.isDefault) || projects[0];
projectId = defaultProject.id;
projectName = defaultProject.name;
}
} catch (error) {
// Fall back to environment variables if ProjectTools fails
projectId = process.env.OPTIMIZELY_PROJECT_ID;
}
// Ensure deployments is an array
const deploymentArray = Array.isArray(deployments) ? deployments : [deployments];
// Sort by start time (most recent first)
const sorted = deploymentArray.sort((a, b) => {
const dateA = new Date(a.startTime || a.created || 0);
const dateB = new Date(b.startTime || b.created || 0);
return dateB - dateA;
});
// Apply limit if specified
const toShow = limit ? sorted.slice(0, limit) : sorted;
let response = `${STATUS_ICONS.DEPLOY} **Deployment Status**\n\n`;
toShow.forEach((deployment) => {
const status = deployment.status || 'Unknown';
let statusIcon = STATUS_ICONS.IN_PROGRESS;
if (status.toLowerCase().includes('success') || status.toLowerCase().includes('completed')) {
statusIcon = STATUS_ICONS.SUCCESS;
} else if (status.toLowerCase().includes('fail')) {
statusIcon = STATUS_ICONS.ERROR;
} else if (status.toLowerCase().includes('reset')) {
statusIcon = STATUS_ICONS.WARNING;
} else if (status.toLowerCase().includes('verification')) {
statusIcon = STATUS_ICONS.VERIFICATION;
}
response += `${statusIcon} **Deployment #${deployment.id}**\n`;
response += `• Status: **${status}**\n`;
const sourceEnv = deployment.startEnvironment || deployment.parameters?.sourceEnvironment || null;
const targetEnv = deployment.endEnvironment || deployment.parameters?.targetEnvironment || 'N/A';
if (!sourceEnv || sourceEnv === 'Unknown' || sourceEnv === 'N/A') {
response += `• To: ${targetEnv} (Package Upload)\n`;
} else {
response += `• From: ${sourceEnv} → To: ${targetEnv}\n`;
}
// Always show preview URL for deployments awaiting verification
if (status.toLowerCase().includes('verification')) {
// DXP-87: Show validation links from API
if (deployment.validationLinks && deployment.validationLinks.length > 0) {
deployment.validationLinks.forEach(link => {
response += `• **🔗 Verification**: ${link}\n`;
});
} else {
const previewUrl = this.getPreviewUrl(deployment.endEnvironment, projectId);
if (previewUrl) {
response += `• **Preview URL**: ${previewUrl}\n`;
}
}
}
if (deployment.startTime) {
response += `• Started: ${this.formatLocalDateTime(deployment.startTime)}\n`;
}
response += '\n';
});
if (limit && deploymentArray.length > limit) {
response += `_Showing ${limit} most recent deployments out of ${deploymentArray.length} total_\n`;
}
return ResponseBuilder.addFooter(response);
}
/**
* Format deployment started response
*/
static formatDeploymentStarted(deployment, args) {
const { FORMATTING: { STATUS_ICONS } } = Config;
// Get project info from args, configured projects, or environment variables
let projectId = args.projectId;
let projectName = args.projectName;
if (!projectId || !projectName) {
try {
const ProjectTools = require('../project-tools');
const projects = ProjectTools.getConfiguredProjects();
if (projects && projects.length > 0) {
const defaultProject = projects.find(p => p.isDefault) || projects[0];
projectId = projectId || defaultProject.id;
projectName = projectName || defaultProject.name;
}
} catch (error) {
// Fall back to environment variables if ProjectTools fails
projectId = projectId || process.env.OPTIMIZELY_PROJECT_ID;
projectName = projectName || process.env.OPTIMIZELY_PROJECT_NAME;
}
}
// Determine deployment type
let deploymentType = args.deploymentType;
if (!deploymentType) {
// Apply smart defaults
const isUpward = this.isUpwardDeployment(args.sourceEnvironment, args.targetEnvironment);
deploymentType = isUpward ? 'code' : 'content';
}
const needsVerification = args.targetEnvironment === 'Production' && !args.directDeploy;
const previewUrl = needsVerification ? this.getPreviewUrl(args.targetEnvironment, projectId) : null;
// Build structured data for automation tools
const structuredData = {
deploymentId: deployment.id,
status: deployment.status || 'InProgress',
sourceEnvironment: args.sourceEnvironment,
targetEnvironment: args.targetEnvironment,
deploymentType: deploymentType,
projectId: projectId,
projectName: projectName,
startTime: deployment.startTime || new Date().toISOString(),
percentComplete: deployment.percentComplete || 0,
needsVerification: needsVerification,
previewUrl: previewUrl,
sourceApps: args.sourceApps || [],
includeBlob: args.includeBlob,
includeDatabase: args.includeDatabase,
directDeploy: args.directDeploy,
useMaintenancePage: args.useMaintenancePage
};
// Build human-readable message
let message = `${STATUS_ICONS.SUCCESS} **Deployment Started`;
if (projectName) {
message += ` - ${projectName}**\n\n`;
} else {
message += `**\n\n`;
}
message += `**Deployment ID**: ${deployment.id}\n`;
message += `**From**: ${args.sourceEnvironment}\n`;
message += `**To**: ${args.targetEnvironment}\n`;
message += `**Type**: ${deploymentType.charAt(0).toUpperCase() + deploymentType.slice(1)}`;
if (deploymentType === 'code' && args.sourceApps) {
message += ` (${args.sourceApps.join(', ')})`;
}
message += '\n';
if (deployment.status) {
message += `**Status**: ${deployment.status}\n`;
}
// Always show preview URL for deployments that will need verification
if (needsVerification) {
if (previewUrl) {
message += `\n**🔗 Preview URL (Slot)**: ${previewUrl}\n`;
message += `_Your deployment will be available for preview at this slot URL once it enters verification state_\n`;
} else {
message += `\n**🔗 Verification URL Information**:\n`;
message += `• When the deployment reaches verification state, the slot URL will be available in the DXP portal\n`;
message += `• Expected format: https://[your-site-name]-slot.dxcloud.episerver.net/\n`;
message += `• You'll need to check the DXP portal deployment details for the exact URL\n`;
}
}
message += '\n## 🎯 **Monitoring Options**:\n';
message += `### Option 1: **Continuous Monitoring** (Recommended)\n`;
message += `Use \`monitor_deployment\` to automatically check progress every 30 seconds:\n`;
message += `\`\`\`\nmonitor_deployment({ deploymentId: "${deployment.id}" })\n\`\`\`\n`;
message += `### Option 2: **Manual Status Checks**\n`;
message += `Use \`get_deployment_status\` to check progress on demand:\n`;
message += `\`\`\`\nget_deployment_status({ deploymentId: "${deployment.id}" })\n\`\`\`\n`;
message += '\n**💡 Important**: Use the MCP monitoring tools above instead of bash loops.\n';
message += 'The \`monitor_deployment\` tool provides intelligent progress tracking and automatic notifications.\n';
if (needsVerification) {
message += '\n**📋 Deployment Stages**:\n';
message += '1. **In Progress** - Deployment is running\n';
message += '2. **Verification** - Review changes at preview URL\n';
message += '3. **Complete** - After you run `complete_deployment`\n';
}
message = ResponseBuilder.addFooter(message);
// Return both structured data and message
return { data: structuredData, message: message };
}
/**
* Format deployment completed response
*/
static formatDeploymentCompleted(deployment, projectName = null, projectId = null) {
const { FORMATTING: { STATUS_ICONS } } = Config;
projectName = projectName || process.env.OPTIMIZELY_PROJECT_NAME;
// Check if we have environment information
const targetEnv = deployment.endEnvironment || deployment.targetEnvironment;
const status = deployment.status || 'Completed';
// Build structured data for automation tools
const structuredData = {
deploymentId: deployment.id,
status: status,
targetEnvironment: targetEnv,
completionTime: deployment.completionTime || new Date().toISOString(),
isProduction: targetEnv === 'Production' || targetEnv?.toLowerCase() === 'production'
};
// Check if status is transitional (Completing) vs final (Succeeded)
const isCompleting = status === 'Completing';
if (isCompleting) {
// DXP-71: Deployment is still completing - provide monitoring instructions
let message = `${STATUS_ICONS.IN_PROGRESS} **Deployment is Now Completing`;
if (projectName) {
message += ` - ${projectName}**\n\n`;
} else {
message += `**\n\n`;
}
message += `**Deployment ID**: ${deployment.id}\n`;
message += `**Status**: ${status}\n`;
if (targetEnv) {
message += `**Target Environment**: ${targetEnv}\n`;
}
message += '\n🔄 **Completion in Progress**\n\n';
if (targetEnv === 'Production' || targetEnv?.toLowerCase() === 'production') {
message += 'The deployment is being finalized and promoted from the verification slot to Production.\n';
message += 'This final step usually takes a few moments.\n\n';
} else if (targetEnv) {
message += `The deployment is being finalized to ${targetEnv}.\n`;
message += 'This usually takes a few moments.\n\n';
} else {
message += 'The deployment is being finalized.\n';
message += 'This usually takes a few moments.\n\n';
}
// Add monitoring instructions (transparent pattern like InProgress)
const monitorInterval = 30; // 30 seconds
message += `📊 **MONITORING INSTRUCTIONS FOR AI**\n\n`;
message += `To monitor until completion, use get_deployment_status with:\n`;
message += `• deploymentId: "${deployment.id}"\n`;
message += `• waitBeforeCheck: ${monitorInterval} (waits ${monitorInterval} seconds then checks)\n`;
message += `• monitor: true (enables monitoring prompts)\n`;
if (projectName) {
message += `• projectName: "${projectName}"\n`;
}
message += `\n`;
message += `**MONITORING PATTERN:**\n`;
message += `1. Wait ${monitorInterval} seconds\n`;
message += `2. Check status with get_deployment_status\n`;
message += `3. If still Completing, repeat\n`;
message += `4. When Succeeded, inform user deployment is live\n`;
message += `5. If Failed, alert user to check logs\n\n`;
message += `**Note:** Completion typically takes 2-15 minutes. Be patient.`;
message = ResponseBuilder.addFooter(message);
return { data: structuredData, message: message };
}
// Status is final (Succeeded, Failed, etc) - show completion message
let message = `${STATUS_ICONS.SUCCESS} **Deployment Completed Successfully`;
if (projectName) {
message += ` - ${projectName}**\n\n`;
} else {
message += `**\n\n`;
}
message += `**Deployment ID**: ${deployment.id}\n`;
if (deployment.status) {
message += `**Final Status**: ${deployment.status}\n`;
}
if (deployment.completionTime) {
message += `**Completed At**: ${this.formatLocalDateTime(deployment.completionTime)}\n`;
}
if (targetEnv === 'Production' || targetEnv?.toLowerCase() === 'production') {
message += '\n✅ **Deployment Completed**\n\n';
message += 'The deployment has been successfully promoted from the verification slot to production.\n';
message += 'Your changes are now live on the production environment.';
} else if (targetEnv) {
message += `\n✅ The deployment to **${targetEnv}** has been completed successfully.`;
} else {
// Generic message when we don't know the environment
message += '\n✅ The deployment has been completed successfully.\n\n';
message += '**Note**: If this was a Production deployment, changes are now live after being promoted from the verification slot.';
}
message = ResponseBuilder.addFooter(message);
// Return both structured data and message
return { data: structuredData, message: message };
}
/**
* Format deployment reset response
*/
static formatDeploymentReset(deployment, includeDbRollback, projectName = null) {
const { FORMATTING: { STATUS_ICONS } } = Config;
projectName = projectName || process.env.OPTIMIZELY_PROJECT_NAME;
// Get deployment details if available
const sourceEnv = deployment.parameters?.sourceEnvironment;
const targetEnv = deployment.parameters?.targetEnvironment;
const status = deployment.status || 'Resetting';
// Build structured data for automation tools
const structuredData = {
deploymentId: deployment.id,
status: status,
sourceEnvironment: sourceEnv,
targetEnvironment: targetEnv,
includeDbRollback: includeDbRollback,
resetInitiatedAt: new Date().toISOString()
};
// DXP-71: Check if status is transitional (Resetting) vs final (Reset)
const isResetting = status === 'Resetting';
if (isResetting) {
// Deployment is still resetting - provide monitoring instructions
let message = `${STATUS_ICONS.WARNING} **Deployment Reset in Progress`;
if (projectName) {
message += ` - ${projectName}**\n\n`;
} else {
message += `**\n\n`;
}
message += `**Deployment ID**: ${deployment.id}\n`;
message += `**Status**: ${status}\n`;
if (deployment.parameters && sourceEnv && targetEnv) {
message += `**Original Deployment**: ${sourceEnv} → ${targetEnv}\n`;
}
message += '\n🔄 **Rollback in Progress**\n\n';
message += 'The deployment is being rolled back. This typically takes 2-5 minutes.\n';
if (includeDbRollback) {
message += '\n⚠️ **Database Rollback**: Database changes are also being reverted.\n';
}
// Add monitoring instructions (transparent pattern)
const monitorInterval = 30; // 30 seconds
message += `\n📊 **MONITORING INSTRUCTIONS FOR AI**\n\n`;
message += `To monitor until reset completes, use get_deployment_status with:\n`;
message += `• deploymentId: "${deployment.id}"\n`;
message += `• waitBeforeCheck: ${monitorInterval} (waits ${monitorInterval} seconds then checks)\n`;
message += `• monitor: true (enables monitoring prompts)\n`;
if (projectName) {
message += `• projectName: "${projectName}"\n`;
}
message += `\n`;
message += `**MONITORING PATTERN:**\n`;
message += `1. Wait ${monitorInterval} seconds\n`;
message += `2. Check status with get_deployment_status\n`;
message += `3. If still Resetting, repeat\n`;
message += `4. When Reset, inform user rollback is complete\n`;
message += `5. If Failed, alert user to check logs\n\n`;
message += `**Note:** Reset typically takes 2-5 minutes.`;
message = ResponseBuilder.addFooter(message);
return { data: structuredData, message: message };
}
// Status is final (Reset, Failed, etc) - show completion message
let message = `${STATUS_ICONS.SUCCESS} **Deployment Reset Complete`;
if (projectName) {
message += ` - ${projectName}**\n\n`;
} else {
message += `**\n\n`;
}
message += `**Deployment ID**: ${deployment.id}\n`;
message += `**Final Status**: ${status}\n`;
if (deployment.parameters && sourceEnv && targetEnv) {
message += `**Original Deployment**: ${sourceEnv} → ${targetEnv}\n`;
}
message += '\n✅ **Rollback Completed**\n\n';
message += 'The deployment has been successfully rolled back.\n';
if (targetEnv) {
message += `The ${targetEnv} environment has been restored to its previous state.\n`;
}
if (includeDbRollback) {
message += '\n✅ **Database Rollback**: Database changes have been reverted.\n';
}
message = ResponseBuilder.addFooter(message);
// Return both structured data and message
return { data: structuredData, message: message };
}
/**
* Helper to check if deployment is upward (code flow)
*/
static isUpwardDeployment(source, target) {
const envOrder = ['Integration', 'Preproduction', 'Production'];
const sourceIndex = envOrder.indexOf(source);
const targetIndex = envOrder.indexOf(target);
return targetIndex > sourceIndex;
}
/**
* Get preview URL for an environment
*/
static getPreviewUrl(environment, projectId) {
if (!projectId) return null;
// Remove any @ prefix from project ID if present
const cleanProjectId = projectId.replace(/^@/, '');
// For Production deployments in verification, we cannot generate the slot URL
// The actual URL needs to come from the DXP API response
if (environment === 'Production') {
// The slot URL format is: https://{short-name}-slot.dxcloud.episerver.net/
// But we cannot determine the short name from the project ID alone
// This should be retrieved from the deployment API response
return null;
}
const envMap = {
'Integration': 'integration',
'Preproduction': 'preproduction'
};
const envSlug = envMap[environment];
if (!envSlug) return null;
// For Int/Pre environments, use standard environment URL
return `https://${cleanProjectId}.${envSlug}.dxp.optimizely.com/`;
}
}
module.exports = DeploymentFormatters;