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
JavaScript
#!/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}`);
});