UNPKG

fast-deployment

Version:

A lightweight Node.js package for rapid deployment of Vue.js, Next.js, and Nuxt.js applications

195 lines (183 loc) 7.33 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.deployToServer = deployToServer; const ssh2_sftp_client_1 = __importDefault(require("ssh2-sftp-client")); const ssh2_1 = require("ssh2"); const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const os_1 = __importDefault(require("os")); const config_1 = require("./config"); /** * Gets the path to the default SSH private key */ function getDefaultPrivateKeyPath() { return path_1.default.join(os_1.default.homedir(), '.ssh', 'id_rsa'); } /** * Gets the SSH connection config with private key */ function getSSHConfig(config) { const privateKeyPath = getDefaultPrivateKeyPath(); if (!fs_1.default.existsSync(privateKeyPath)) { throw new Error(`SSH private key not found at ${privateKeyPath}`); } return { host: config.serverHost, username: config.serverUser, privateKey: fs_1.default.readFileSync(privateKeyPath) }; } /** * Uploads the tarball to the server via SFTP and deploys it * @param config The deployment configuration * @param localTarballPath The local path to the tarball * @returns A promise that resolves when the deployment is complete */ async function deployToServer(config, localTarballPath) { const timestamp = new Date().toISOString().replace(/[-:.]/g, ''); const remoteTmpDir = path_1.default.join(config.serverDir, '.tmp'); const remoteTarball = path_1.default.join(remoteTmpDir, path_1.default.basename(localTarballPath)); const remoteDeployPath = path_1.default.join(config.serverDir, `${config.folderName}_${timestamp}`); console.log(`Deploying to server ${config.serverHost}...`); // 1. Upload tarball via SFTP await uploadTarball(config, localTarballPath, remoteTmpDir, remoteTarball); // 2. Execute deployment commands via SSH await executeDeployment(config, remoteTarball, remoteDeployPath, timestamp); console.log('Deployment completed successfully!'); } /** * Uploads the tarball to the server via SFTP */ async function uploadTarball(config, localTarballPath, remoteTmpDir, remoteTarball) { console.log(`Uploading tarball to ${config.serverHost}:${remoteTarball}...`); const sftp = new ssh2_sftp_client_1.default(); try { await sftp.connect(getSSHConfig(config)); // Create tmp directory if it doesn't exist await sftp.mkdir(remoteTmpDir, true); // Upload the tarball await sftp.fastPut(localTarballPath, remoteTarball); console.log('Tarball uploaded successfully.'); } finally { await sftp.end(); } } /** * Executes the deployment commands on the server via SSH */ async function executeDeployment(config, remoteTarball, remoteDeployPath, timestamp) { console.log('Executing deployment commands on server...'); return new Promise((resolve, reject) => { const ssh = new ssh2_1.Client(); ssh.on('ready', () => { const htaccessContent = (0, config_1.getHtaccessContent)(config.htaccessTemplate); const deploymentScript = generateDeploymentScript(config, remoteTarball, remoteDeployPath, htaccessContent); ssh.exec(deploymentScript, (err, stream) => { if (err) { reject(new Error(`SSH execution error: ${err.message}`)); return; } let output = ''; let errorOutput = ''; stream.on('data', (data) => { const chunk = data.toString(); output += chunk; console.log(chunk); }); stream.stderr.on('data', (data) => { const chunk = data.toString(); errorOutput += chunk; console.error(chunk); }); stream.on('close', (code) => { ssh.end(); if (code === 0) { resolve(); } else { reject(new Error(`Deployment failed with exit code ${code}:\n${errorOutput}`)); } }); }); }); ssh.on('error', (err) => { reject(new Error(`SSH connection error: ${err.message}`)); }); ssh.connect(getSSHConfig(config)); }); } /** * Generates the shell script to be executed on the server for deployment */ function generateDeploymentScript(config, remoteTarball, remoteDeployPath, htaccessContent) { const currentLink = path_1.default.join(config.serverDir, 'current'); const htaccessPath = path_1.default.join(remoteDeployPath, '.htaccess'); // Create a script with proper error handling return ` #!/bin/bash set -e echo "Starting deployment on server..." # 1. Create deployment directory mkdir -p "${remoteDeployPath}" echo "Created deployment directory: ${remoteDeployPath}" # 2. Extract the tarball tar -xzf "${remoteTarball}" -C "${remoteDeployPath}" echo "Extracted tarball to deployment directory" # 3. Copy .env file from current deployment if it exists if [ -L "${currentLink}" ] && [ -f "${currentLink}/.env" ]; then cp "${currentLink}/.env" "${remoteDeployPath}/.env" echo "Copied .env file from current deployment" fi # 4. Install dependencies based on application type cd "${remoteDeployPath}" npm install --production echo "Installed dependencies" # 5. Run Prisma generate if schema exists (for Next.js) if [ -f "${remoteDeployPath}/prisma/schema.prisma" ]; then npx prisma generate echo "Generated Prisma client" fi # 6. Create .htaccess file cat > "${htaccessPath}" << 'EOL' ${htaccessContent} EOL echo "Created .htaccess file" # 7. Update the symlink to point to the new deployment ln -snf "${remoteDeployPath}" "${currentLink}" echo "Updated 'current' symlink to point to new deployment" # 8. Restart application if PM2 is being used and app name is provided ${config.pm2AppName ? ` if command -v pm2 &> /dev/null; then # Try to reload the app first pm2 reload "${config.pm2AppName}" || NODE_ENV=production pm2 start npm --name "${config.pm2AppName}" -- run start echo "Application restarted with PM2" fi ` : ''} # 9. Perform health check if URL is provided ${config.healthCheckUrl && config.healthCheckStatus ? ` sleep 5 HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" ${config.healthCheckUrl}) if [ "$HTTP_STATUS" -eq ${config.healthCheckStatus} ]; then echo "Health check passed with status code ${config.healthCheckStatus}" else echo "Health check failed. Expected ${config.healthCheckStatus} but got $HTTP_STATUS" echo "Reverting to previous deployment..." # Find previous deployment directory PREVIOUS_DEPLOY=$(find ${config.serverDir} -maxdepth 1 -name "${config.folderName}_*" -type d | sort -r | sed -n '2p') if [ -n "$PREVIOUS_DEPLOY" ]; then ln -snf "$PREVIOUS_DEPLOY" "${currentLink}" ${config.pm2AppName ? `pm2 reload "${config.pm2AppName}"` : ''} echo "Reverted to $PREVIOUS_DEPLOY" fi exit 1 fi ` : ''} # 10. Clean up rm -f "${remoteTarball}" echo "Deployment completed successfully" `; }