282 lines
8.8 KiB
JavaScript
282 lines
8.8 KiB
JavaScript
const { execSync } = require('node:child_process');
|
||
const fs = require('node:fs');
|
||
const path = require('node:path');
|
||
|
||
console.log(`脚本开始运行了!时间:${new Date().toLocaleString()}`);
|
||
console.log('环境变量检查 SENTRY_URL:', process.env.SENTRY_URL);
|
||
// --- 【配置区】 ---
|
||
const CONFIG = {
|
||
// 1. 开发源码所在的根目录
|
||
devProjectRoot: path.join(__dirname, './'),
|
||
// 2. 实际运行网站的目录 (目标位置)
|
||
prodProjectPath: 'D:\\phpstudy_pro\\WWW\\alert',
|
||
|
||
deployRemote: false,
|
||
// 需要配置ssh密钥对,否则会提示输入密码
|
||
remote: {
|
||
host: '1.2.3.4', // 服务器IP
|
||
user: 'root', // 用户名
|
||
path: '/www/wwwroot/my-app', // 服务器项目根目录
|
||
},
|
||
};
|
||
|
||
// 预定义几个常用路径
|
||
const DEV_DIST = path.join(CONFIG.devProjectRoot, 'dist');
|
||
const PROD_RELEASES = path.join(CONFIG.prodProjectPath, 'releases');
|
||
const PROD_CURRENT = path.join(CONFIG.prodProjectPath, 'current');
|
||
|
||
// 颜色工具
|
||
const log = {
|
||
info: (msg) => console.log(`\x1b[36m>>> ${msg}\x1b[0m`),
|
||
success: (msg) => console.log(`\x1b[32m✔ ${msg}\x1b[0m`),
|
||
warn: (msg) => console.log(`\x1b[33m!${msg}\x1b[0m`),
|
||
error: (msg) => console.log(`\x1b[31m✘ ${msg}\x1b[0m`),
|
||
};
|
||
|
||
async function main() {
|
||
// 解析参数: node scripts/deploy.js --rollback v1.0.0
|
||
const args = process.argv.slice(2);
|
||
const isStatus = args[0] === '--status' || args[0] === '-s';
|
||
const isRollback = args[0] === '--rollback' || args[0] === '-r';
|
||
const targetVersion = isRollback ? args[1] : '';
|
||
|
||
if (isStatus) {
|
||
showStatus();
|
||
return; // 结束执行
|
||
}
|
||
|
||
if (isRollback) {
|
||
if (!targetVersion) {
|
||
log.error(
|
||
'请指定要切换的版本号,例如: node scripts/deploy.js --rollback v1.0.0',
|
||
);
|
||
process.exit(1);
|
||
}
|
||
await performRollback(targetVersion);
|
||
} else {
|
||
await performFullDeploy();
|
||
}
|
||
}
|
||
|
||
// --- 【逻辑函数:秒级回退/切换】 ---
|
||
async function performRollback(version) {
|
||
log.info(`开始秒级切换至版本: ${version}...`);
|
||
|
||
// 1. 检查本地是否存在该版本
|
||
const localTarget = path.join(PROD_RELEASES, version);
|
||
if (!fs.existsSync(localTarget)) {
|
||
log.error(
|
||
`本地 releases 目录中未找到版本 ${version},请检查路径: ${localTarget}`,
|
||
);
|
||
process.exit(1);
|
||
}
|
||
|
||
// 2. 切换本地软链接
|
||
try {
|
||
updateLocalLink(localTarget);
|
||
log.success(`本地 Prod项目 已成功切换至 ${version}`);
|
||
} catch (e) {
|
||
log.error(`本地切换失败: ${e.message}`);
|
||
}
|
||
|
||
// 3. 切换远程软链接
|
||
if (CONFIG.deployRemote) {
|
||
try {
|
||
updateRemoteLink(version);
|
||
log.success(`远程服务器已成功切换至 ${version}`);
|
||
} catch (e) {
|
||
log.error(`远程切换失败: ${e.message}`);
|
||
}
|
||
}
|
||
|
||
log.info('切换任务结束。');
|
||
}
|
||
|
||
// --- 【逻辑函数:全流程发布】 ---
|
||
async function performFullDeploy() {
|
||
let version = '';
|
||
// 1. 获取 Git Tag
|
||
try {
|
||
version = execSync('git describe --tags --abbrev=0').toString().trim();
|
||
log.success(`读取到目标版本: ${version}`);
|
||
} catch (_e) {
|
||
log.error('无法获取 Git Tag。请确保本地有 tag (例如执行: git tag v1.0.0)');
|
||
process.exit(1);
|
||
}
|
||
|
||
log.info(`开始全流程发布版本: ${version}`);
|
||
|
||
// 2. 执行编译
|
||
try {
|
||
log.info('开始执行构建 (npm run build)...');
|
||
execSync('npm run build', {
|
||
stdio: 'inherit',
|
||
env: { ...process.env, UMI_APP_SENTRY_RELEASE: version },
|
||
});
|
||
log.success('项目构建成功');
|
||
} catch (_e) {
|
||
log.error('项目构建失败,请检查代码语法或 Umi/Webpack 配置');
|
||
process.exit(1);
|
||
}
|
||
|
||
// 3. 上传 SourceMap
|
||
try {
|
||
log.info('正在尝试上传 SourceMap 到 GlitchTip...');
|
||
// 如果这里报错,通常不中断部署,因为可能是网络波动,以后可以补传
|
||
execSync(
|
||
`sentry-cli releases files "${version}" upload-sourcemaps ./dist --url-prefix "~/"`,
|
||
{ stdio: 'inherit' },
|
||
);
|
||
log.success('SourceMap 上传成功');
|
||
} catch (_e) {
|
||
log.warn(
|
||
'SourceMap 上传失败!原因可能是:sentry-cli 未安装、环境变量未配置、.sentryclirc 文件未配置或网络不通。',
|
||
);
|
||
log.warn('您可以稍后手动补传,部署将继续进行...');
|
||
}
|
||
|
||
// 4. 清理 Map
|
||
try {
|
||
log.info('正在尝试清理 SourceMap...');
|
||
cleanSourceMaps(DEV_DIST);
|
||
log.success('SourceMap 清理成功');
|
||
} catch (_e) {
|
||
log.error(`SourceMap 清理失败: ${_e.message}`);
|
||
}
|
||
|
||
// 5. 归档到本地 Prod项目
|
||
try {
|
||
log.info('正在尝试归档到本地 Prod目录...');
|
||
const targetDir = path.join(PROD_RELEASES, version);
|
||
if (fs.existsSync(targetDir))
|
||
fs.rmSync(targetDir, { recursive: true, force: true });
|
||
if (!fs.existsSync(PROD_RELEASES))
|
||
fs.mkdirSync(PROD_RELEASES, { recursive: true });
|
||
fs.cpSync(DEV_DIST, targetDir, { recursive: true });
|
||
updateLocalLink(targetDir);
|
||
log.success('本地部署成功');
|
||
} catch (e) {
|
||
log.error(`本地归档失败: ${e.message}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
// 6. 远程部署
|
||
if (CONFIG.deployRemote) {
|
||
try {
|
||
log.info(`准备向远程服务器部署: ${CONFIG.remote.host}...`);
|
||
deployToRemote(version);
|
||
log.success('远程部署任务全部完成');
|
||
} catch (_e) {
|
||
log.error(`远程部署失败!错误详情: ${_e.message}`);
|
||
log.error(
|
||
'请检查:1.服务器IP/路径是否正确;2.是否配置了 SSH 免密登录;3.服务器磁盘空间是否充足。',
|
||
);
|
||
process.exit(1);
|
||
}
|
||
}
|
||
|
||
log.info(`\n🎉 所有任务执行完毕 [Version: ${version}]`);
|
||
}
|
||
|
||
// --- 新增状态查看函数 ---
|
||
function showStatus() {
|
||
log.info('正在查询当前版本状态...');
|
||
|
||
// 1. 查看本地版本 (Node.js 获取软链接指向的真实路径)
|
||
if (fs.existsSync(PROD_CURRENT)) {
|
||
// realpathSync 可以获取软链接指向的真实绝对路径
|
||
const realPath = fs.realpathSync(PROD_CURRENT);
|
||
const version = path.basename(realPath);
|
||
log.success(`本地 Prod 当前版本: ${version}`);
|
||
log.info(` 路径: ${realPath}`);
|
||
} else {
|
||
log.warn('本地尚未创建 current 软链接。');
|
||
}
|
||
|
||
// 2. 查看远程版本 (通过 SSH 执行 readlink)
|
||
if (CONFIG.deployRemote) {
|
||
try {
|
||
const { host, user, path: remotePath } = CONFIG.remote;
|
||
const remoteCurrentLink = `${remotePath}/current`;
|
||
// readlink -f 可以获取链接指向的完整路径
|
||
const cmd = `ssh ${user}@${host} "readlink -f ${remoteCurrentLink}"`;
|
||
const remoteRealPath = execSync(cmd).toString().trim();
|
||
const remoteVersion = path.basename(remoteRealPath);
|
||
log.success(`远程服务器当前版本: ${remoteVersion}`);
|
||
} catch (_e) {
|
||
log.error('无法获取远程版本状态,请检查 SSH 连接。');
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- 【通用工具函数】 ---
|
||
|
||
function updateLocalLink(targetDir) {
|
||
if (fs.existsSync(PROD_CURRENT)) {
|
||
fs.rmSync(PROD_CURRENT, { recursive: true, force: true });
|
||
}
|
||
const linkType = process.platform === 'win32' ? 'junction' : 'dir';
|
||
fs.symlinkSync(targetDir, PROD_CURRENT, linkType);
|
||
}
|
||
|
||
function updateRemoteLink(version) {
|
||
const { host, user, path: remotePath } = CONFIG.remote;
|
||
const remoteVersionDir = `${remotePath}/releases/${version}`;
|
||
const remoteCurrentLink = `${remotePath}/current`;
|
||
// 直接发送 ln 命令,前提是该目录已经在服务器 releases 里了
|
||
execSync(
|
||
`ssh ${user}@${host} "ln -snf ${remoteVersionDir} ${remoteCurrentLink}"`,
|
||
);
|
||
}
|
||
|
||
function deployToRemote(version) {
|
||
const { host, user, path: remotePath } = CONFIG.remote;
|
||
const localVersionDir = path.join(CONFIG.localReleasesDir, version);
|
||
const remoteVersionDir = `${remotePath}/releases/${version}`;
|
||
const remoteCurrentLink = `${remotePath}/current`;
|
||
|
||
// A. 在服务器创建目录
|
||
try {
|
||
execSync(`ssh ${user}@${host} "mkdir -p ${remotePath}/releases"`, {
|
||
timeout: 10000,
|
||
});
|
||
} catch (_e) {
|
||
throw new Error(`无法连接服务器或创建目录: ${_e.message}`);
|
||
}
|
||
|
||
// B. 使用 scp 上传文件夹
|
||
try {
|
||
log.info(` 正在上传至: ${remoteVersionDir} (请耐心等待...)`);
|
||
execSync(
|
||
`scp -r "${localVersionDir}" ${user}@${host}:${remotePath}/releases/`,
|
||
{ timeout: 60000 },
|
||
);
|
||
} catch (_e) {
|
||
throw new Error(`文件传输失败 (SCP): ${_e.message}`);
|
||
}
|
||
|
||
// C. 更新远程软链接
|
||
try {
|
||
const sshCmd = `ln -snf ${remoteVersionDir} ${remoteCurrentLink}`;
|
||
execSync(`ssh ${user}@${host} "${sshCmd}"`);
|
||
} catch (_e) {
|
||
throw new Error(`远程软链接更新失败: ${_e.message}`);
|
||
}
|
||
}
|
||
|
||
function cleanSourceMaps(dir) {
|
||
if (!fs.existsSync(dir)) return;
|
||
const files = fs.readdirSync(dir);
|
||
for (const file of files) {
|
||
const fullPath = path.join(dir, file);
|
||
if (fs.statSync(fullPath).isDirectory()) {
|
||
cleanSourceMaps(fullPath);
|
||
} else if (file.endsWith('.map')) {
|
||
fs.unlinkSync(fullPath);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 启动执行
|
||
main();
|