Initial TrueGrowth source import
This commit is contained in:
217
scripts/rollback-release.js
Normal file
217
scripts/rollback-release.js
Normal file
@@ -0,0 +1,217 @@
|
||||
#!/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);
|
||||
}
|
||||
Reference in New Issue
Block a user