Initial TrueGrowth source import

This commit is contained in:
2026-07-07 09:36:36 +08:00
commit 3b6781d695
2283 changed files with 691996 additions and 0 deletions

178
scripts/rollback-hybrid.js Normal file
View File

@@ -0,0 +1,178 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execSync, spawnSync } = require('child_process');
const ROOT_DIR = path.resolve(__dirname, '..');
/**
* 加载 .env 配置文件
*/
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;
}
/**
* 解析 SSH 密钥路径
*/
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);
}
/**
* 检查 sshpass 是否安装
*/
function checkSshpassInstalled() {
try {
execSync('which sshpass', { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
/**
* 转义 Shell 参数
*/
function shellEscape(value) {
return `'${String(value).replace(/'/g, `'\\''`)}'`;
}
/**
* 执行命令并实时输出
*/
function executeCommand(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: ROOT_DIR,
encoding: 'utf8',
stdio: 'inherit',
...options,
});
if (result.error) {
throw result.error;
}
if (result.status !== 0) {
throw new Error(`${command} 执行失败,退出码: ${result.status}`);
}
}
/**
* 主函数
*/
async function main() {
const args = process.argv.slice(2);
const isProd = args.includes('--prod');
const isTest = args.includes('--test') || !isProd; // 默认测试环境
const envFlag = isProd ? '--prod' : '--test';
const envName = isProd ? '生产' : '预览';
// 获取版本号(如果有的话)
const version = args.find(arg => !arg.startsWith('--'));
const config = loadEnvConfig();
if (!config.DEPLOY_HOST || !config.DEPLOY_USER) {
console.error('❌ 错误: 缺少 DEPLOY_HOST 或 DEPLOY_USER 配置');
process.exit(1);
}
if (!config.DEPLOY_SCRIPT_PATH) {
console.error('❌ 错误: 缺少 DEPLOY_SCRIPT_PATH 配置');
process.exit(1);
}
// 构建远程命令
// 关键:参数顺序必须是 --rollback [version] [--test|--prod]
// 否则 deploy.sh 可能会把 --test 误认为是版本号
let remoteCommand = `bash ${config.DEPLOY_SCRIPT_PATH} --rollback`;
if (version) {
remoteCommand += ` ${shellEscape(version)}`;
}
remoteCommand += ` ${envFlag}`;
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);
}
// 添加 -t 以支持 sudo 可能需要的 tty
sshArgs.push('-t');
sshArgs.push('-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null');
sshArgs.push(`${config.DEPLOY_USER}@${config.DEPLOY_HOST}`, remoteCommand);
console.log(`\n🚀 开始回滚${envName}环境...`);
console.log(`📄 远程脚本: ${config.DEPLOY_SCRIPT_PATH}`);
if (version) {
console.log(`📦 目标版本: ${version}`);
} else {
console.log(`📦 目标版本: 前一版本`);
}
try {
if (usePassword) {
if (!checkSshpassInstalled()) {
throw new Error('检测到 DEPLOY_SSH_PASSWORD但本机未安装 sshpass');
}
executeCommand('sshpass', ['-p', config.DEPLOY_SSH_PASSWORD, 'ssh', ...sshArgs]);
} else {
executeCommand('ssh', sshArgs);
}
console.log(`\n${envName}环境回滚指令执行完成`);
} catch (error) {
console.error(`\n❌ 回滚失败: ${error.message}`);
process.exit(1);
}
}
main();