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();