UNPKG

selah-cli

Version:

Zero-setup AWS deployment for Bolt.new apps. Build in Bolt, deploy to production in 3 minutes with AI guidance.

219 lines (188 loc) 6.43 kB
#!/usr/bin/env node /** * EC2 Deployment Runner for Selah CLI - UPDATED VERSION * * This script runs on an EC2 instance and handles remote deployments from Bolt.new * AWS credentials are passed securely from the client, no local AWS setup needed. * * UPDATE INSTRUCTIONS: * 1. SSH to EC2: ssh -i selah.pem ec2-user@3.86.246.81 * 2. Stop current runner: pm2 stop ec2-deploy-runner * 3. Backup old runner: mv ec2-deploy-runner.js ec2-deploy-runner.js.bak * 4. Upload this file: scp -i selah.pem ec2-deploy-runner-update.js ec2-user@3.86.246.81:~/ec2-deploy-runner.js * 5. Start updated runner: pm2 start ec2-deploy-runner.js * 6. Save PM2 config: pm2 save */ const express = require('express'); const multer = require('multer'); const fs = require('fs').promises; const path = require('path'); const { spawn, execSync } = require('child_process'); const unzipper = require('unzipper'); const { createReadStream, createWriteStream } = require('fs'); const crypto = require('crypto'); const https = require('https'); const http = require('http'); const app = express(); const PORT = process.env.PORT || 3000; const jobs = new Map(); // Configure AWS account function configureAWSAccount(awsEnv) { console.log('🔧 Configuring AWS account for CDK...'); if (awsEnv.AWS_ACCESS_KEY_ID && awsEnv.AWS_SECRET_ACCESS_KEY) { try { // Get AWS account ID using CLI const accountId = execSync('aws sts get-caller-identity --query Account --output text', { env: { ...process.env, ...awsEnv }, encoding: 'utf8' }).trim(); const region = awsEnv.AWS_REGION || process.env.AWS_REGION || 'us-east-1'; return { accountId, region }; } catch (error) { console.error('Failed to get AWS account ID:', error); throw error; } } throw new Error('AWS credentials not provided'); } // Download deployment package from signed URL async function downloadDeploymentPackage(signedUrl, targetPath) { return new Promise((resolve, reject) => { const file = createWriteStream(targetPath); const request = (signedUrl.startsWith('https') ? https : http).get(signedUrl, response => { if (response.statusCode !== 200) { reject(new Error(`Failed to download: ${response.statusCode}`)); return; } response.pipe(file); file.on('finish', () => { file.close(); resolve(); }); }); request.on('error', error => { fs.unlink(targetPath).catch(() => {}); reject(error); }); file.on('error', error => { fs.unlink(targetPath).catch(() => {}); reject(error); }); }); } // Handle deployment request app.post('/deploy', express.json(), async (req, res) => { const { jobId, stackId, projectName, region, environment, deploymentUrl } = req.body; if (!jobId || !deploymentUrl) { res.status(400).json({ error: 'Missing required parameters' }); return; } console.log(`📦 Processing deployment ${jobId}...`); try { // Create deployment directory const deployDir = path.join('/tmp', `deploy-${jobId}`); await fs.mkdir(deployDir, { recursive: true }); // Download deployment package const packagePath = path.join(deployDir, 'deployment.zip'); console.log('⬇️ Downloading deployment package...'); await downloadDeploymentPackage(deploymentUrl, packagePath); // Extract package console.log('📂 Extracting package...'); await fs.mkdir(path.join(deployDir, 'extracted'), { recursive: true }); await new Promise((resolve, reject) => { createReadStream(packagePath) .pipe(unzipper.Extract({ path: path.join(deployDir, 'extracted') })) .on('close', resolve) .on('error', reject); }); // Load AWS credentials from .env.deployment const envPath = path.join(deployDir, 'extracted', '.env.deployment'); const envContent = await fs.readFile(envPath, 'utf8'); const awsEnv = {}; envContent.split('\n').forEach(line => { const [key, value] = line.split('='); if (key && value) { awsEnv[key.trim()] = value.trim(); } }); // Configure AWS account const { accountId } = await configureAWSAccount(awsEnv); // Start deployment console.log('🚀 Starting deployment...'); const deployProcess = spawn('bash', ['deploy.sh'], { cwd: path.join(deployDir, 'extracted'), env: { ...process.env, ...awsEnv, CDK_DEFAULT_ACCOUNT: accountId, CDK_DEFAULT_REGION: region || 'us-east-1' } }); // Store job info jobs.set(jobId, { status: 'running', output: [], process: deployProcess }); // Handle process output deployProcess.stdout.on('data', data => { const output = data.toString(); console.log(output); jobs.get(jobId).output.push(output); }); deployProcess.stderr.on('data', data => { const output = data.toString(); console.error(output); jobs.get(jobId).output.push(output); }); deployProcess.on('close', code => { const job = jobs.get(jobId); job.status = code === 0 ? 'completed' : 'failed'; job.exitCode = code; // Cleanup fs.rm(deployDir, { recursive: true, force: true }).catch(console.error); }); res.json({ status: 'accepted', message: 'Deployment started', jobId }); } catch (error) { console.error('Deployment failed:', error); res.status(500).json({ error: 'Deployment failed', message: error.message }); // Update job status jobs.set(jobId, { status: 'failed', error: error.message, output: [] }); } }); // Get deployment status app.get('/status/:jobId', (req, res) => { const { jobId } = req.params; const job = jobs.get(jobId); if (!job) { res.status(404).json({ error: 'Job not found' }); return; } res.json({ status: job.status, output: job.output.join('\n'), ...(job.error && { error: job.error }), ...(job.exitCode !== undefined && { exitCode: job.exitCode }) }); }); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'healthy', nodeVersion: process.version }); }); app.listen(PORT, () => { console.log(`🚀 EC2 deployment runner listening on port ${PORT}`); });