start_deployment
Initiate deployments between Optimizely DXP environments with automated monitoring. Supports code, content, or full deployments with configurable options for files and databases.
Instructions
🚀 Start new deployment from source to target environment. ASYNC: 5-30min. Initiates deployment and auto-monitors progress with real-time updates. CODE deployments flow upward (Integration→Preproduction→Production). CONTENT deployments flow downward (Production→Preproduction→Integration). Returns deploymentId immediately. Set includeBlob=true for static files, includeDB=true for database sync. When status reaches "AwaitingVerification", use get_deployment_status() to get slot URL for testing, then complete_deployment() to finalize. Required: sourceEnvironment, targetEnvironment.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| sourceEnvironment | Yes | ||
| targetEnvironment | Yes | ||
| deploymentType | No | ||
| sourceApps | No | ||
| includeBlob | No | ||
| includeDatabase | No | ||
| directDeploy | No | ||
| useMaintenancePage | No | ||
| webhookUrl | No | HTTP endpoint to receive deployment events (HTTPS required in production) | |
| webhookHeaders | No | Custom headers to include in webhook requests (e.g., { "Authorization": "Bearer token" }) | |
| projectName | No | ||
| projectId | No | ||
| apiKey | No | ||
| apiSecret | No |
Implementation Reference
- Primary handler for 'start_deployment' tool. Performs permissions check, validates path/type, calls DXP REST API to start deployment, formats response, emits events, registers webhooks/monitoring.class DeploymentActionOperations { /** * Start a new deployment */ static async handleStartDeployment(args: StartDeploymentArgs): Promise<any> { // Check if this is a self-hosted project if (args.isSelfHosted || args.connectionString) { return ResponseBuilder.invalidParams('Deployments are not available for self-hosted projects. Self-hosted projects can only download existing backups and blobs.'); } if (!args.apiKey || !args.apiSecret || !args.projectId) { return ResponseBuilder.invalidParams('Missing required parameters'); } try { const result = await this.startDeployment(args); // Check if result is already a structured response with data and message if (result && typeof result === 'object' && 'data' in result && 'message' in result) { return ResponseBuilder.successWithStructuredData(result.data, result.message); } // Fallback for legacy string responses return ResponseBuilder.success(result); } catch (error: any) { console.error('Start deployment error:', error); return ResponseBuilder.internalError('Failed to start deployment', error.message); } } static async startDeployment(args: StartDeploymentArgs): Promise<any> { const { apiKey, apiSecret, projectId, projectName, sourceEnvironment, targetEnvironment, deploymentType, sourceApps, includeBlob, includeDatabase, directDeploy, useMaintenancePage, webhookUrl, webhookHeaders } = args; // DXP-67: Defensive check for useMaintenancePage to prevent accidental production downtime if (useMaintenancePage === true) { console.error('⚠️ WARNING: useMaintenancePage is set to TRUE'); console.error(` This will show a maintenance page during deployment to ${targetEnvironment}`); if (targetEnvironment === 'Production' || targetEnvironment === 'Preproduction') { console.error(' ⚠️ CAUTION: Deploying to Production/Preproduction with maintenance page!'); console.error(' This will cause site downtime for end users.'); } } console.error(`Starting deployment from ${sourceEnvironment} to ${targetEnvironment} for project ${projectId}`); // Check permissions for both environments first const projectConfig: ProjectConfig = { apiKey: apiKey!, apiSecret: apiSecret!, projectId: projectId!, id: projectId!, name: projectName || 'Project' }; const permissions: Permissions = await PermissionChecker.getOrCheckPermissionsSafe(projectConfig); // Check if user has access to both source and target const missingAccess: string[] = []; if (!permissions.accessible.includes(sourceEnvironment!)) { missingAccess.push(sourceEnvironment!); } if (!permissions.accessible.includes(targetEnvironment!)) { missingAccess.push(targetEnvironment!); } if (missingAccess.length > 0) { let response = `ℹ️ **Access Level Check**\n\n`; response += `Deployments require access to both source and target environments.\n\n`; response += `**Requested:** ${sourceEnvironment} → ${targetEnvironment}\n`; response += `**Your access level:** ${permissions.accessible.join(', ')} environment${permissions.accessible.length > 1 ? 's' : ''}\n`; response += `**Additional access needed:** ${missingAccess.join(', ')}\n\n`; // Suggest valid deployment paths based on what they have access to if (permissions.accessible.length >= 2) { response += `**Available Deployment Options:**\n\n`; // Check for valid deployment paths const hasInt = permissions.accessible.includes('Integration'); const hasPre = permissions.accessible.includes('Preproduction'); const hasProd = permissions.accessible.includes('Production'); if (hasInt && hasPre) { response += `• **Integration → Preproduction** (Code deployment)\n`; response += ` \`start_deployment sourceEnvironment: "Integration" targetEnvironment: "Preproduction"\`\n\n`; } if (hasPre && hasProd) { response += `• **Preproduction → Production** (Code deployment)\n`; response += ` \`start_deployment sourceEnvironment: "Preproduction" targetEnvironment: "Production"\`\n\n`; } // For content copy (if they have the environments but trying wrong direction) if (hasProd && hasPre) { response += `• **Production → Preproduction** (Content copy - use copy_content instead)\n`; response += ` \`copy_content sourceEnvironment: "Production" targetEnvironment: "Preproduction"\`\n\n`; } if (hasProd && hasInt) { response += `• **Production → Integration** (Content copy - use copy_content instead)\n`; response += ` \`copy_content sourceEnvironment: "Production" targetEnvironment: "Integration"\`\n\n`; } response += `\n💡 **Important:** Code deployments only work upward (Int→Pre→Prod).\n`; response += `For downward content sync, use the \`copy_content\` tool instead.`; } else if (permissions.accessible.length === 1) { response += `⚠️ You need access to at least 2 environments for deployments.\n`; response += `Your API key only has access to ${permissions.accessible[0]}.`; } // Return as structured response return { data: { error: 'insufficient_permissions', missingAccess: missingAccess, availableEnvironments: permissions.accessible }, message: response }; } // Check for any running deployments before starting a new one console.error('Checking for active deployments...'); try { const DeploymentListOperations = require('./deployment-list'); const listResult = await DeploymentListOperations.listDeployments({ apiKey, apiSecret, projectId, limit: 5, offset: 0 }); // DEBUG: Log the actual result to troubleshoot blocking issue console.error('DEBUG: listResult type:', typeof listResult); console.error('DEBUG: listResult (first 500 chars):', typeof listResult === 'string' ? listResult.substring(0, 500) : JSON.stringify(listResult).substring(0, 500)); // Check if there's a deployment in progress // CRITICAL FIX (v3.17.2): First check if this is an error response, not deployment data // Previously, error messages containing "InProgress" text would falsely trigger // the "deployment already in progress" check, preventing all new deployments if (listResult && typeof listResult === 'string') { // Skip check if this is an error message const isError = listResult.includes('❌') || listResult.includes('Error') || listResult.includes('Failed') || listResult.includes('Invalid') || listResult.includes('Forbidden') || listResult.includes('support@jaxondigital.com'); console.error('DEBUG: isError check result:', isError); if (isError) { // Log error but continue - we can't check deployment status but shouldn't block console.error('DEBUG: Detected error in deployment list, continuing with deployment...'); console.error('Warning: Could not check for active deployments due to error:', listResult.substring(0, 200)); } else { // Only check for in-progress deployments if this is actual deployment data console.error('DEBUG: Checking for in-progress deployments in actual deployment data...'); // FIXED: Don't use 🔄 emoji as it appears in all deployments (even completed ones) // Only look for actual status indicators of in-progress deployments const hasInProgress = listResult.includes('InProgress') || listResult.includes('Deploying') || listResult.includes('Status: **InProgress**') || listResult.includes('Status: **Deploying**'); console.error('DEBUG: hasInProgress result:', hasInProgress); console.error('DEBUG: Contains InProgress:', listResult.includes('InProgress')); console.error('DEBUG: Contains Deploying:', listResult.includes('Deploying')); console.error('DEBUG: Contains Status: **InProgress**:', listResult.includes('Status: **InProgress**')); console.error('DEBUG: Contains Status: **Deploying**:', listResult.includes('Status: **Deploying**')); if (hasInProgress) { console.error('DEBUG: Found in-progress deployment, blocking new deployment...'); // Extract details about the in-progress deployment if possible const lines = listResult.split('\n'); let inProgressDetails = ''; for (let i = 0; i < lines.length; i++) { if (lines[i].includes('InProgress') || lines[i].includes('🔄')) { // Try to get the deployment ID and environments const deploymentIdMatch = lines[i].match(/#([a-f0-9-]+)/); if (deploymentIdMatch) { inProgressDetails = `Deployment ${deploymentIdMatch[1]} `; } // Look for environment info in nearby lines for (let j = Math.max(0, i-2); j < Math.min(lines.length, i+3); j++) { if (lines[j].includes('→')) { const envMatch = lines[j].match(/(\w+)\s*→\s*(\w+)/); if (envMatch) { inProgressDetails += `(${envMatch[1]} → ${envMatch[2]})`; break; } } } break; } } return `⚠️ **Deployment Already In Progress**\n\n` + `Cannot start a new deployment while another is running.\n\n` + (inProgressDetails ? `**Active Deployment:** ${inProgressDetails}\n\n` : '') + `Please wait for the current deployment to complete or reset it before starting a new one.\n\n` + `**Options:**\n` + `• Use \`get_deployment_status\` to check progress\n` + `• Use \`reset_deployment\` if the deployment is stuck\n` + `• Wait for automatic completion (usually 10-30 minutes)`; } } } } catch (checkError: any) { // Don't fail the deployment if we can't check status console.error('Warning: Could not check for active deployments:', checkError.message); } // Validate deployment path const pathValidation: PathValidation = DeploymentValidator.validateDeploymentPath(sourceEnvironment!, targetEnvironment!); if (!pathValidation.valid) { // Check if this is a downward deployment that should use content copy if (sourceEnvironment === 'Production' || (sourceEnvironment === 'Preproduction' && targetEnvironment === 'Integration')) { let response = `ℹ️ **Invalid Deployment Direction**\n\n`; response += `You're trying to deploy from **${sourceEnvironment}** to **${targetEnvironment}**.\n\n`; response += `❌ **Code deployments can only go upward:**\n`; response += `• Integration → Preproduction\n`; response += `• Preproduction → Production\n\n`; response += `✅ **For downward content synchronization, use:**\n`; response += `\`copy_content sourceEnvironment: "${sourceEnvironment}" targetEnvironment: "${targetEnvironment}"\`\n\n`; response += `💡 **Why?** Code changes should flow through proper testing stages (Int→Pre→Prod),\n`; response += `while content can be copied from production back to lower environments for testing.`; // Return as structured response return { data: { error: 'invalid_deployment_direction', sourceEnvironment: sourceEnvironment, targetEnvironment: targetEnvironment, suggestion: 'use_copy_content' }, message: response }; } // For other invalid paths (like Int→Prod), show the standard error 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: 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: 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)'); } } // DXP-101: Build deployment parameters for REST API (replacing PowerShell) // Based on EpiCloud.psm1 Start-EpiDeployment SourceEnvironment parameter set const deploymentParams: DeploymentParams = { sourceEnvironment: sourceEnvironment!, // Lowercase per PowerShell source targetEnvironment: targetEnvironment! // Lowercase per PowerShell source }; // Add deployment type parameters if (deployCode) { // SourceApp is required for code deployments const appsToUse = sourceApps && sourceApps.length > 0 ? sourceApps : ['cms']; // Default to CMS app deploymentParams.sourceApps = appsToUse; // Plural 'sourceApps' per PowerShell source console.error(`Deploying code with apps: ${appsToUse.join(', ')}`); } if (deployContent) { // Add content deployment flags - always include (per PowerShell source) deploymentParams.includeBlob = includeBlob !== false; deploymentParams.includeDB = includeDatabase !== false; console.error(`Deploying content with includeBlob=${deploymentParams.includeBlob}, includeDB=${deploymentParams.includeDB}`); } // Add optional parameters - only if explicitly set to true if (directDeploy === true) { deploymentParams.directDeploy = true; // Lowercase per PowerShell source } if (useMaintenancePage === true) { deploymentParams.maintenancePage = true; // 'maintenancePage' not 'UseMaintenancePage' per PowerShell source } console.error(`Starting deployment via REST API with payload:`); console.error(JSON.stringify(deploymentParams, null, 2)); // DXP-101: Use REST API instead of PowerShell (3-10x faster, no PowerShell dependency) try { const result: DeploymentResult = await DXPRestClient.startDeployment( projectId!, apiKey!, apiSecret!, deploymentParams as any, { apiUrl: args.apiUrl } // Support custom API URLs ); // Format response if (result) { const formatted = DeploymentFormatters.formatDeploymentStarted(result, args); // Extract deployment ID from result and start monitoring if (result.id) { // DXP-136: Emit deployment started event try { DeploymentResourceHandler.emitStarted(result.id, { project: projectName, environment: targetEnvironment, sourceEnvironment: sourceEnvironment, targetEnvironment: targetEnvironment, deploymentType: deploymentType, status: result.status }); } catch (eventError: any) { console.error(`Failed to emit deployment event: ${eventError.message}`); // Don't fail the deployment if event emission fails } // DXP-140: Register webhook if provided if (webhookUrl) { try { const webhookManager = getGlobalWebhookManager(); const registrationResult = webhookManager.register( result.id, // operationId (deploymentId) webhookUrl, { headers: webhookHeaders || {}, project: projectName, environment: targetEnvironment } ); const logger = new StructuredLogger({ context: { tool: 'start_deployment', deployment_id: result.id, project: projectName, environment: targetEnvironment } }); if (registrationResult.success) { logger.info('Webhook registered for deployment', { webhook_url: webhookUrl, deployment_id: result.id }); console.log(`🔔 Webhook registered for deployment ${result.id}`); } else { logger.warn('Webhook registration failed', { error: registrationResult.error, deployment_id: result.id }); console.log(`⚠️ Webhook registration failed: ${registrationResult.error}`); } } catch (webhookError: any) { console.error(`Failed to register webhook: ${webhookError.message}`); // Don't fail the deployment if webhook registration fails } } try { const monitor = getGlobalMonitor(); monitor.startMonitoring({ deploymentId: result.id, projectId: args.projectId!, apiKey: args.apiKey!, apiSecret: args.apiSecret!, interval: 60 * 1000 // 1 minute default }); const logger = new StructuredLogger({ context: { tool: 'start_deployment', deployment_id: result.id } }); logger.info('Auto-monitoring started for deployment', { deployment_id: result.id, interval_ms: 60 * 1000 }); console.log(`🔄 Auto-monitoring started for deployment ${result.id}`); } catch (monitorError: any) { console.error(`Failed to start monitoring: ${monitorError.message}`); // Don't fail the deployment if monitoring fails } } // Return structured response with both data and message return formatted; } return { data: null, message: ResponseBuilder.addFooter('Deployment started but no details available') }; } catch (error: any) { // Handle REST API errors const errorDetails = { operation: 'Start Deployment', projectId, projectName: args.projectName, sourceEnvironment, targetEnvironment, apiKey }; // Check if this is an access denied error if (error.statusCode === 401 || error.statusCode === 403) { return ErrorHandler.formatError({ type: 'ACCESS_DENIED', message: 'Access denied to deployment API', statusCode: error.statusCode } as any, errorDetails); } // Generic error handling return ErrorHandler.formatError({ type: 'API_ERROR', message: error.message, statusCode: error.statusCode } as any, errorDetails); } }
- Input schema/arguments type for start_deployment tool.interface StartDeploymentArgs { apiKey?: string; apiSecret?: string; projectId?: string; projectName?: string; sourceEnvironment?: string; targetEnvironment?: string; deploymentType?: string; sourceApps?: string[]; includeBlob?: boolean; includeDatabase?: boolean; directDeploy?: boolean; useMaintenancePage?: boolean; isSelfHosted?: boolean; connectionString?: string; apiUrl?: string; webhookUrl?: string; webhookHeaders?: Record<string, string>; }
- lib/tools/deployment/index.ts:22-23 (handler)Public DeploymentTools.handleStartDeployment - entrypoint delegated to action operations.static async handleStartDeployment(args: any): Promise<any> { return DeploymentActionOperations.handleStartDeployment(args);
- Helper formatter for deployment started response used by handler.static formatDeploymentStarted(deployment: Deployment, args: StartDeploymentArgs): StructuredResult { 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: any) => 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) : 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 }; }
- lib/cache-manager.ts:90-91 (registration)Tool listed in no-cache operations and invalidation map, indicating registration awareness.'start_deployment', 'complete_deployment',