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
JavaScript
"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"
`;
}