Files
TrueGrowth/scripts/rollback-release.js

218 lines
5.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execFileSync, spawnSync } = require('child_process');
const ROOT_DIR = path.resolve(__dirname, '..');
function loadEnvConfig() {
const envPath = path.join(ROOT_DIR, '.env');
const config = {
DEPLOY_HOST: '',
DEPLOY_USER: '',
DEPLOY_PORT: '22',
DEPLOY_SSH_KEY: '',
DEPLOY_SSH_PASSWORD: '',
DEPLOY_SCRIPT_PATH: '',
};
if (!fs.existsSync(envPath)) {
return config;
}
const envContent = fs.readFileSync(envPath, 'utf8');
envContent.split('\n').forEach((line) => {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) {
return;
}
const match = trimmed.match(/^([^=]+)=(.*)$/);
if (!match) {
return;
}
const key = match[1].trim();
let value = match[2].trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
if (Object.prototype.hasOwnProperty.call(config, key)) {
config[key] = value;
}
});
return config;
}
function getCurrentVersion() {
const packagePath = path.join(ROOT_DIR, 'package.json');
return JSON.parse(fs.readFileSync(packagePath, 'utf8')).version;
}
function resolveSshKeyPath(sshKey) {
if (!sshKey) {
return '';
}
if (sshKey.startsWith('/')) {
return sshKey;
}
if (sshKey.startsWith('~/')) {
return path.join(process.env.HOME || '', sshKey.slice(2));
}
return path.join(process.env.HOME || '', sshKey);
}
function checkSshpassInstalled() {
try {
execFileSync('which', ['sshpass'], { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
function shellEscape(value) {
return `'${String(value).replace(/'/g, `'\\''`)}'`;
}
function getRollbackScriptPath(deployScriptPath) {
const normalizedPath = deployScriptPath.replace(/\\/g, '/');
const deployDir = path.posix.dirname(normalizedPath);
return path.posix.join(deployDir, 'rollback.sh');
}
function getRequestedVersion() {
const cliVersion = process.argv.slice(2).find((arg) => arg && !arg.startsWith('-'));
return cliVersion || getCurrentVersion();
}
function formatExecError(error) {
const stdout = Buffer.isBuffer(error.stdout) ? error.stdout.toString('utf8') : error.stdout || '';
const stderr = Buffer.isBuffer(error.stderr) ? error.stderr.toString('utf8') : error.stderr || '';
return stderr.trim() || stdout.trim() || error.message || '未知错误';
}
function executeCommand(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: ROOT_DIR,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
...options,
});
const stdout = result.stdout || '';
const stderr = result.stderr || '';
if (stdout) {
process.stdout.write(stdout);
}
if (stderr) {
process.stderr.write(stderr);
}
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
const error = new Error(stderr.trim() || stdout.trim() || `${command} 执行失败`);
error.status = result.status;
error.stdout = stdout;
error.stderr = stderr;
throw error;
}
return `${stdout}\n${stderr}`.trim();
}
function assertRollbackSucceeded(output) {
const failurePatterns = [
/取消回滚/,
/取消发布/,
/已取消/,
/操作已取消/,
/\[INFO\]\s*取消/,
];
if (failurePatterns.some((pattern) => pattern.test(output))) {
throw new Error('远端 rollback.sh 已取消执行');
}
}
function runRemoteRollback(config, version) {
if (!config.DEPLOY_HOST || !config.DEPLOY_USER) {
throw new Error('缺少 DEPLOY_HOST 或 DEPLOY_USER 配置');
}
if (!config.DEPLOY_SCRIPT_PATH) {
throw new Error('缺少 DEPLOY_SCRIPT_PATH 配置');
}
const rollbackScriptPath = getRollbackScriptPath(config.DEPLOY_SCRIPT_PATH);
const rollbackDir = path.posix.dirname(rollbackScriptPath);
const remoteCommand = `cd ${shellEscape(rollbackDir)} && ./rollback.sh --yes ${shellEscape(version)}`;
const sshArgs = [];
const sshKeyPath = resolveSshKeyPath(config.DEPLOY_SSH_KEY);
const useSshKey = sshKeyPath && fs.existsSync(sshKeyPath);
const usePassword = !useSshKey && config.DEPLOY_SSH_PASSWORD;
if (config.DEPLOY_PORT && config.DEPLOY_PORT !== '22') {
sshArgs.push('-p', config.DEPLOY_PORT);
}
if (useSshKey) {
sshArgs.push('-i', sshKeyPath);
}
sshArgs.push('-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null');
sshArgs.push(`${config.DEPLOY_USER}@${config.DEPLOY_HOST}`, remoteCommand);
console.log(`🚀 开始发布版本: ${version}`);
console.log(`📄 远程脚本: ${rollbackScriptPath}`);
try {
let output = '';
if (usePassword) {
if (!checkSshpassInstalled()) {
throw new Error('检测到 DEPLOY_SSH_PASSWORD但本机未安装 sshpass');
}
output = executeCommand('sshpass', ['-p', config.DEPLOY_SSH_PASSWORD, 'ssh', ...sshArgs]);
} else {
output = executeCommand('ssh', sshArgs);
}
assertRollbackSucceeded(output);
} catch (error) {
if (error && typeof error === 'object' && 'status' in error) {
throw new Error(formatExecError(error));
}
throw error;
}
}
function main() {
const version = getRequestedVersion();
const config = loadEnvConfig();
runRemoteRollback(config, version);
console.log('\n✅ 发布完成');
console.log(`💡 当前版本: ${version}`);
}
try {
main();
} catch (error) {
console.error(`\n❌ 发布失败: ${error.message}`);
process.exit(1);
}