Files
test-alert/deploy.js
2026-04-07 21:51:19 +08:00

280 lines
8.7 KiB
JavaScript
Raw 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.
const { execSync } = require('node:child_process');
const fs = require('node:fs');
const path = require('node:path');
// --- 【配置区】 ---
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();